# Developing a Loyalty App
# General Overview
PoyntOS SDK provides an easy way for third-party application to register as a loyalty service on the smart terminal by implementing the IPoyntLoyaltyService
interface. The solution will work with any smart terminal application that deals with orders on PoyntOS.
TIP
We have also created a GitHub repository with a Loyal Application Sample (opens new window) for your reference.
# App Components
Loyalty Capability Configuration (loyalty_capability.xml)
Service implementation of IpoyntLoyaltyService (opens new window)
Activity to provide additional information such as PIN from the merchant side. (if needed)
# Capability Configuration
This configuration file provides the necessary information for PoyntOS to discover and register this application as a loyalty provider when the app is installed on the smart terminal. The configuration file should be located in the res/xml directory of your application and referenced from your app’s manifest.
<?xml version="1.0" encoding="utf-8"?>
<capability>
<!-- Use the package name as the capability identifier-->
<appid>co.poynt.posapp</appid>
<!-- capability supported by this app -->
<type>LOYALTY</type>
<!-- descriptive name of this capability. This name will show in Payment Fragment options menu-->
<provider>POS Loyalty</provider>
<!-- not used currently -->
<logo>@mipmap/ic_launcher</logo>
<!-- custom entry mode managed by capability -->
<entry_method type="CUSTOM" />
</capability>
Below is the snippet to export your capability service and reference the loyalty capability configuration:
<!-- Loyalty service and the activity needs to be exported-->
<service
android:name=".services.LoyaltyService"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="co.poynt.os.services.v1.IPoyntLoyaltyService"/>
</intent-filter>
<meta-data
android:name="co.poynt.os.service.capability"
android:resource="@xml/loyalty_capability"/>
</service>
<!-- Activity to fetch additional information from merchant or user -->
<activity
android:name=".activities.LoyaltyActivity"
android:exported="true"/>
# IPoyntLoyaltyService UI
Your Android Service class needs to implement the IPoyntLoyaltyService
interface’s process() method, and after verifying that the Payment object has an order, you can follow the applicable steps outlined below:
Create a discount object, update the order object and call
loyaltyApplied(payment, requestId)
on the listener to return control back to the Payment Fragment.If the discount cannot be applied call
noLoyaltyApplied(requestId)
on the listener.If you require an additional input from merchant or customer such as customer check-in, you can create an Intent and return it to the Payment Fragment using listener’s
onLaunchActivity(intent, requestId)
callback.
Payment Fragment will launch your activity using the intent provided. (Highlighted code in the sample)
public class LoyaltyService extends Service {
public static final String TAG = LoyaltyService.class.getSimpleName();
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
private final IPoyntLoyaltyService.Stub mBinder = new IPoyntLoyaltyService.Stub() {
@Override
public void process(Payment payment, String requestId, IPoyntLoyaltyServiceListener iPoyntLoyaltyServiceListener) throws RemoteException {
Log.d(TAG, "Process Payment");
// Apply discount if there is an order associated with the payment
if (payment.getOrder() != null) {
/* Use this block of code to launch an activity to collect additional information
Intent intent = new Intent(Intents.ACTION_PROCESS_LOYALTY);
intent.setComponent(new ComponentName(getPackageName(), LoyaltyActivity.class.getName()));
intent.putExtra("payment", payment);
iPoyntLoyaltyServiceListener.onLaunchActivity(intent, requestId);*/
Order order = payment.getOrder();
// Create a Discount object
Discount loyaltyDiscount = new Discount();
loyaltyDiscount.setAmount(-200l); // discount is always negative
loyaltyDiscount.setCustomName("Loyalty Discount");
loyaltyDiscount.setProcessor(getPackageName());
// Get the existing discounts from order and add the new discount to it
List<Discount> orderDiscounts = order.getDiscounts() != null ? order.getDiscounts() : new ArrayList<>();
orderDiscounts.add(loyaltyDiscount);
order.setDiscounts(orderDiscounts);
// Update the discount total in order amounts
long discountAmount = order.getAmounts().getDiscountTotal();
discountAmount += loyaltyDiscount.getAmount();
order.getAmounts().setDiscountTotal(discountAmount);
// Add the discount to net total to reflect in the order totals
long orderTotalAmount = order.getAmounts().getNetTotal();
orderTotalAmount += loyaltyDiscount.getAmount();
order.getAmounts().setNetTotal(orderTotalAmount);
payment.setOrder(order);
payment.setAmount(orderTotalAmount);
Log.d(TAG, "Applied discount to order");
iPoyntLoyaltyServiceListener.loyaltyApplied(payment, requestId);
} else {
Log.d(TAG, "Order not found, no discount applied");
iPoyntLoyaltyServiceListener.noLoyaltyApplied(requestId);
}
}
};
}
# Collecting Additional Input
If your loyalty application needs to perform an action such as collecting additional input from the customer or merchant, allowing the merchant to select specific reward, etc, your service class should create an explicit Intent for Payment Fragment to launch your activity.
TIP
Make sure you also pass the payment object as an extra in the intent.
We have added a sample activity below, which is launched by the Payment Fragment and presents a button which applies a “$2 Discount” to the order.
NOTE
Make sure to always set the result before exiting an activity and in the case where you apply a discount, make sure to create an intent with the action Intents.ACTION_PROCESS_LOYALTY_RESULT.
public class LoyaltyActivity extends Activity {
public static final String TAG = LoyaltyService.class.getSimpleName();
public static final String WATER_SKU = "082011500013";
@BindView(R.id.pinEditText)
EditText pinText;
Payment payment;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d(TAG, "Creating Loyalty Activity");
setContentView(R.layout.activity_loyalty);
ButterKnife.bind(this);
// Get the intent that launched the activity
Intent receivedIntent = getIntent();
if (receivedIntent == null) {
Log.e(TAG, "Loyalty activity launched with no intent");
setResult(Activity.RESULT_CANCELED);
finish();
} else {
handleIntent(receivedIntent);
}
}
// Handle all the negative cases
private void handleIntent(Intent intent) {
String action = intent.getAction();
// If the action is to process loyalty proceed further
if (Intents.ACTION_PROCESS_LOYALTY.equals(action)) {
// Get the payment object from intent extras
this.payment = intent.getParcelableExtra("payment");
// If payment object is null close the activity
if (payment == null) {
Log.e(TAG, "launched with no payment object");
Intent result = new Intent(Intents.ACTION_PROCESS_LOYALTY_RESULT);
setResult(Activity.RESULT_CANCELED, result);
finish();
} else {
// If the payment has no valid order finish the activity
if (payment.getOrder() == null) {
Log.e(TAG, "launched with no order object");
Intent result = new Intent(Intents.ACTION_PROCESS_LOYALTY_RESULT);
setResult(Activity.RESULT_CANCELED, result);
finish();
} else {
// wait till the Add discount is clicked
}
}
}
// If the action is not to process loyalty quit the activity
else {
Log.e(TAG, "launched with unknown intent action");
Intent result = new Intent(Intents.ACTION_PROCESS_LOYALTY_RESULT);
setResult(Activity.RESULT_CANCELED, result);
finish();
}
}
// Button that is clicked after entering PIN
@OnClick(R.id.enterLoyaltyButton)
public void onEnterLoyaltyButton(View view) {
// Check the pin and do necessary operations
long pin = Long.valueOf(pinText.getText().toString());
if (pin == 123456) {
Order order = payment.getOrder();
// Create a Discount object for the order
Discount loyaltyDiscount = new Discount();
loyaltyDiscount.setAmount(-200l); // discount is always negative
loyaltyDiscount.setCustomName("Loyalty Discount");
loyaltyDiscount.setProcessor(getPackageName());
// Get the existing discounts from order and add the new discount to it
List<Discount> orderDiscounts = order.getDiscounts() != null ? order.getDiscounts() : new ArrayList<>();
orderDiscounts.add(loyaltyDiscount);
order.setDiscounts(orderDiscounts);
// Update the discount total in order amounts
long discountAmount = order.getAmounts().getDiscountTotal();
discountAmount += loyaltyDiscount.getAmount();
order.getAmounts().setDiscountTotal(discountAmount);
long orderTotalAmount = order.getAmounts().getNetTotal();
// Check if the discount exceeds order total
if (orderTotalAmount >= Math.abs(discountAmount)) {
// Add the discount to net total to reflect in the order totals
orderTotalAmount += loyaltyDiscount.getAmount();
order.getAmounts().setNetTotal(orderTotalAmount);
payment.setOrder(order);
payment.setAmount(orderTotalAmount);
Log.d(TAG, "Applied discount to order");
Intent result = new Intent(Intents.ACTION_PROCESS_LOYALTY_RESULT);
result.putExtra("payment", payment);
setResult(Activity.RESULT_OK, result);
finish();
} else {
finishWithToast("Discount amount is larger than total order amount", Toast.LENGTH_LONG);
}
} else {
Toast.makeText(LoyaltyActivity.this, "Incorrect pin entered", Toast.LENGTH_SHORT).show();
pinText.setText("");
}
}
private void finishWithToast(String toastMessage, int toastLength) {
Toast.makeText(LoyaltyActivity.this, toastMessage, toastLength).show();
// It is important to set activity result before exiting
Intent result = new Intent(Intents.ACTION_PROCESS_LOYALTY_RESULT);
setResult(Activity.RESULT_CANCELED, result);
finish();
}
}
# Transaction Notification
If your loyalty application needs to get notified when the transaction is completed or canceled, you can create a BroadcastReceiver
and register it to listen to Intents.ACTION_TRANSACTION_COMPLETED and Intents.ACTION_PAYMENT_CANCELED.
# Item Discounts
Discounts can also be applied to a specific product within the order.
the following snippet applies a discount to an item within an order.
if (order.getItems() != null) {
// Get the list of items in the order and apply the discount to item if the order has it
List<OrderItem> orderItems = order.getItems();
for (int i=0; i<orderItems.size(); i++){
OrderItem item = orderItems.get(i);
if (item.getSku() != null && item.getSku().equals(WATER_SKU)) {
// Create a discount object
Discount itemDiscount = new Discount();
itemDiscount.setProcessor(getPackageName());
itemDiscount.setAmount(50l);
itemDiscount.setCustomName("Loyalty Discount");
// Add discount object to the item Discounts
List<Discount> itemDiscounts = item.getDiscounts() != null ? item.getDiscounts() : new ArrayList<>();
itemDiscounts.add(itemDiscount);
item.setDiscounts(itemDiscounts);
item.setDiscount(item.getDiscount() + itemDiscount.getAmount());
orderItems.set(i, item);
order.setItems(orderItems);
// Update order total discount
long discountAmount = order.getAmounts().getDiscountTotal();
discountAmount -= itemDiscount.getAmount();
order.getAmounts().setDiscountTotal(discountAmount);
// Add up the item discount to the total order discount
long orderTotalAmount = order.getAmounts().getNetTotal();
orderTotalAmount -= itemDiscount.getAmount();
order.getAmounts().setNetTotal(orderTotalAmount);
payment.setOrder(order);
payment.setAmount(orderTotalAmount);
Log.d(TAG, "Applied discount to item with sku: " + WATER_SKU);
Intent result = new Intent(Intents.ACTION_PROCESS_LOYALTY_RESULT);
result.putExtra("payment", payment);
setResult(Activity.RESULT_OK, result);
finish();
}
}
} else {
Log.d(TAG, "No items in order");
finishWithToast("No order items in the order", Toast.LENGTH_SHORT);
}
WARNING!
Product Identifier
The SKU and product Id's can change from merchant to merchant, use this with caution.
Once the discount is applied to the order, please remember to pass back the modified order in the payment object and finish the activity with a RESULT_OK.