tl;dr Be aware of the different regions when setting up test accounts for Live App Testing (Amazon.de != Amazon.com). If Live App Testing throws INVALID_SKU just wait a few days and try again. In all cases call NotifyFulfillment().

About

We like to publish our game Nory’s Escape on Amazon Appstore. To introduce it to a wider audience and publish it without an upfront paywall we choose to use InApp payment. We won first price during Gamescon 2017 and on Amazon we exclusively offer the first part of the game for free.

In this article I like to collect my findings and share some of the things I learned during the integrating of the Payment process.

Setup Amazon Developer Account

Creating the dev account itself with the ability to sell stuff is strongly bound to your local tax situation. Depending where you live US? yes/no and what kind of individual you are company? yes/no you have to provide different tax details. With teamnory we solved that with the help of our tax clerk and I suggest to do the same.

Setup an app on Amazon Developer

To allow the game to offer InApp payments I start by adding the App to our account on Amazon Developer. I put in some light configuration:

Create IAP item
Create IAP item

Choose the right plugin for Unity

On Server side we should be good for now. If we take a look at the Unity side we see different things. For one it seems Amazon provides a plugin. Unity offers IAP via Unity Serives.

To be more or less free from an additional account with additional configuration and additional GDPR considerations I give the Amazon Plugin a go.

Install plugin, understand the API

Following the docs I installed the IAP plugin v2.0. I also renamed the AmazonIapV2SampleAndroidManifest.xml to AndroidManifest.xml and made sure that the automerged result looks fine for the game.

According to the iap-docs we should now be able to call the following functions:

  • GetUserData
  • Purchase
  • GetProductData
  • GetPurchaseUpdates
  • NotifyFulfillment

The API, naturally, is async and deals with callbacks. Except for one function they all need a handler function which is called every time the server returns with a request result. For identification they offer a requestID. Using GetUserData it should be possible to check if the store is accessible at all and if a Marketplace exists in the current environment.

Okay, reading into all of this I personally would translate it like this:

Function Amazon Desc My Desc
GetUserData Initiates a request to retrieve the user ID and marketplace of the currently logged-in user store available
Purchase Initiates a purchase-flow for a product. Open purchase dialog
GetProductData Initiates a request to retrieve item data for up to one hundred SKUs. Get generic product data from server. Does not say if the user bought the product
GetPurchaseUpdates Initiates a request to retrieve updates about items the customer has purchased and/or cancelled. Get receipts of previously bought/abort products.
NotifyFulfillment Notifies Amazon about the purchase fulfillment. Notify that a consumable has been used. Notify if your app can provide the bought service to the user.

Design a payment workflow

For Nory’s Escape it is planned to offer the level sets as InApp payment only once. If you bought the level set before you don’t have to pay for it again. To support this we need to make sure that we cache a validated payment and offer means to revalidate an existing payment.

With the API at hand the workflow when starting the app could be like this:

Identify current payment state
Identify current payment state

If the app cannot find the receipt it should show the payment button. This is safe as the Purchase request can return ALREADY_PURCHASED. If the button is pressed we could do this:

Buy button
Buy button

Basically we just check if we have a valid payment according to the local storage. If not we ask Amazon for an update and offer a button to perform the payment if we cannot determine the pay state.

The only thing that wondered me a bit is notifyFulfillment. The docs say I have to call it once the receipt is full filled. It never really states in what scenario I have to call it. However, reading this I assume I only need to call it for consumables. In the google-iap-to-amazon-guide they say it’s the equivalent to consumeAsync().

notifyFulfillment is a requirement. According to this I actually do have to call notifyFulfillment after a purchase of an Entitlement. Also, aside from the Unity explanation, there is a whole section (Implementing notifiyFulfillment) in the migration guide from iapv1 to iapv2.

Implementation and sandbox testing IAP with App Tester

Following the IAP Testing Overview we can test in three modes: Sandbox, Live App Testing and Production.

Sandbox is the most reasonable to start with. It is coupled with something the docs call SDK Tester or App Tester. SDK Tester seems to be old but if it is still installed I need to uninstall it. App Tester needs to be downloaded from Amazon Appstore on every sandbox testing device.

I have to put a json file on the device and can test the app. That is a very lean approach and compared to other vendors. Efficient and nice.

Things I’ve noticed during implementation / sandbox testing

  • When calling GetPurchaseUpdates() you have to provide an ResetInput object. I was a little confused what it is what I’m resetting. In my naming world I might have called it: ContinueList or ContinueQueue.
  • notifyFulfillment needs to be called. Even for Entitlement items.
  • When the user hits the cancel button in the InApp dialog the API returns an FAILED. The way I see it we cannot distinct between user cancel and network failure.
  • I need to provide payment data on the Amazon account to download Amazon App Tester.
  • It seems, once published, you cannot edit InApp item data in the project on Amazon developer. It seems you can only edit InApp items after some time.

