Create custom tender apps

United States
Canada
Europe
Latin America

This tutorial assumes that you're already familiar with creating an Android app for Clover devices.

Setup for this tutorial

  • JVM 1.8.0
  • Gradle 2.3
  • Android Studio 1.3.2

Required permissions

The following permissions are required to initialize a custom tender:

  • MERCHANT_R (to get/read tenders)
  • MERCHANT_W (to create/write tenders)

Initialize a custom tender for a merchant

For this part of this tutorial, we will be using the TenderConnector to initialize a custom tender for our merchant.

  1. Add the SDK as a dependency in your build.gradle file.Example: compile 'com.clover.sdk:clover-android-sdk:latest.release'
  2. Add the necessary Android permission to your AndroidManifest.xml file.Example: uses-permission android:name="android.permission.GET_ACCOUNTS"

🚧

IMPORTANT

The app must run once before a custom tender can be used. This is because the initialization code must execute in order to register the tender with the merchant. We recommend running the custom tender app immediately after it has finished installing.

Initialize your custom tender

The following code demonstrates how to initialize a custom tender for a merchant.

private void createTenderType(final Context context) {
    new AsyncTask<Void, Void, Exception>() {

        private TenderConnector tenderConnector;

        @Override
        protected void onPreExecute() {
            super.onPreExecute();
            tenderConnector = new TenderConnector(context, CloverAccount.getAccount(context), null);
            tenderConnector.connect();
        }

        @Override
        protected Exception doInBackground(Void... params) {
            try {
                tenderConnector.checkAndCreateTender(getString(R.string.tender_name), getPackageName(), true, false); // initialization
            } catch (Exception exception) {
                Log.e(TAG, exception.getMessage(), exception.getCause());
                return exception;
            }
            return null;
        }

        @Override
        protected void onPostExecute(Exception exception) {
            tenderConnector.disconnect();
            tenderConnector = null;
        }
    }.execute();
}

👍

TIP

This is a streamlined example. Production-ready code must handle error scenarios and properly inform the user with appropriate messages.

2657

Custom Tender Initialized screen of Extensible Tender Example

Link into the Register, Sale, and Secure Payment apps

You can choose to implement one of two payment experiences, depending on your specific use case. For a purely merchant-facing payment, such as one that only allows the merchant to scan a QR code or enter a customer's phone number for something like a digital wallet redemption, then follow the steps in the Merchant-Facing Setup section below.

To develop a customer-facing experience that launches from within the Secure Payment app on a Clover Mobile or Clover Mini (in a one-device use case), or on a tethered Clover Mini (in a two-device use case), skip to the Customer-Facing Setup section.

👍

TIP

In some cases, it may make sense to allow the merchant to use either method, such as in the phone number example above. You must develop your user experiences and UI accordingly.

🚧

IMPORTANT

Be sure to add setResult(RESULT_CANCELED) in onCreate to create a default exit result.

Merchant-facing setup

  1. Follow the example below to set up an activity for a merchant-facing custom tender.
public class MerchantFacingTenderActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_tender);

        setResult(RESULT_CANCELED);

        /**
         * @see Intents.ACTION_MERCHANT_TENDER
         */
        final long amount = getIntent().getLongExtra(Intents.EXTRA_AMOUNT, 0);
        final Currency currency = (Currency) getIntent().getSerializableExtra(Intents.EXTRA_CURRENCY);
        final long taxAmount = getIntent().getLongExtra(Intents.EXTRA_TAX_AMOUNT, 0);
        final ArrayList taxableAmounts = getIntent().getParcelableArrayListExtra(Intents.EXTRA_TAXABLE_AMOUNTS);
        final ServiceChargeAmount serviceCharge = getIntent().getParcelableExtra(Intents.EXTRA_SERVICE_CHARGE_AMOUNT);

        final String orderId = getIntent().getStringExtra(Intents.EXTRA_ORDER_ID);
        final String merchantId = getIntent().getStringExtra(Intents.EXTRA_MERCHANT_ID);

        final Tender tender = getIntent().getParcelableExtra(Intents.EXTRA_TENDER);

        // Merchant Facing specific fields
        final Order order = getIntent().getParcelableExtra(Intents.EXTRA_ORDER);
        final String note = getIntent().getStringExtra(Intents.EXTRA_NOTE);

        setupViews(amount, currency, orderId, merchantId);
    }

    public void setupViews(final long amount, Currency currency, String orderId, String merchantId) {
        TextView amountText = (TextView) findViewById(R.id.text_amount);
        amountText.setText(Utils.longToAmountString(currency, amount));

        TextView orderIdText = (TextView) findViewById(R.id.text_orderid);
        orderIdText.setText(orderId);
        TextView merchantIdText = (TextView) findViewById(R.id.text_merchantid);
        merchantIdText.setText(merchantId);

        Button approveButton = (Button) findViewById(R.id.acceptButton);
        approveButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent data = new Intent();
                data.putExtra(Intents.EXTRA_AMOUNT, amount);
                data.putExtra(Intents.EXTRA_CLIENT_ID, Utils.nextRandomId());
                data.putExtra(Intents.EXTRA_NOTE, "Transaction Id: " + Utils.nextRandomId());

                setResult(RESULT_OK, data);
                finish();
            }
        });

        Button declineButton = (Button) findViewById(R.id.declineButton);
        declineButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent data = new Intent();
                data.putExtra(Intents.EXTRA_DECLINE_REASON, "You pressed the decline button");

                setResult(RESULT_CANCELED, data);
                finish();
            }
        });
    }
}
  1. You need to declare a MERCHANT_TENDER intent filter in the AndroidManifest.xml file in order for Android to resolve any intents to your activity. Set exported to true.
