Myket In-App Purchase Guide for Java and Kotlin
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:
- IPC Service Binding: Direct AIDL binding to the Myket market service.
- Broadcast Receiver Fallback: Secondary communication channel if direct service binding is restricted.
- 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
onActivityResultdelegation 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):
- Kotlin DSL
- Groovy
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
maven("https://maven.myket.ir")
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
maven { url "https://maven.myket.ir" }
}
}
2. Add Dependency
You can declare the dependency using Version Catalog (recommended) or directly in your app-level build.gradle.
Option A: Version Catalog (gradle/libs.versions.toml) (Recommended)
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
- Kotlin DSL
- Groovy
dependencies {
implementation("ir.myket:billing:2.2")
}
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):
- Kotlin DSL
- Groovy
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\""
)
}
}
android {
defaultConfig {
// ...
def marketApplicationId = "ir.mservices.market"
def marketBindAddress = "ir.mservices.market.InAppBillingService.BIND"
manifestPlaceholders = [
marketApplicationId: marketApplicationId,
marketBindAddress: marketBindAddress,
marketPermission: "${marketApplicationId}.BILLING"
]
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:
- Kotlin DSL
- Groovy
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\"")
}
}
}
android {
flavorDimensions "store"
productFlavors {
myket {
dimension "store"
def marketApplicationId = "ir.mservices.market"
def marketBindAddress = "ir.mservices.market.InAppBillingService.BIND"
manifestPlaceholders = [
marketApplicationId: marketApplicationId,
marketBindAddress: marketBindAddress,
marketPermission: "${marketApplicationId}.BILLING"
]
buildConfigField "String", "IAB_PUBLIC_KEY", "\"YOUR_MYKET_PUBLIC_KEY\""
}
bazaar {
dimension "store"
def marketApplicationId = "com.farsitel.bazaar"
def marketBindAddress = "ir.cafebazaar.pardakht.InAppBillingService.BIND"
manifestPlaceholders = [
marketApplicationId: marketApplicationId,
marketBindAddress: marketBindAddress,
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:
- Kotlin
- Java
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()
}
}
}
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;
public class MainActivity extends AppCompatActivity {
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, "MyketBilling");
mHelper.startSetup(result -> {
if (!result.isSuccess()) {
Log.e("Billing", "Setup failed: " + result.getMessage());
return;
}
// Billing connection ready
queryInventory();
});
}
}
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:
- 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.
- Non-Consumable Entitlements: Verify ownership for non-consumable items (such as premium unlocks) and grant access.
- Product Metadata: Retrieve up-to-date titles, descriptions, and localized prices configured in the Myket Developer Portal.
- Kotlin
- Java
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)
}
}
private void queryInventory() {
List<String> skuList = Arrays.asList("premium_upgrade", "coin_pack_100");
mHelper.queryInventoryAsync(true, skuList, (result, inventory) -> {
if (result.isFailure() || inventory == null) {
Log.e("Billing", "Failed to query inventory: " + result.getMessage());
return;
}
// Non-consumable
Purchase premiumPurchase = inventory.getPurchase("premium_upgrade");
boolean isPremium = (premiumPurchase != null && verifyDeveloperPayload(premiumPurchase));
// Pending unconsumed consumable
Purchase coinPurchase = inventory.getPurchase("coin_pack_100");
if (coinPurchase != null && verifyDeveloperPayload(coinPurchase)) {
consumeProduct(coinPurchase);
}
updateUi(isPremium);
});
}
Purchase Class Fields
| Method / Property | Type | Description |
|---|---|---|
getItemType() | String | Item type (inapp or subs) |
getOrderId() | String | Unique Myket transaction/order identifier |
getPackageName() | String | Package name of the purchasing application |
getSku() | String | Product SKU identifier |
getPurchaseTime() | long | Timestamp of transaction completion (epoch milliseconds) |
getPurchaseState() | int | Purchase state (0 = purchased/success) |
getDeveloperPayload() | String | Custom metadata string attached at the start of purchase |
getToken() | String | Unique purchase token used for verification and consumption |
getOriginalJson() | String | Raw JSON string payload received from billing service |
getSignature() | String | Digital RSA signature for verifying payload authenticity |
3. Launching a Purchase Flow (launchPurchaseFlow)
Initiate the checkout UI for an in-app product.
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.
- Kotlin
- Java
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
)
}
private fun buyItem(String sku) {
String developerPayload = generateSecurePayload();
mHelper.launchPurchaseFlow(
this,
sku,
(result, purchase) -> {
if (result.isFailure() || purchase == null) {
Log.e("Billing", "Purchase failed: " + result.getMessage());
return;
}
if (!verifyDeveloperPayload(purchase)) {
Log.e("Billing", "Authenticity verification failed.");
return;
}
if ("coin_pack_100".equals(purchase.getSku())) {
consumeProduct(purchase);
} else if ("premium_upgrade".equals(purchase.getSku())) {
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:
- Kotlin
- Java
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}")
}
}
}
private void consumeProduct(Purchase purchase) {
mHelper.consumeAsync(purchase, (consumedPurchase, result) -> {
if (result.isSuccess()) {
// Provision the goods to the user
creditUserCoins(100);
Log.d("Billing", "Product successfully consumed: " + consumedPurchase.getSku());
} else {
Log.e("Billing", "Consumption failed: " + result.getMessage());
}
});
}
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:
- Kotlin
- Java
override fun onDestroy() {
super.onDestroy()
iabHelper?.dispose()
iabHelper = null
}
@Override
protected void onDestroy() {
super.onDestroy();
if (mHelper != null) {
mHelper.dispose();
mHelper = 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
- 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.
- Authoritative Server-Side Validation:
- For high-security applications and anti-fraud protection, send
Purchase.getOriginalJson()andPurchase.getSignature()to your backend server. - Verify the signature using your Myket public key or via the Myket Developer API.
- For high-security applications and anti-fraud protection, send
- 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.
- Use the purchase token (
- Delivery Sequence for Consumables:
- Deliver consumable goods only after
consumeAsynccompletes successfully, or retain a robust local state machine to consume on subsequent app launches without duplicate crediting.
- Deliver consumable goods only after
- 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.
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).
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:
- Register your app package name in the Myket Developer Portal.
- Add your public key to your app's build configuration.
- Define one or more low-priced test products in the portal.
- Install the latest Myket application on your physical device or emulator.
- 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
queryInventoryAsyncon restart - Consumable item re-purchase limitation before consumption
- Web payment fallback flow when Myket app is absent