The sandbox testing works incredible. I was able to identify payment issues and thanks to the direct approach I never had to wait for submission or implement fake-errors within my code to test edge cases. You can just simply configure the scenarios using App Tester. Nice.

Live App Testing (LAT): Part I

Using Live App Testing I basically can:

  • simulate entering all data required for submitting the app (including videos, images, etc)
  • visiting the shop page
  • simulate purchase of the app
  • simulate purchase InApp goods
  • invite a set of users that see the test app

Sounds easy enough. Actually, it’s a great experience. However, invite Live App Testing Users is a bit tricky. I’m having trouble to receive test invitiation emails. Reading this I noticed that it’s a requirement to have marketing emails allowed.

How to setup marketing preferences: https://www.amazon.com/gp/gss/ccp/
How to setup marketing preferences: https://www.amazon.com/gp/gss/ccp/

I checked that but it’s in place. Hm…

A little later that evening I got two emails:

First email: success!
First email: success!

Second email: failed!
Second email: failed!

In my first test I entered two test users. It seems that according to amazon my personal account is restricted and my developer account is not. I’m pretty sure both accounts allow Amazon marketing communications.

Investing further into that email issue. It is important to understand, that the configuration depends on the domain.

Amazon.com != Amazon.de

If I follow the given link, sign in with my credentials and check the email settings they are fine. However, if I sign in on Amazon.de I can see my local settings. In my local settings I in fact opted out marketing.

Depending on where you live you seem to need a different link:

Country Link to marketing preferences
US https://www.amazon.com/gp/gss/ccp/
Germany https://www.amazon.de/gp/gss/ccp/
XXX https://www.amazon.XXX/gp/gss/ccp/
This actually helped for one account…
…but for one account I still have the issue.

First results

Once I received a Live App Tester invitation email I followed the instruction and here are the first results:

What worked:

  • Create test app
  • Submit it to the test store
  • Send (most) of the test-invitation emails
  • Install it to my Amazon Tablet

Nory's Escape in Live App Testing
Nory's Escape in Live App Testing

What didn’t work:

  • Cannot install the App from Amazon.com but only from Amazon.de. Not a big deal as I assume that it’s only because of my physical position.
  • Some detail information are not correct (see image above, ?)
  • InApp Payment failed (see below)

Fixing INVALID_SKU

Running the app in Live App Testing mode I’m unable to perform InApp purchase. Let’s try to find out what is wrong.

Nory's Escape in Live App Testing, payment failed
Nory's Escape in Live App Testing, payment failed

Digging into LogCat I found the json response of the Amazon server:

// I/AmazonIapV2: onPurchaseResponse:(com.amazon.device.iap.model.PurchaseResponse@15fd9d9b, 
{
  "requestId": "XXXXXXX-...", 
  "purchaseRequestStatus": "INVALID_SKU", 
  "userId": {
      "userId": "XXXX=",
      "marketplace": "DE"
  }, 
  "receipt": null
}

The SKU is invalid? Welp… that’s strange. I used the same SKU I used in the sandbox tools. I downloaded the sandbox-test data (amazon.sdktester.json) from the page. It should be identical.

Both SKU's are live
Both SKU's are live

Attempt 1: replace items in amazon.sdktester.json

My first idea is to make sure there is no conflict between the test environment and the App Tester configuration. It would make sense to me because now we have two concepts providing free access to the same InApp payment items.

Result: No

I still get the same error.

Attempt 2: Check the obfuscation warning

Potential source for InApp failing
Potential source for InApp failing
I must admit I never give obfuscation a thought till now. The core value of the game is the level data not the code. But maybe obfuscation is automatically enabled. Here one can read that in fact in new Android Studio builds R8 based obfuscation is in place and in that event the InApp payment fails. I’m using an exported project from Unity to gradle. Let’s see if I’m doing obfuscation.

buildTypes {
  debug {
    jniDebuggable true
  }
  release {
    // Set minifyEnabled to true if you want to run ProGuard on your project
    minifyEnabled false
    proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-unity.txt'
    signingConfig signingConfigs.release
  }
}

Note In a later investigation I build the app with the correct obfuscation configuration and still had the issue.

Result: No (mabye)

minifiyEnable false feels to me like: no obfuscation. Hm… but just to be on the safe side I add the given lines to the pro guard file.

Quote: Note that starting with Unity 2017.1, you can specify to use a proguard file [source]. Which means, since I’m using Unity 5.x.x, I need to pimp my build.sh:

# Add amazon IAP obfuscation lines
if grep -q "com.amazon" ./proguard-unity.txt
then
    # amazon already in file
    echo "skip obfuscation."
