How to monetize Kotlin apps with Amazon

Anisha Malde - Feb 22 - - Dev Community

What’s the secret to making bank from your Android app? Publishing on Amazon Appstore! By doing so your app is able to reach millions of Amazon customers across multiple device form factors.

Now you might be thinking:

Do I really need to support another Appstore?

but I’m here to tell you don’t worry! Amazon’s IAP API allows your app to present, process, and fulfill purchases using built-in Amazon UI & modals saving you time and development effort.

Amazon Appstore IAP UI

Amazon handles the user journey (see animated example above) once the customer decides to purchase an item and completing when Amazon provides your app with either a receipt for the purchase or a status code.

Still not convinced? Let me show you in 7 steps how you can configure your app and implement the API. If you want to follow along in code, try out the boilerplate I created for this guide on GitHub: github.com/AmazonAppDev/amazon-iap-kotlin

Configure your app and IAP items

📲 Step 1: Add the Appstore SDK to your app

To get started with implementing Amazon’s IAP API you need to add the Appstore SDK to your app. With Maven you can integrate the Amazon Appstore SDK with one line of code:

implementation 'com.amazon.device:amazon-appstore-sdk:3.0.3'
Enter fullscreen mode Exit fullscreen mode

Tip: Make sure your project’s top-level build.gradle has the Maven Central repository defined!

Alternatively, you can visit the SDK Download page and to get the Appstore SDK for Android. The download includes the IAP JAR file and API documentation.

🔑 Step 2: Configure your app with your public key

Tip: You can skip this step if you are only planning on testing locally.

Next you need to configure your app with a public key. The public key is unique per app and is used to establish a secure communication channel between the Amazon Appstore and your app. To retrieve your public key, head to the Amazon Developer Portal Web Console.

  • If you have an existing app, create a new version of your app
  • If you are new to Amazon Appstore, create a new app and fill in the required fields.

Once you have completed this step, head over to the APK files tab to find the public key link in the upper-right area of the main page section:

The public key on the developer portal app details page

Clicking this link will download your AppstoreAuthenticationKey.pem file which includes your public key. Back in your app, create an assets folder within app/src/main and then copy over the AppstoreAuthenticationKey.pem into it.

🛍️ Step 3: Create your In-App Items

Now it’s time to map out your In-App Items. A quick refresh for In-App item categories and examples:

Purchase category What it is Examples
Consumables Purchase that is made, then consumed within the app Extra lives, extra moves, or in-game currency
Entitlements One-time purchase To unlock access to features or content within your app or game
Subscriptions Offers access to a premium set of content or features for a limited period of time Freemium model where the app itself is free but a customer can pay a $8 a month to unlock a premium tier

Understanding SKUs: Your app should contain unique identifiers for each purchasable item called SKUs (technically a Stock Keeping Unit). The SKU is unique to your developer account as purchasable items and SKUs have a 1:1 mapping. The SKU is how the Amazon client knows what your customer is trying to purchase, and will manage the purchase flow accordingly. Guidance on how to create the IAP Items can be found here but for now you just need to decide your items SKU’s e.g. com.amazon.example.iap.consumable

Guidance on how to create the IAP Items

Guidance on how to create the IAP Items

Guidance on how to create the IAP Items

Implementing the IAP API in your app

Now that we have configured our app, we can move on to the implementation, where you will need to add each component of the IAP API to your app. But before we do that let’s look at what the IAP API looks like under the hood.

The main components of the IAP API are:

  • PurchasingService: this is the Class that initiates requests through the Amazon Appstore.
  • ResponseReceiver: this is the Class that receives broadcast intents from the Amazon Appstore.
  • PurchasingListener: this is the Interface that receives asynchronous responses to the requests initiated by PurchasingService.

Main components of the IAP API

📡 Step 4: Add the response receiver to the Android Manifest

Since the In-App Purchasing API performs all of its activities in an asynchronous manner, your app needs to receive broadcast intents from the Amazon Appstore via the ResponseReceiver class. This class is never used directly in your app, but for your app to receive intents, you must add an entry for the ResponseReceiver to your AndroidManifest.xml file:

<application>  
...
    <receiver
        android:name="com.amazon.device.iap.ResponseReceiver"
        android:exported="true"
        android:permission="com.amazon.inapp.purchasing.Permission.NOTIFY">
        <intent-filter>
             <action android:name="com.amazon.inapp.purchasing.NOTIFY" />
        </intent-filter>
    </receiver>
</application>
Enter fullscreen mode Exit fullscreen mode

