Unity No-Backend OSP
1. Create the C# Script
To implement OSP, create a script that contains the logic and can be added to the IAP buttons. Firstly create 2 dynamic and 2 static values (productid, value, and domain, currency), this way every button will have the domain and currency to build the URL, but each button can have a different product and value associated.
public string productId;
public float value;
private static string DOMAIN = "com.mygamestudio.game";
private static string CURRENCY = "USD";
Then create a function that starts the OSP purchase flow and set it as the onClick listener for the associated button. To avoid repetition you can also store the Unity activity as a field and instantiate it on "Start".
//This two fields will be necessary later
private AndroidJavaObject activity;
private static int MATCH_DEFAULT_ONLY_ANDROID_PM = 65536;
void Start ()
{
var unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
activity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity");
GetComponent<Button>().onClick.AddListener(TaskOnClick);
}
void TaskOnClick()
{
var url = CreateURL();
var ospActivity = new AndroidJavaClass("com.appcoins.osp.OspActivity");
ospActivity.CallStatic("start", activity, url);
}
2. Create URL
The first step is to add the functionality to your app to generate the URL as specified in OSP URL Structure: OSP URL Structure documentation.
The domain must be the package name and it must be certified in catappult even for testing purposes.
To next example shows how to create the URL:
string CreateURL()
{
return [email protected]"https://apichain.catappult.io/transaction/inapp
?value={value}
&product={productId}
&domain={DOMAIN}
¤cy={CURRENCY}";
}
3. Create Osp Activity
To validate a transaction client-side it is needed to have an onActivityResult, and for this, it is necessary to create an Activity whose sole purpose will be to handle the call of the intent and the validation, this has to be done in Java/Kotlin.
Go to Assets/Plugins and create OspActivity and add the following code:
package com.appcoins.osp
class OspActivity : Activity() {
companion object {
private val TAG = OspActivity::class.java.simpleName
private const val REQUEST_CODE = 1234
lateinit var url: String
@JvmStatic
fun start(context: Activity, url: String) {
Log.d(TAG, "$context && $url")
OspActivity.url = url
val starter = Intent(context, OspActivity::class.java)
context.startActivity(starter)
}
}
}
package com.appcoins.osp;
public class OSPActivity extends Activity {
private final static String TAG = OSPActivity.class.getSimpleName();
private final static int REQUEST_CODE = 1234;
private static String url;
public static void start(Activity context, String url) {
Log.d(TAG, context.toString() + " && " + url);
OSPActivity.url = url;
Intent starter = new Intent(context, OSPActivity2.class);
context.startActivity(starter);
}
}
The method "start" is the one called from the C# scripts and handles the transfer of data from the script to the Osp Activity, and starts itself.
To make the Activity available to Android go to the Unity AndroidManifest and add this activity:
<activity android:name="com.appcoins.osp.OspActivity"
android:theme="@android:style/Theme.Translucent.NoTitleBar">
</activity>
If you don’t know how to access Unity’s AndroidManifest go to File > Build Settings… > Player Settings… > Publishing Settings, and tick the option Build > Custom Main Manifest, this will add an AndroidManifest template that will be used by Unity.

