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:
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. |
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:
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:
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.
I love sequencediagram.org
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 anResetInput
object. I was a little confused what it is what I’m resetting. In my naming world I might have called it:ContinueList
orContinueQueue
. 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.
I checked that but it’s in place. Hm…
A little later that evening I got two emails:
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
What didn’t work:
- Cannot install the App from
Amazon.com
but only fromAmazon.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.
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.
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
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:
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.
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.
Attempt 10: More web search
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
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.
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 expectGetProductData
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.