Create custom tender apps
Before you begin
To follow this tutorial, you need to be 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 use the TenderConnector
to initialize a custom tender for our merchant.
- Add the SDK as a dependency in your
build.gradle
file.Example:compile 'com.clover.sdk:clover-android-sdk:latest.release'
- 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.
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)
inonCreate
to create a default exit result.
Merchant-facing setup
- 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();
}
});
}
}
- You need to declare a
MERCHANT_TENDER
intent filter in theAndroidManifest.xml
file in order for Android to resolve any intents to your activity. Setexported
totrue
.
<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.
Customer-facing setup
Example 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 callsonCreate(Bundle savedInstanceState)
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.
- 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" />
- 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);
- 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, theEXTRA_AMOUNT
is the sum of thelineItems.amounts
that were selected. As such, theEXTRA_AMOUNT
might be greater than the amount that was paid using the custom tender. For Orders on which the Refund'sEXTRA_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 callingfinish()
.
For more information, see this thread in the Clover Developer Community.
Updated 2 months ago