👂 Step 5: Initialise the PurchasingListener

Next you will need to initialise the PurchasingListener in the onCreate method so that your app can listen for and process the callbacks triggered by the ResponseReceiver. To do this, register the PurchasingListener in the MainActivity of your app.

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = ActivityMainBinding.inflate(layoutInflater)
        val view = binding.root
        setContentView(view)

        PurchasingService.registerListener(this, purchasingListener)
    }
Enter fullscreen mode Exit fullscreen mode

Tip: You should call registerListener before calling other methods in the PurchasingService class.

🧾 Step 6: Fetch the user and receipts data (PurchasingService)

PurchasingService

After initialising the PurcahsingListener you can use the methods in the PurchasingService to retrieve information on user and product data, make purchases, and notify Amazon about the fulfilment of a purchase.

In the onResume method you can call 3 methods:

  • getUserData which is method used to retrieve the app-specific ID for the user who is currently logged on. For example, if a user switched accounts or if multiple users accessed your app on the same device, this call will help you make sure that the receipts that you retrieve are for the current user account.
  • getPurchaseUpdates which is a method that retrieves all purchase transactions by a user since the last time the method was called that is purchases across all devices. You need to call getPurchaseUpdates in the onResume method to ensure you are getting latest updates.

Tip: This method takes a boolean parameter called reset. Set the value to true or false depending on how much information you want to retrieve:
True - retrieves a user's entire purchase history. You need to store this data somewhere, such as in server-side data cache or hold everything in memory.
False- returns a paginated response of purchase history from the last call to getPurchaseUpdates(). This is the Amazon recommended approach.

  • getProductData: which is a method used to retrieve item data for your SKUs, to display in your app.
const val parentSKU = "com.amazon.sample.iap.subscription.mymagazine"

override fun onResume() {
        super.onResume()

        PurchasingService.getUserData()
        PurchasingService.getPurchaseUpdates(true)

        val productSkus = hashSetOf(parentSKU)
        PurchasingService.getProductData(productSkus)
    }
Enter fullscreen mode Exit fullscreen mode

👂Step 7: Implement the Purchasing Listener

Purchasing Listener

Next you need to implement the PurchasingListener interface to process the asynchronous callbacks made in the PurchasingService. Every call initiated results in a corresponding response received by the PurchasingListener. e.g. onUserDataResponse() is invoked after a call to getUserData().

private var purchasingListener: PurchasingListener = object : PurchasingListener {

    override fun onUserDataResponse(response: UserDataResponse) {...}
    override fun onProductDataResponse(productDataResponse: ProductDataResponse) {...}
    override fun onPurchaseResponse(purchaseResponse: PurchaseResponse) {...}  
    override fun onPurchaseUpdatesResponse(purchaseUpdatesResponse: PurchaseUpdatesResponse){...} 
 };     
Enter fullscreen mode Exit fullscreen mode

Lets look into whats returned by each of these callbacks in detail:

UserDataResponse

The UserDataResponse provides the app-specific userId and marketplace for the currently logged on user.

private lateinit var currentUserId: String
private lateinit var currentMarketplace: String

override fun onUserDataResponse(response: UserDataResponse) {
            when (response.requestStatus) {
                UserDataResponse.RequestStatus.SUCCESSFUL -> {
                    currentUserId = response.userData.userId
                    currentMarketplace = response.userData.marketplace
                }
                UserDataResponse.RequestStatus.FAILED, UserDataResponse.RequestStatus.NOT_SUPPORTED, null -> {
                    Log.e("Request", "Request error")
                }
            }
        }
Enter fullscreen mode Exit fullscreen mode

Tip: Persist the User Id and marketplace into memory for possible future use by the app.

ProductDataResponse

The ProductDataResponse, retrieves item data keyed by SKUs about the items you would like to sell from your app.
This method also validates your SKUs so that a user's purchase does not accidentally fail due to an invalid SKU.
If the RequestStatus is SUCCESSFUL, retrieve the product data keyed by the SKU to be displayed in the app.
Additionally, if RequestStatus is SUCCESSFUL but has unavailable SKUs, you can call PurchaseUpdatesResponse.getUnavailableSkus() to retrieve the product data for the invalid SKUs and prevent your app's users from being able to purchase these products.

