Google PlayStore IAPs – ShiVa Engine

Google PlayStore IAPs

In this tutorial, you will learn how to integrate Google PlayStore in-app purchases into your ShiVa application, so you can have a nice store front like this:
store

Shiva Setup

Store_PlayStore AIModel

Create a new AIModel with the name “Store_PlayStore”. It has to have the following custom handlers in it:

Store_PlayStore.onAddProduct ( sProductIdentifier, sConsumable )
Store_PlayStore.onBuyProduct ( sProductIdentifier )
Store_PlayStore.onRestore ( )
Store_PlayStore.onPlayStoreBuyProductFailed ( )
Store_PlayStore.onPlayStoreProductPurchased ( sProductIdentifier )
Store_PlayStore.onPlayStoreProductRestored ( sProductIdentifier )

onAddProduct

Let’s describe each of the handlers. The first 3 are callable.

Store_PlayStore.onAddProduct ( sProductIdentifier, sConsumable )

Use it on init to store the information of the product, whether it is consumable or not. This is later used for restoring purposes.
Usage example:

this.sendEvent ( "onAddProduct", "premium", "true" )
this.sendEvent ( "onAddProduct", "coins", "false" )

onBuyProduct

Store_PlayStore.onBuyProduct ( sProductIdentifier )

Use it when the player wants to buy something.
Usage example:

user.sendEvent ( hUser, "Store_PlayStore", "onBuyProduct", "premium" )
user.sendEvent ( hUser, "Store_PlayStore", "onBuyProduct", "coins" )

onRestore

Store_PlayStore.onRestore ( )

Use it when the player wants to restore an item they bought.
Usage example:

user.sendEvent ( hUser, "Store_PlayStore", "onRestore" )

Result handlers

The following AI handlers are called when the action is done.

Store_PlayStore.onPlayStoreBuyProductFailed (  )

Used when a purchase had failed, for any reason: Not enough money, no internet connection, or just closing the popup.

Store_PlayStore.onPlayStoreProductPurchased ( sProductIdentifier )

Used when a product was successfully bought.

Store_PlayStore.onPlayStoreProductRestored ( sProductIdentifier )

Used when a product was successfully restored.
Keep in mind that in this implementation of Google iAPs, one item is restored per restore call. Also, if no item is restored, or there was an error, the parameter “sProductIdentifier” is going to be empty. This is the way to check if an item was really restored:

if (sProductIdentifier ~= "") then
	-- an item is restored
else
	-- no item to restore
end

Eclipse / Android Studio Setup – JNI

Now it is time to make it possible for our application to “communicate” with our Java code. This is where JNI comes into play.

S3DClient.cpp globals

In your Eclipse/AS project, locate “jni” folder. Open up “S3DClient.cpp” file. Locate the JNI Globals code block by searching for this line (Ctrl + F):

// @@BEGIN_JNI_GLOBALS@@

After all the defined variables, add these two lines:

static char str_AddProduct1 [255] = "";
static char str_AddProduct2 [255] = "";

Make sure you add them before this line:

// @@END_JNI_GLOBALS@@

S3DClient.cpp callbacks

By searching, locate this line:

// @@BEGIN_JNI_CALLBACKS@@

Add all of the following code after it.

