Custom tenders

Overview

Custom tenders allow developers to build out additional payment options that can launch as either customer-facing or merchant-facing experiences. These user flows enable additional payment methods for a given merchant, such as QR scanning.

This tutorial walks through the process of building an app that can create a custom tender type. It also implements a payment flow that can be initiated from the Register, Sale, and Secure Payment apps.

Use Cases

  1. Clover Mini or Clover Mobile:
    • Initiated through the Register or Sale apps for a merchant-facing experience
    • Initiated through the Secure Payments app for a customer-facing experience
  2. Clover Station or Clover Mini with a tethered Clover Mini:
    • Initiated through the Secure Payments app on a tethered Clover Mini for a customer-facing experience
    • Initiated through the Register or Sale apps for a merchant-facing experience
  3. Semi-integrated approach of a third-party POS with a tethered Clover Mini:
    • Initiated through the Secure Payments app on a tethered Clover Mini for a customer-facing experience

Design Requirements

  1. There must be a Cancel button so the customer or merchant can exit the payment flow.
  2. The graphic for the tender button must be a logo with one of the following resolutions:
    • mdpi: 230 x 104px
    • hdpi: 345 x 156px
  3. You must include a logo asset corresponding to each experience (customer and/or merchant). This should be:
    • A white (#FFFFFF) logo with a transparent background for the Secure Payments app (for a customer-facing experience).
      logo-customer-tender
    • A gray (#666666) logo with a transparent background for the Sale app (for a merchant-facing experience).
      logo-merchant-tender
      If you do not provide a logo, the button will display the name of the custom tender.

Note

You may want to review the Example App in the Android Examples repository to see how we’ve implemented these features.

Create a Custom Tender app

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

Note

Setup for this tutorial:

  • JVM 1.8.0
  • Gradle 2.3
  • Android Studio 1.3.2

Initializing 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.

Note

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 on your Clover device.

Getting started

  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"

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

This is a streamlined example. Note that production-ready code must also be able to handle error messages and properly inform the user.

Custom Tender Initialized
Custom Tender Initialized screen of Extensible Tender Example.

Linking 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.

Note

In some cases, it may make sense to allow the merchant to use either method, such as in the phone number example above. Please 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<Parcelable> 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();
                }
            });
        }
    }
  2. You will 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.

    Custom Tender: Merchant-facing button in the Register app
    Custom Tender: Merchant-facing button in the Register app.

    Custom Tender: Merchant-facing button in the Sale app.

Customer-Facing Setup

    1. 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 will 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).

    Custom Tender: Customer Facing button in Secure Payments
    Custom Tender: Customer Facing button in Secure Payments

Partial payments

Support for 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.

Overview

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.

Handling 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" />
  2. From your Refund Activity, capture some Extras from the Refund Intent:final long amount = 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);
  3. 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();

Note

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’l also need to handle cases where the custom tender was applied as a partial payment, and pass back the appropriate EXTRA_AMOUNT to Clover before calling finish().

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