override fun onProductDataResponse(productDataResponse: ProductDataResponse) {
            when (productDataResponse.requestStatus) {
                ProductDataResponse.RequestStatus.SUCCESSFUL -> {
                    val products = productDataResponse.productData
                    for (key in products.keys) {
                        val product = products[key]
                        Log.v(
                            "Product:",
                            "Product: ${product!!.title} \n Type: ${product.productType}\n SKU: ${product.sku}\n Price: ${product.price}\n Description: ${product.description}\n"
                        )
                    }
                    for (s in productDataResponse.unavailableSkus) {
                        Log.v("Unavailable SKU:$s", "Unavailable SKU:$s")
                    }
                }
                ProductDataResponse.RequestStatus.FAILED -> Log.v("FAILED", "FAILED")

                else -> {
                    Log.e("Product", "Not supported")
                }
            }
        }
Enter fullscreen mode Exit fullscreen mode

PurchaseResponse

PurchaseResponse

Now let’s look into what happens if a user goes to make a purchase. Let’s say a purchase is initiated when a user taps a button and this action triggers the PurchasingService.purchase() method to run and initialise a purchase.

binding.subscriptionButton.setOnClickListener { PurchasingService.purchase(parentSKU) }
Enter fullscreen mode Exit fullscreen mode

Once the purchase is initiated the onPurchaseResponse method is used to determine the status of a purchase initiated within your app. If the request is successful, you can call the notifyFulfillment method to send the FulfillmentResultof the specified receiptId.

override fun onPurchaseResponse(purchaseResponse: PurchaseResponse) {
            when (purchaseResponse.requestStatus) {
                PurchaseResponse.RequestStatus.SUCCESSFUL -> PurchasingService.notifyFulfillment(
                    purchaseResponse.receipt.receiptId,
                    FulfillmentResult.FULFILLED
                )
                PurchaseResponse.RequestStatus.FAILED -> {
                }
                else -> {
                    Log.e("Product", "Not supported")
                }
            }
        }
Enter fullscreen mode Exit fullscreen mode

Tip: If the PurchaseResponse.RequestStatus returns a result of FAILED, this could simply mean that the user canceled the purchase before completion.

PurchaseUpdatesResponse

Finally if you want to get the most up to date information about the user purchases you can use the response returned by getPurchaseUpdates(). Triggering this function, initiates the PurchasingListener.onPurchaseUpdatesResponse() callback. When the PurchasingListener.onPurchaseUpdatesResponse() callback is triggered, it retrieves the purchase history.

Check the request status returned by PurchaseUpdatesResponse.getPurchaseUpdatesRequestStatus(). If requestStatus is SUCCESSFUL, process each receipt.

You can use the getReceiptStatus method to retrieve details about the receipts. To handle pagination, get the value for PurchaseUpdatesResponse.hasMore(). If PurchaseUpdatesResponse.hasMore() returns true, make a recursive call to getPurchaseUpdates(), as shown in the following sample code.

Tip: Persist the returned PurchaseUpdatesResponse data and query the system only for updates.

   override fun onPurchaseUpdatesResponse(response: PurchaseUpdatesResponse) {
       when (response.requestStatus) {
           PurchaseUpdatesResponse.RequestStatus.SUCCESSFUL -> {
               for (receipt in response.receipts) {
                   if (!receipt.isCanceled) {
                       binding.textView.apply {
                           text = "SUBSCRIBED"
                           setTextColor(Color.RED)
                       }
                   }
               }
               if (response.hasMore()) {
                   PurchasingService.getPurchaseUpdates(true)
               }
           }
           PurchaseUpdatesResponse.RequestStatus.FAILED -> Log.d("FAILED", "FAILED")
           else -> {
               Log.e("Product", "Not supported")
           }
       }
   }
Enter fullscreen mode Exit fullscreen mode

And thats it, you should now have successfully implemented the IAP API in your app! The following is a demo of the boilerplate sample app I created for this guide:

So what next?

🧪 Next: Testing!

Now that you have implemented the IAP API you can go ahead and test your implementation. You can use Amazon App Tester to test your IAP API code before you submit your app to the Amazon Appstore.

Tip: If when tested your purchases don’t fulfil, make sure you are using the latest version of the App Tester from the Amazon Appstore, this usually fixes this.

You can also checkout a livestream we did with AWS Amplify where we show you how you can build a brand new android app using AWS Amplify, monetize it with the IAP API and then finally test it with the App Tester in under an hour! Or, you can get started right away by downloading my boilerplate code.

Finally, you can stay up to date with Amazon Appstore developer updates on the following platforms:

📣 Follow @AmazonAppDev on Twitter
📺 Subscribe to our Youtube channel
📧 Sign up for the Developer Newsletter

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .