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"

/**
 * This method creates the One Step URL to perform the payment
 * @param value The cost value of the payment
 * @param currency The currency to be used on the payment (Note "USD" is allways advised)
 * @param product The name of the product.
 */
public fun createURL(value: Float, currency: String, product: String): String {
    val url = ("https://apichain.catappult.io/transaction/inapp"
            + "?value=" + value
            + "&currency=" + currency
            + "&domain=" + DOMAIN
            + "&product=" + product)
    return url
}
String DOMAIN = "com.appcoins.trivialdrivesample";

/**
 * This method creates the One Step URL to perform the payment
 * @param value The cost value of the payment
 * @param currency The currency to be used on the payment (Note "USD" is allways advised)
 * @param product The name of the product.
 */
public String createURL(float 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.

// Class variable
var RC_ONE_STEP = 10003

// try-catch executed from function that triggers payment
try {
  startOneStepPayment(url);
} catch (e: Exception) {
  e.printStackTrace();
}

/**
 * This method starts the intent with the provided One Step URL to target the
 * AppCoins Wallet. 
 * Note: startActivityForResult was used for simplicity. Consider using registerForActivityResult instead.
 * @param url The url that is generated by following the One Step payment rules
 */
fun startOneStepPayment(url: String) {
  val intent = Intent(Intent.ACTION_VIEW)
  intent.data = Uri.parse(url)
  // If AppCoins Wallet is installed then start the Billing flow
  // Otherwise open the URL with default action to install the Wallet
  if (isWalletInstalled()) {
    intent.setPackage("com.appcoins.wallet")
  }
  startActivityForResult(intent, RC_ONE_STEP)
}
private fun isWalletInstalled(): Boolean {
  val packageManager = applicationContext.packageManager
  val intentForCheck = Intent(Intent.ACTION_VIEW)
  return if (intentForCheck.resolveActivity(packageManager) != null)
    try {
      packageManager.getPackageInfo("com.appcoins.wallet", PackageManager.GET_ACTIVITIES)
      true
    } catch (e: PackageManager.NameNotFoundException) {
      false
    }
  else false
}
// Class variable
private static int RC_ONE_STEP = 10003;

// try-catch executed from function that triggers payment
try {
  startOneStepPayment(url);
} catch (Exception e) {
  e.printStackTrace();
}

/**
 * This method starts the intent with the provided One Step URL to target the
 * AppCoins Wallet. 
 * Note: startActivityForResult was used for simplicity. Consider using registerForActivityResult instead. 
 * @param url The url that is generated by following the One Step payment rules
 */
public void startOneStepPayment(String url) {
  Intent intent = new Intent(Intent.ACTION_VIEW);
  intent.setData(Uri.parse(url));
  // If AppCoins Wallet is installed then start the Billing flow
  // Otherwise open the URL with default action to install the Wallet
  if (isWalletInstalled()) {
    intent.setPackage("com.appcoins.wallet");
  }
  startActivityForResult(intent, RC_ONE_STEP);
}
private Boolean isWalletInstalled() {
  PackageManager packageManager = getApplicationContext().getPackageManager();
  Intent intentForCheck = new Intent(Intent.ACTION_VIEW);
  if (intentForCheck.resolveActivity(packageManager) != null) {
    try {
      packageManager.getPackageInfo("com.appcoins.wallet", PackageManager.GET_ACTIVITIES);
      return true;
    } catch (PackageManager.NameNotFoundException e) {
      return false;
    }
  }
  return false;
}

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>
        <package android:name="com.appcoins.wallet" />
    ...
    </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 == RC_ONE_STEP) {
            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 == RC_ONE_STEP) {
            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;
}