<activity
    android:name=".MerchantFacingTenderActivity"
    android:exported="true"
    android:label="@string/title_activity_test_tender">

    <meta-data
        android:name="clover.intent.meta.MERCHANT_TENDER_IMAGE"
        android:resource="@mipmap/thirdparty_gray_example" />

    <intent-filter>
        <action android:name="clover.intent.action.MERCHANT_TENDER" />
        <category android:name="android.intent.category.DEFAULT" />
    </intent-filter>
    </activity>

📘

NOTE

You may also use a logo in the Sale app instead of the name of your custom tender. To do this, declare MERCHANT_TENDER_IMAGE metadata for the same Activity the intent filter was declared for and referencing your logo asset. The logo must be solid gray, in accordance with Clover's design guidelines.

2484

Custom Tender: Merchant-facing button in the Register app

2483

Custom Tender: Merchant-facing button in the Sale app

Customer-facing setup

Follow the example below to set up an activity for a customer-facing custom tender.

public class CustomerFacingTenderActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_tender);

        setResult(RESULT_CANCELED);

        // Necessary for Customer Facing user experiences
        setSystemUiVisibility();

        /**
         * @see Intents.ACTION_CUSTOMER_TENDER
         */
        final long amount = getIntent().getLongExtra(Intents.EXTRA_AMOUNT, 0);
        final Currency currency = (Currency) getIntent().getSerializableExtra(Intents.EXTRA_CURRENCY);
        final long taxAmount = getIntent().getLongExtra(Intents.EXTRA_TAX_AMOUNT, 0);
        final ArrayList taxableAmounts = getIntent().getParcelableArrayListExtra(Intents.EXTRA_TAXABLE_AMOUNTS);
        final ServiceChargeAmount serviceCharge = getIntent().getParcelableExtra(Intents.EXTRA_SERVICE_CHARGE_AMOUNT);

        final String orderId = getIntent().getStringExtra(Intents.EXTRA_ORDER_ID);
        final String employeeId = getIntent().getStringExtra(Intents.EXTRA_EMPLOYEE_ID);
        final String merchantId = getIntent().getStringExtra(Intents.EXTRA_MERCHANT_ID);

        final Tender tender = getIntent().getParcelableExtra(Intents.EXTRA_TENDER);

        // Customer Facing specific fields
        final long tipAmount = getIntent().getLongExtra(Intents.EXTRA_TIP_AMOUNT, 0);

        setupViews(amount, currency, orderId, merchantId);
    }

    public void setSystemUiVisibility() {
        getWindow().getDecorView().setSystemUiVisibility(
                View.SYSTEM_UI_FLAG_LAYOUT_STABLE
                        | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
                        | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                        | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
                        | View.SYSTEM_UI_FLAG_LOW_PROFILE
                        | View.SYSTEM_UI_FLAG_FULLSCREEN
                        | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
    }

    public void setupViews(final long amount, Currency currency, String orderId, String merchantId) {
        TextView amountText = (TextView) findViewById(R.id.text_amount);
        amountText.setText(Utils.longToAmountString(currency, amount));

        TextView orderIdText = (TextView) findViewById(R.id.text_orderid);
        orderIdText.setText(orderId);
        TextView merchantIdText = (TextView) findViewById(R.id.text_merchantid);
        merchantIdText.setText(merchantId);

        Button approveButton = (Button) findViewById(R.id.acceptButton);
        approveButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent data = new Intent();
                data.putExtra(Intents.EXTRA_AMOUNT, amount);
                data.putExtra(Intents.EXTRA_CLIENT_ID, Utils.nextRandomId());
                data.putExtra(Intents.EXTRA_NOTE, "Transaction Id: " + Utils.nextRandomId());

                setResult(RESULT_OK, data);
                finish();
            }
        });

        Button declineButton = (Button) findViewById(R.id.declineButton);
        declineButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent data = new Intent();
                data.putExtra(Intents.EXTRA_DECLINE_REASON, "You pressed the decline button");

                setResult(RESULT_CANCELED, data);
                finish();
            }
        });
    }
}

