No-Backend OSP Step-by-Step

1. Create URL

The first step is to add the functionality to your app to generate the URL as specified in OSP URL Structure.
The domain must be the package name and it must be certified in Catappult even for testing purposes.

The next example shows how to create the URL:

val DOMAIN = "com.appcoins.trivialdrivesample"

public fun createURL(value: Int, currency: String, product: String) {
    val url = ("https://apichain.catappult.io/transaction/inapp"
            + "?value=" + value
            + "&currency=" + currency
            + "&domain=" + DOMAIN
            + "&product=" + product)
    return url
}
String DOMAIN = "com.appcoins.trivialdrivesample";

public String createURL(int value, String currency, String product) {
    String url = "https://apichain.catappult.io/transaction/inapp"
                + "?value=" + value
                + "&currency=" + currency
                + "&domain=" + DOMAIN
                + "&product=" + product;
    return url; 
}

2. Call the URL

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. In order to avoid this, we suggested adding the following sample to trigger the One Step billing flow.

val RC_ONE_STEP = 10003

var intent = buildTargetIntent(url);
try {
    startActivityForResult(intent, RC_ONE_STEP);
} catch (e: Exception) {
    e.printStackTrace();
}

/**
* 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 {
    var intent = Intent(Intent.ACTION_VIEW)
    intent.data = Uri.parse(url)

    // Check if there is an application that can process the AppCoins Billing
    // flow
    var packageManager = applicationContext.packageManager
    var 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
}
private static int RC_ONE_STEP = 10003

Intent intent = buildTargetIntent(url);
try {
    startActivityForResult(intent, RC_ONE_STEP);
} catch (Exception e) {
    e.printStackTrace();
}
/**
* 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;
}

Target SDK 30 and over

When the app targets SDK 30 or above you need to add the intent to AndroidManifest as such:

<manifest>
...
    <queries>
        <intent>
            <action android:name="android.intent.action.VIEW">
        </intent>
    ...
    </queries>
...
</manifest>

3. Verify the transaction from the app

After the transaction is processed, the wallet activity will return, as a result, an intent with the extra "transaction_hash" as an extra in the intent.
This transaction hash can be used to call the catapult transaction's API and receive the transaction data.
After receiving the data you can compare its values with the ones you created the OSP URL like so:

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (requestCode == REQUEST_CODE) {
            if (data == null || data.extras == null) {
                return
            }
            val transactionHash = data.getStringExtra("transaction_hash")
            val response: String = getTransactionData(transactionHash)
            val isValid = isValid(response, ospPurchase)
        }
}

// OspPurchase is a data object containing the data used to create the OSP URL 
private fun isValid(response: String, ospPurchase: OspPurchase): Boolean {
        val purchase = JSONObject(response)
        val transactionDetails = purchase.getJSONArray("items").getJSONObject(0)
        val value = transactionDetails.getJSONObject("price").getDouble("usd")
        val reference = ospPurchase.reference ?: "null"

        return ospPurchase.product == transactionDetails.getString("product")
                && ospPurchase.domain == transactionDetails.getString("domain")
                && reference == transactionDetails.getString("reference")
                && ospPurchase.value == value
}
@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) {
                return;
            }
            String transactionHash = data.getStringExtra("transaction_hash");
            String response = getTransactionData(transactionHash);
            try {
                boolean isValid = isValid(response, ospPurchase);
            } catch (JSONException e) {
                e.printStackTrace();
            }
        }
}
    
// OspPurchase is a data object containing the data used to create the OSP URL 
private boolean isValid(String response, OspPurchase ospPurchase) throws JSONException {
        JSONObject purchase = new JSONObject(response);
        JSONObject transactionDetails = purchase.getJSONArray("items").getJSONObject(0);
        double value = transactionDetails.getJSONObject("price").getDouble("usd");
        String reference = ospPurchase.reference;
        if (reference == null) reference = "null";

        return ospPurchase.product.equals(transactionDetails.getString("product"))
                && ospPurchase.domain.equals(transactionDetails.getString("domain"))
                && reference.equals(transactionDetails.getString("reference"))
                && ospPurchase.value == value;
}