Create a custom tender app

United States
Canada
Europe
Latin America

Before you begin

Review Advanced information to build with Android.

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, use 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".
  3. Required. Run the app once before you can use a custom tender. This is because the initialization code needs to be executed to register the tender with the merchant. We recommend running the custom tender app immediately after it is installed.

Initialize your custom tender

The following code demonstrates how to initialize a custom tender for a merchant. This is a streamlined example. Production-ready code must handle error scenarios and properly inform the user with appropriate messages.

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();
}

🚧

IMPORTANT

Required. Add setResult(RESULT_CANCELED) in onCreate to create a default exit result.

2657

Custom Tender Initialized screen of Extensible Tender Example

Link into the Register, Sale, and Secure Payment apps

You can implement one of two payment experiences, depending on your specific use case:

  • For a merchant-facing payment, such as when the merchant scans a QR code or enters a customer's phone number for a digital wallet redemption, follow the steps in the Merchant-facing setup section.
  • For 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 a tethered Clover Mini (in a two-device use case), see the Customer-facing setup section.

👍

TIP

In some cases, it may allow the merchant to use either method, such as when using a customer phone number. You must develop your user experiences and user interface accordingly.

Merchant-facing setup

  1. Follow the example 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. Declare a MERCHANT_TENDER intent filter in the AndroidManifest.xml file for Android to resolve any intents to your activity.
  2. 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>
  1. Optional. To use a logo in the Sale app instead of the name of your custom tender, 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 (#666666) in accordance with the design requirements.
2484

Custom Tender: Merchant-facing button in the Register app

2483

Custom Tender: Merchant-facing button in the Sale app

Customer-facing setup

Example to set up an activity for a customer-facing custom tender. Customer-facing user experiences must always be full-screen to prevent the customer from navigating away from your app.

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();
            }
        });
    }
}
  1. Add the required flags:
  • 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
  1. Declare a CUSTOMER_TENDER intent-filter in the AndroidManifest.xml file in order for Android to resolve any intents to your activity.
  2. 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>
  1. Optional. To use a logo instead of the name of your custom tender, 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 (#FFFFFF) in accordance with the design requirements.
  2. Required. Apply a full-screen Android theme (Theme.Holo.Light.NoActionBar.Fullscreen in this example) to a customer-facing experience so that the user interface appears smoothly before the system calls onCreate(Bundle savedInstanceState).
2651

Custom Tender: Customer Facing button in Secure Payments

Partial payment with custom tender

A merchant can initiate partial payments using a custom tender, depending on their Clover hardware and plan setup:

  • Partial payments allowed—Merchants using Clover hardware, such as a Station device with the Register and Register Lite plan that includes the Register or Payments app, can accept partial payments using a custom tender.
  • Partial payments not allowed—Merchants on a Payments Plus plan, such as those using a Clover Mini that includes the Sales app, can only process full payments using a custom tender.

Handle refunds on partial payments

You 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. Make sure 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 used, the EXTRA_AMOUNT is the sum of the lineItems.amounts that were selected. As such, the EXTRA_AMOUNT can be greater than the amount that was paid using the custom tender.

For orders on which the refund EXTRA_AMOUNT is less than or equal to the custom tender payment, the flow works as intended but you need to add logic to verify this.

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

Related topics