//==========================================================================================
//	custom voids
//********************************************************************
//	add purchase product
void onAddProduct(unsigned char _iArgumentCount, const void *_pArguments, void *_pUserData)
{
	if ( _pArguments && ( _iArgumentCount == 2 ) )
	{
		const S3DX::AIVariable *pVariables = (const S3DX::AIVariable *)_pArguments ;
		if ( pVariables[0].GetType ( ) == S3DX::AIVariable::eTypeString )
		{
			strncpy ( str_AddProduct1, pVariables[0].GetStringValue ( ), 254 ) ;
		}
		if ( pVariables[1].GetType ( ) == S3DX::AIVariable::eTypeString )
		{
			strncpy ( str_AddProduct2, pVariables[1].GetStringValue ( ), 254 ) ;
		}
	}
	if ( pJavaVM )
	{
		JNIEnv *pEnv ;
		if    ( pJavaVM->GetEnv ( (void**) &pEnv, JNI_VERSION_1_4 ) >= 0 )
		{
			jclass pClass  = pEnv->FindClass ( "com/company/game/Bridge" );
			if   ( pClass != 0 )
			{
				jmethodID pMethod = pEnv->GetStaticMethodID ( pClass, "onAddProduct", "(Ljava/lang/String;Ljava/lang/String;)V");
				if      ( pMethod )
				{
				pEnv->CallStaticVoidMethod ( pClass, pMethod, pEnv->NewStringUTF ( str_AddProduct1 ), pEnv->NewStringUTF ( str_AddProduct2 ) ) ;
				}
			}
		}
	}
}
// add purchase product
//********************************************************************
//********************************************************************
//	purchase
void onBuyProduct(unsigned char _iArgumentCount, const void *_pArguments, void *_pUserData)
{
	JNIEnv *pJNIEnv = GetJNIEnv();
	if (pJNIEnv)
	{
		if ( _pArguments && ( _iArgumentCount > 0 ) )
		{
			const S3DX::AIVariable *pVariables = (const S3DX::AIVariable *)_pArguments ;
			for ( uint8_t i = 0 ; i < _iArgumentCount ; i++ )
			{
				//We only want strings
				if(pVariables[i].GetType() == S3DX::AIVariable::eTypeString)
				{
					jclass pJNIActivityClass = pJNIEnv->FindClass ( "com/company/game/Bridge" );
					if(pJNIActivityClass == NULL)
						LOGI("jclass was null!?!");
					else
					{
						jmethodID pJNIMethodID = pJNIEnv->GetStaticMethodID(pJNIActivityClass, "onBuyProduct", "(Ljava/lang/String;)V" );
						if(pJNIMethodID == NULL)
							LOGI("jmethodID was null!?!?");
						else
						{
							//Create a new string
							jstring arg;
							arg = pJNIEnv->NewStringUTF(pVariables[i].GetStringValue());
							//Call the method and pass the string parameter along
							pJNIEnv->CallStaticVoidMethod(pJNIActivityClass, pJNIMethodID, arg);
							//Free the string
							pJNIEnv->DeleteLocalRef(arg);
						}
					}
				}
			}
		}
	}
}
// purchase
//********************************************************************
//********************************************************************
//	restore purchase
void onRestore(unsigned char _iArgumentCount, const void *_pArguments, void *_pUserData)
{
	JNIEnv *pJNIEnv = GetJNIEnv();
	if (pJNIEnv)
	{
		jclass pJNIActivityClass = pJNIEnv->FindClass ( "com/company/game/Bridge" );
		if(pJNIActivityClass == NULL)
				LOGI("jclass was null!?!");
		else
		{
			jmethodID pJNIMethodID = pJNIEnv->GetStaticMethodID(pJNIActivityClass, "onRestore", "()V" );
			if(pJNIMethodID == NULL)
					LOGI("jmethodID was null!?!?");
			else
			{
				pJNIEnv->CallStaticVoidMethod(pJNIActivityClass, pJNIMethodID );
			}
		}
	}
}
//	restore purchase
//********************************************************************
//	custom voids
//==========================================================================================
//	callbacks
//********************************************************************
//	purchase callback
JNIEXPORT void JNICALL Java_com_company_game_Bridge_callBackPurchase ( JNIEnv *_pEnv, jobject obj, jstring id )
{
	//Convert java string 'id' to a const char * so we can pass it to shiva
	const char *nativeString = _pEnv->GetStringUTFChars(id, NULL);
	S3DX::AIVariable args[1];
	args[0].SetStringValue( nativeString );
	S3DClient_SendEventToCurrentUser( "Store_PlayStore", "onPlayStoreProductPurchased", 1, (const void*)args);
	//Release string
	_pEnv->ReleaseStringUTFChars(id, nativeString);
}
//	purchase callback
//********************************************************************
//********************************************************************
//	restore callback
JNIEXPORT void JNICALL Java_com_company_game_Bridge_callBackPurchaseRestore ( JNIEnv *_pEnv, jobject obj, jstring id )
{
	//Convert java string 'id' to a const char * so we can pass it to shiva
	const char *nativeString = _pEnv->GetStringUTFChars(id, NULL);
	S3DX::AIVariable args[1];
	args[0].SetStringValue( nativeString );
	S3DClient_SendEventToCurrentUser( "Store_PlayStore", "onPlayStoreProductRestored", 1, (const void*)args);
	//Release string
	_pEnv->ReleaseStringUTFChars(id, nativeString);
}
//	restore callback
//********************************************************************
//********************************************************************
//	purchase callback failure
JNIEXPORT void JNICALL Java_com_company_game_Bridge_callBackPurchaseFailure ( JNIEnv *_pEnv, jobject obj, jstring id )
{
	//Convert java string 'id' to a const char * so we can pass it to shiva
	const char *nativeString = _pEnv->GetStringUTFChars(id, NULL);
	S3DX::AIVariable args[1];
	args[0].SetStringValue( nativeString );
	S3DClient_SendEventToCurrentUser( "Store_PlayStore", "onPlayStoreBuyProductFailed", 1, (const void*)args);
	//Release string
	_pEnv->ReleaseStringUTFChars(id, nativeString);
}
//	purchase callback failure
//********************************************************************
//	callbacks
//==========================================================================================