else
    # no amazon in file yet
    echo "Inject obfuscation lines."
    echo "-dontwarn com.amazon.**" >> ./proguard-unity.txt
    echo "-keep class com.amazon.** {*;}" >> ./proguard-unity.txt
    echo "-keepattributes *Annotation*" >> ./proguard-unity.txt
fi

Attempt 3: Add debug log output to see the results of GetProductData

So far I tried to build the app without call GetProductData(). However, to make sure the app has access to the right data on the Amazon server, let’s see if the products appear correctly.

…waiting for submission.

A quick side note: it seems the submission status Publishing is not documented.

Hm… this is not very promising.

I still had an old test running while I was waiting for the new to be submitted. As the old test doesn’t help me anyway I simply ended it. And then the UI changed to this:

Missing testers
Missing testers

So, now, I am missing testers? Okay, let’s see what happens if I add then. It actually sends out new emails and installs the new version!.

Now let’s check the log out.

Nullpointer Exception???

SkusInput skuList = new SkusInput();
skuList.Skus.Add(...); // <<<<<<<<<<<< causes nullpointer!
amazonService.GetProductData(skuList);
Stupid me. I should have started the app at least once in Unity before I initialize the long test-submission process.

Let’s do this again.

…next submission and more time for coffee.

Interesting: even after the new invitation email for the new version is out it does not mean that the link within the email points to the last version. One has to carefully monitor the version on the shop-page to make sure you actually download the latest submission.

Result: no

This wasn’t it. GetProductData() populates UnavailableSkus.

It means the products are not available to my app in general. But why?

Attempt 4: Investigating IllegalBlockSizeException

While I checked the log output I noticed this error block:

07-26 16:34:38.596 28786-28803/? E/AmazonAppstore.SimpleObfuscator: Error preparing data. 8e5760e7
    javax.crypto.IllegalBlockSizeException: last block incomplete in decryption
        at com.android.org.bouncycastle.jcajce.provider.symmetric.util.BaseBlockCipher.engineDoFinal(BaseBlockCipher.java:850)
        at javax.crypto.Cipher.doFinal(Cipher.java:1340)
        at com.amazon.mas.client.util.encryption.SimpleObfuscator.deobfuscate(SimpleObfuscator.java:381)
        at com.amazon.mas.client.util.encryption.SimpleObfuscator.deobfuscate(SimpleObfuscator.java:120)
        at com.amazon.mas.client.iap.datastore.IAPCheckpointTable.getCheckpoint(IAPCheckpointTable.java:167)
        at com.amazon.mas.client.iap.datastore.IAPDataStoreImpl.getCheckpoint(IAPDataStoreImpl.java:346)
        at com.amazon.mas.client.iap.receipt.SyncReceiptsManager.syncReceipts(SyncReceiptsManager.java:150)
        at com.amazon.mas.client.iap.command.purchaseupdates.PurchaseUpdatesAction.executeRequestInner(PurchaseUpdatesAction.java:124)
        at com.amazon.mas.client.iap.command.purchaseupdates.PurchaseUpdatesAction.executeRequest(PurchaseUpdatesAction.java:87)
        at com.amazon.mas.client.iap.command.purchaseupdates.PurchaseUpdatesAction.executeRequest(PurchaseUpdatesAction.java:41)
        at com.amazon.mas.client.iap.command.IapCommandAction.execute(IapCommandAction.java:58)
        at com.amazon.venezia.command.action.CommandActionChain.execute(CommandActionChain.java:31)
        at com.amazon.venezia.command.action.CommandActionChain.execute(CommandActionChain.java:31)
        at com.amazon.venezia.command.action.CommandActionChain.execute(CommandActionChain.java:31)
        at com.amazon.venezia.command.action.CommandActionChain.execute(CommandActionChain.java:31)
        at com.amazon.venezia.command.action.CommandActionExecutor.execute(CommandActionExecutor.java:40)
        at com.amazon.venezia.command.CommandServiceStub.execute(CommandServiceStub.java:198)
        at com.amazon.venezia.command.CommandService$Stub.onTransact(CommandService.java:56)
        at android.os.Binder.execTransact(Binder.java:446)

As this only and always appeared when the accessed the IAP api it might be worth checking (especially if one reads: PurchaseUpdatesAction).

Result: no. nothing conclusive

Searching for this I found this and this. But not much more.

In a later attempt (on a none-Amazon device) I discovered that this error does not occur at all.

Attempt 5: Contacting support I

I’m out of ideas for now. Let’s see if they can help me

Result: not yet

They answered to my request with the hint check obfuscation. It’s a natural first assumption. However, I’m pretty sure it’s not the case hope they take another look.

To be extra sure I followed the APK Tool - Install Instructions. Using the APK tool I decompiled the APK to see if somehow the obfuscation was still active and wrongly configured. It was not.