🚧

IMPORTANT

Customer-facing user experiences must always be fullscreen to prevent the customer from navigating away from your app.

The necessary flags are:

  • View.SYSTEM_UI_FLAG_LAYOUT_STABLE
  • View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
  • View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
  • View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
  • View.SYSTEM_UI_FLAG_LOW_PROFILE
  • View.SYSTEM_UI_FLAG_FULLSCREEN
  • View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY

You need to declare a CUSTOMER_TENDER intent-filter in the AndroidManifest.xml file in order for Android to resolve any intents to your activity. Set exported to true.

<activity
    android:name=".CustomerFacingTenderActivity"
    android:exported="true"
    android:label="@string/title_activity_test_tender"
    android:theme="@android:style/Theme.Holo.Light.NoActionBar.Fullscreen">

    <meta-data
        android:name="clover.intent.meta.CUSTOMER_TENDER_IMAGE"
        android:resource="@mipmap/thirdparty_white_example" />

    <intent-filter>
        <action android:name="clover.intent.action.CUSTOMER_TENDER" />
        <category android:name="android.intent.category.DEFAULT" />
    </intent-filter>
</activity>

📘

NOTE

You may also use a logo instead of your custom tender's name. To do this, declare CUSTOMER_TENDER_IMAGE metadata for the same activity the intent-filter was declared for and reference your logo asset. It must be solid white, in accordance with Clover's design guidelines.

Please note that a fullscreen Android theme must be applied (Theme.Holo.Light.NoActionBar.Fullscreen in this example) to a customer-facing experience in order for the UI to appear smoothly before the system calls onCreate(Bundle savedInstanceState)

2651

Custom Tender: Customer Facing button in Secure Payments

Partial payments

Merchant plans that support the Register and Payments apps allow partial payments. These include the Register and Register Lite plans. The Sale app on the Payment Plus Plan does not support partial payments.

You can make a partial payment by responding with an amount, which can be less than the total amount. Please see the documentation for ACTION_MERCHANT_TENDER in the class Intents in the Clover Android SDK for more information.

Handle refunds

You will also need to account for all refunds on partial payments made by custom tenders. Note that the merchant can issue a LineItem-level refund using Custom tenders.

  1. First, ensure your custom tender's Refund Activity can handle refund intents:
<intent-filter>
<action android:name="clover.intent.action.REFUND" />
<category android:name="android.intent.category.DEFAULT" />
  1. From your Refund Activity, capture some Extras from the Refund Intent:
getIntent().getLongExtra(Intents.EXTRA_AMOUNT, 0);
final String orderId = getIntent().getStringExtra(Intents.EXTRA_ORDER_ID);
final String paymentId = getIntent().getStringExtra(Intents.EXTRA_PAYMENT_ID);
final ArrayList lineItemIds = getIntent().getStringArrayListExtra(Intents.EXTRA_LINE_ITEM_IDS);
  1. If the Refund POSTs successfully on your end, tell Clover everything went as planned:
Intent data = new Intent();
data.putExtra(Intents.EXTRA_ORDER_ID, orderId);
data.putExtra(Intents.EXTRA_PAYMENT_ID, paymentId);
data.putExtra(Intents.EXTRA_LINE_ITEM_IDS, lineItemIds);
data.putExtra(Intents.EXTRA_AMOUNT, amount);
data.putExtra(Intents.EXTRA_CLIENT_ID, "Your_external_payment_ID");
setResult(RESULT_OK, data);
finish();Else:
setResult(RESULT_CANCELED, data);
finish();

🚧

IMPORTANT

When a lineItem-level, custom tender Refund Intent is fired, the EXTRA_AMOUNT is the sum of the lineItems.amounts that were selected. As such, the EXTRA_AMOUNT might be greater than the amount that was paid using the custom tender. For Orders on which the Refund's EXTRA_AMOUNT is less than or equal to the custom tender payment, the flow will work as intended. However, you will still need to add logic to verify this.

You must also handle cases where the custom tender was applied as a partial payment, and pass the appropriate EXTRA_AMOUNT back to Clover before calling finish().

For more information, see this thread in the Clover Developer Community.