Product identifiers

Okay, now we need to set the code up so that it works for your game. Replace all of the text fragments “com/company/game” with the ones you need, for example: “com/turborocketgames/panthersim”.
If you are not sure what to paste here, go to the “src” folder of your project. You will find something like “com.turborocketgames.panthersim”, that’s it – except you need to use ‘/’ intead of ‘.’
Also replace “com_company_game” to something similar as I explained above: “com_turborocketgames_panthersim”

Event hooks

Search for the line:

// @@BEGIN_JNI_INSTALL_EVENT_HOOKS@@

Paste these lines of code there:

S3DClient_InstallCurrentUserEventHook ( "Store_PlayStore", "onBuyProduct", onBuyProduct, NULL );
S3DClient_InstallCurrentUserEventHook ( "Store_PlayStore", "onRestore", onRestore, NULL );
S3DClient_InstallCurrentUserEventHook ( "Store_PlayStore", "onAddProduct", onAddProduct, NULL );

Eclipse / Android Studio Setup – Java

Make sure you have Google Play Services installed for your project.

Manifest

In your AndroidManifest.xml, add this permission:

uses-permission android:name="com.android.vending.BILLING" 

Make sure that the launch mode in the Manifest is “singleTop”.

android:launchMode="singleTop"

IAP files

Now download required files for the iAPS:
AndroidIAPfiles.rar @ ShiVa WIKI server
Copy&paste the folders “android” and “example” into the “src/com” folder of your Eclipse project. Then, copy&paste the Bridge.java file into the folder “src/com/company/game” Your “src” folder should now look similar to this:
project_src
Note that there is one slight difference for Android Studio, concerning the .aidl file. In Android Studio, you have to create a separate folder : src/main/aidl, and place the file called “IInAppBillingService.aidl” there instead.

Bridge.java

Now you need to change the Bridge.java so it works with your project. First, modify line 1 of the Bridge.java so it works with your package.
Then locate this line:

static String base64EncodedPublicKey = "YOUR_CODE_HERE";

Replace YOUR_CODE_HERE with your Base64 encoded public key. You can get it in the Google Play Dev console.
Now search for “PantherSimulator” and replace it with your main activity name. My main Java file of the game is “PantherSimulator.java”, that is how the name came to be.

Main Java

Now it is time to modify your main java file. For me, this was “PantherSimulator.java” in the “com.turborocketgames.panthersim” package. Your names will be different of course.
We must make sure that our activity does’t get paused when it is not needed. This is important because ShiVa shows the splash screen when the activity is paused, and we don’t want that. Locate this line:

public static final int MSG_ENABLE_VIBRATOR = 7;

Add this line after it:

public static int nIgnorePause = 0;

Now locate the onPause method by searching for this line

protected void onPause()

Replace this method with:

@Override
protected void onPause() {
	super.onPause ( ) ;
	if(nIgnorePause == 0)
	{
		// Security
		//
		bWantToResume   = false ;
		// Send a delayed event to actually pause engine and hide the 3D View
		//
		Message msg     = new Message ( )  ;
		msg.what        = MSG_PAUSE_ENGINE ;
		msg.obj         = this ;
		//oUIHandler  	.sendMessageDelayed ( msg, 500 ) ;
		oUIHandler  	.sendMessage ( msg ) ;
	}
	else
	{
		//oThis.onResume();
		nIgnorePause = nIgnorePause - 1;
	}
}

Now locate the onResume method by searching for

protected void onResume ( )

Replace the whole method with:

@Override
protected void onResume ( )
{
	super.onResume ( ) ;
	if( bPaused == true )
	{
		// If screen is locked, just wait for unlock, else resume
		//
		bWantToResume = true ;
		if ( ! bScreenLocked )
		{
					onResumeActually ( ) ;
		}
	}
}

Now locate this method:

protected void createAsync()

Add the following code before that:

// for in-app purchase
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data)
{
	if (Bridge.mHelper != null && !Bridge.mHelper.handleActivityResult(requestCode, resultCode, data))
	{
		super.onActivityResult(requestCode, resultCode, data);
	}
};
// for in-app purchase

Locate:

oThis = this;

Add this code:

activity = this;

Locate:

private static PantherSimulator oThis;
// Replaced with your name of game naturally

and add this code:

public static Activity activity;

Credits

Written by Vladimir NiddHogg
Code provided by Henrik Hakobyan
Tested by Tuomas Karmakallio




Need more answers?

  • slackBanner