Attempt 6: Contacting support II

As they simply bounced my first request I asked them again to check the case. I explained that obfuscation is not the issue.

Result: no

I’m not sure if they’ll ever answer. The first one came instantly, the second however takes days now. Also similar questions were not answered in the forum as well.

Attempt 7: Try it on a None-Amazon device

While I’m waiting for the support I had the idea to test it on a Google Playstore Phone. I downloaded and connected the phone with my Amazon Appstore account and checked the logcat there.

Result: no. Fails too, but differently!

I was able to install the app on my device and tried the purchase. It failed with a different error.

InApp purchase error message on None-Amazon-Device
InApp purchase error message on None-Amazon-Device

That’s odd! Why would it open a differently looking box with the words Website Temporarily Unavailable? Website?

Checking logcat I also noticed that here I never see the AmazonAppstore.SimpleObfuscator: Error. Or any error.

Attempt 8: Crawl through the API doc + compare to SDK example

Obviously I read most of the documentation but I concentrated on the Unity plugin docs so far. Maybe there is some hint in the regular Android SDK docs. Some kind of a switch, configuration or general step that I need to take care of before submitting to the LAT.

Result: no

If it in there I’v missed it :(

Attempt 9: Try access the IAP using the REST API

It’s hard for me to proof but my gut tells me this is somehow related to either: Unity, Unity version or regional lock. Rule out the Unity factor is a bit time consuming. However, to see if the IAP products are available at all I can quickly modify the IAP v2.0 Example Web App.

Result: no

I missed the fact that Web App in this context really and only means App build with web tools. Using the web API for testing I would still need to perform a new submission. However, as the support team is still on that issue I don’t like to modify the existing state.

Others facing the same issue: IAP not working in Live App Testing - Unavailable SKU. Even the Exception occurs in the same way in the log.

According to this: IAP in Live App Testing gives error it seems that LAT does not work…

Result: not sure

It works in production?
It works in production?

This would be an reasonable answer. But really? If it were just that wouldn’t they just say so in the forum?

Attempt 11: Modify Android-SDK version

Maybe, for some reason, the SDK configuration breakts compability with parts of the encryption classes which might…

… I don’t know. Just trying things now.

Waiting for submission.

Result: no

It’s not the SDK version.

Attempt 12: Patience

Just wait.

Result: YES

A while later I got an email from amazon support:

[...] It can take some time for IAP items to propagate across all systems.

LAT in-app payment
LAT in-app payment

It works now.

Live App Testing (LAT): Part II

Now that the products are in place and can be accessed it’s time to see if the LAT environment works.

Aside from many small usability things to consider the follwing use cases are important to check:

Test Expected behavior
(A) App installed for the first time. No purchase exists In this scenario the additional content should not be usable. An option to purchase the items should be visible
(B) App installed for the first time on the device. The account already ownes an in-app item The app should automatically detect the existing purchase of the account and unlock the additional content automatically.
(C) Same as (B) but in this case the user has no internet connection during app startup. The automatic detection should fail. The app should not block but extra content can’t be used. Next time the app starts with a connection the additional content should automatically become available.
(D) Same as (C) but the automatic unlock fails or the user deleted app data. The additional content should not be usable. However, if user tries to buy the content it should unlock the content without extra charge.

Now, let’s check the app using the LAT environment:

Test Amazon Device Other Android Device
(A) OK OK
(B) OK OK
(C) OK OK
(D) OK OK

Analog to the app tester results it turns out the implementation stands. The only thing that was unexpected is that in scenario (C) the purchase can be identified without an active connection. I assume the Amazon appstore internally caches purchases and therefore allows reactivation even without an active connection.

Conclusion and findings

Adding Amazon Appstore in-app-purchases to an Unity is simple enough. One only has to setup some meta data and thanks to the provided testing tools one can make sure that everything works as expectd. Especially the App Tester is a great tool to quickly build a solid workflow and test all edge cases without having to wait for submission or anything. The Live App Testing (LAT) workflow is exactly what you would expect for the next iteration once App Tester results look fine. It is very unfortunate, though, that there is this undefined time of waiting for the in-app-items to be actually useable. However, once that’s done LAT works perfectly and increases the overall confidence in the implementation.

To summarize my findings I would say they are:

  • Don’t just read the Unity-plugin-docs but also the usual IAP docs
  • Call NotifyFulfillment for all purchase types
  • GetPurchaseUpdates is kind of what I would expect GetProductData to be (but maybe that’s just me)
  • Test the workflow with App Tester early. It is brilliant. No need to write a fake shop
  • For Live App Testing email invitation make sure your tester use the right Amazon region (us, de, etc) when setting up their marketing preferences.
  • If App Tester is successfull and Live App Testing throws INVALID_SKU just wait a few more days and try again.