4. Create and Start Intent
Inside the app/game, an intent with the URL should be opened either using the web browser (whenever the AppCoins Wallet is not installed), or via the AppCoins Wallet.
In some cases, the user chooses to open the URL with the browser when the AppCoins Wallet is already installed or even chooses the option to always open the URL with the browser. To avoid this add the following sample to trigger the One Step billing flow.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val intent = buildTargetIntent(url)
startActivityForResult(intent, REQUEST_CODE)
}
/**
* This method generates the intent with the provided One Step URL to target the
* AppCoins Wallet.
* @param url The url that generated by following the One Step payment rules
*
* @return The intent used to call the wallet
*/
private fun buildTargetIntent(url: String): Intent {
val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse(url)
// Check if there is an application that can process the AppCoins Billing
// flow
val packageManager = applicationContext.packageManager
val appsList: List<ResolveInfo> = packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY)
for (app in appsList) {
if (app.activityInfo.packageName == "cm.aptoide.pt") {
// If there's aptoide installed always choose Aptoide as default to open
// url
intent.setPackage(app.activityInfo.packageName)
break
} else if (app.activityInfo.packageName == "com.appcoins.wallet") {
// If Aptoide is not installed and wallet is installed then choose Wallet
// as default to open url
intent.setPackage(app.activityInfo.packageName)
}
}
return intent
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent intent = buildTargetIntent(url);
startActivityForResult(intent, REQUEST_CODE);
}
/**
* This method generates the intent with the provided One Step URL to target the
* AppCoins Wallet.
* @param url The url that generated by following the One Step payment rules
*
* @return The intent used to call the wallet
*/
private Intent buildTargetIntent(String url) {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse(url));
// Check if there is an application that can process the AppCoins Billing
// flow
PackageManager packageManager = getApplicationContext().getPackageManager();
List<ResolveInfo> appsList = packageManager
.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
for (ResolveInfo app : appsList) {
if (app.activityInfo.packageName.equals("cm.aptoide.pt")) {
// If there's aptoide installed always choose Aptoide as default to open
// url
intent.setPackage(app.activityInfo.packageName);
break;
} else if (app.activityInfo.packageName.equals("com.appcoins.wallet")) {
// If Aptoide is not installed and wallet is installed then choose Wallet
// as default to open url
intent.setPackage(app.activityInfo.packageName);
}
}
return intent;
}
5. Receive data from the wallet
To validate the transaction you need to receive the transaction hash from the wallet, this data is received on Activity Result:
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == REQUEST_CODE) {
if (data == null || data.extras == null) {
finish()
return
}
val transactionHash = data.getStringExtra("transaction_hash")
val url = URL("https://api.catappult.io/broker/8.20200101/transactions?hash=$transactionHash")
Thread {
validateTransaction(url)
}.start()
Log.d(TAG, "$transactionHash")
finish()
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_CODE) {
if (data == null || data.getExtras() == null) {
finish();
return;
}
String transactionHash = data.getStringExtra("transaction_hash");
try {
URL url = new URL("https://api.catappult.io/broker/8.20200101/transactions?hash=" + transactionHash);
new Thread() {
public void run() {
try {
validateTransaction(url);
} catch (JSONException e) {
e.printStackTrace();
}
}
}.start();
} catch (MalformedURLException e) {
e.printStackTrace();
}
Log.d(TAG, transactionHash);
finish();
}
}
6. Validate the transaction
To validate the transaction fetch the transaction data from Catappult's API and compare the data with the values in the OSP URL. To see how to fetch the transaction data from Catappult's API, click here.
For this to work the currency in the OSP URL must be either USD or APPC.
import com.unity3d.player.UnityPlayer
private fun validateTransaction(url: URL) {
val transaction: String = getTransactionData(url) ?: return
val json = JSONObject(transaction)
val transactionDetails = json.getJSONArray("items").getJSONObject(0)
val ospUri = Uri.parse(OspActivity.url)
val methodToCall = if (isValid(transactionDetails, ospUri)) "OnPurchaseSuccess" else "OnPurchaseUnsuccessful"
// This method calls the method "methodToCall" belonging to the game object PurchaseManager, this game
// object must be created and added a script that contains the possible methods that methodToCall contains and receive a string
// With this it is possible to preform diferent functionalities in c# for when the validation fails or is successful
UnityPlayer.UnitySendMessage(
"PurchaseManager",
methodToCall,
ospUri.getQueryParameter("product")
)
}
// Fetches the data from catappult's API
private fun getTransactionData(url: URL): String? {
val urlConnection= url.openConnection() as HttpURLConnection
return try {
val content = urlConnection.inputStream.bufferedReader().readText()
content
} catch (e: Exception) {
e.printStackTrace()
null
} finally {
urlConnection.disconnect()
}
}
// Compares the values received from catappult with the query parameters from the OSP URL
private fun isValid(transactionDetails: JSONObject, ospUri: Uri): Boolean {
val priceDetails = transactionDetails.getJSONObject("price")
val product = ospUri.getQueryParameter("product")
val domain = ospUri.getQueryParameter("domain")
val reference = ospUri.getQueryParameter("reference") ?: "null"
val value = (ospUri.getQueryParameter("value")?.toDouble() ?: 0.0) as Double?
return product == transactionDetails.getString("product")
&& domain == transactionDetails.getString("domain")
&& reference == transactionDetails.getString("reference")
&& value == priceDetails.getDouble("usd")
}
import com.unity3d.player.UnityPlayer;
private void validateTransaction(URL url) throws JSONException {
String transaction = getTransactionData(url);
if (transaction == null) return;
JSONObject json = new JSONObject(transaction);
JSONObject transactionDetails = json.getJSONArray("items").getJSONObject(0);
Uri ospUri = Uri.parse(OSPActivity.url);
String methodToCall = isValid(transactionDetails, ospUri) ? "OnPurchaseSuccess" : "OnPurchaseUnsuccessful";
Log.d(TAG, methodToCall);
/*UnityPlayer.UnitySendMessage(
"PurchaseManager",
methodToCall,
ospUri.getQueryParameter("product")
)*/
}
private boolean isValid(JSONObject transactionDetails, Uri ospUri) throws JSONException {
JSONObject priceDetails = transactionDetails.getJSONObject("price");
String product = ospUri.getQueryParameter("product");
String domain = ospUri.getQueryParameter("domain");
String reference = ospUri.getQueryParameter("reference");
if (reference == null) reference = "null";
double value = Double.parseDouble(ospUri.getQueryParameter("value"));
return product.equals(transactionDetails.getString("product"))
&& domain.equals(transactionDetails.getString("domain"))
&& reference.equals(transactionDetails.getString("reference"))
&& value == priceDetails.getDouble("usd");
}
private String getTransactionData(URL url) {
HttpURLConnection urlConnection = null;
try {
urlConnection = (HttpURLConnection) url.openConnection();
InputStream in = new BufferedInputStream(urlConnection.getInputStream());
// Function that returns a string from an InputStream
return readStream(in);
} catch (IOException e) {
e.printStackTrace();
return null;
} finally {
urlConnection.disconnect();
}
}
Also, make sure the app has internet permission in Android Manifest:
<uses-permission android:name="android.permission.INTERNET" />
7. Create a Purchase Manager
As explained with comments, to inform Unity scripts of the result it is used the UnitySendMessage, that in this example we use to call a method from PurchaseManager with the product as an argument, to make this work add the game object PurchaseManager and attach to it a script with the methods called from the activity. This script is where you can implement the logic for what to do with successful and unsuccessful transactions.
Updated 7 months ago