implementing pos apps - AEVI-AppFlow/pos-android-sdk GitHub Wiki
This page describes how to implement POS applications using the Payment Initiation API
. The API provides methods allowing an application to initiate flows to perform various functions, as well as to query the system for general information.
It is recommended you familiarise yourself with the concepts below before continuing on this page.
For API docs, please see JavaDocs.
Please also see the Payment Initiation Sample
for extensive examples of how to use this API.
The main entry point to the API is via the PaymentApi
class. From here you can retrieve the client object that allows you to interact with different parts of the API.
// You must check that the processing service is installed, and if not, show message to merchant to contact support
if (!PaymentApi.isProcessingServiceInstalled(context)) {
// Show error message and exit
}
PaymentClient paymentClient = PaymentApi.getPaymentClient(context);
Your application can query the API for information that will help you to decide what kind of flows can be initiated and what data can be defined in the request.
This information is contained with a PaymentSettings
model object which exposes useful information such as;
- Installed flow services
- Flow configurations
- FPS settings
paymentClient.getPaymentSettings()
.subscribe(new Consumer<PaymentSettings>() {
@Override
public void accept(PaymentSettings paymentSettings) throws Exception {
// ...
}
});
A PaymentFlowServices
object can be obtained via paymentSettings.getPaymentFlowServices()
, which can be used to query for collated information across all flow services, or from a specific flow service.
- Supported currencies
- Supported payment methods
- Custom request types
- The functions of a flow service
- What stages it supports
- Whether it can update amounts
- Whether it can pay amounts
- And much more...
This, in combination with the information from the next section, will help you determine what is possible on any given device.
The FlowConfigurations
model returned via paymentSettings.getFlowConfigurations()
exposes information about all the active flows in FPS.
The flow configurations alone determine what functions are available for a client application, such as whether refund is supported or whether a sale can be split or not. These flow configurations are generally defined by AEVI and/or the acquirer/bank to match the requirements for the environment and/or enabled device in which they will be used.
It is assumed that most client applications will have a particular range of functions they support. This API is designed for this, allowing the client to:
- Check whether a given flow type (such as "sale") is supported
- Get the defined flows for that flow type (can be zero to many)
- Check what stages are defined for a given flow
- And more...
It is also possible to get all or stream all the available flow configs and apply Rx filtering on these to get the results required.
The Flow Processing Service (FPS), can to some extent be configured by acquirers and/or merchants.
Examples of such settings are
- Various timeouts relating to flow execution
- Feature toggles (such as currency conversion, multi-device, etc)
- UI theming, show/hide controls, etc
These settings can be retrieved via paymentSettings.getFpsSettings()
.
There are two ways to initiate a flow via PaymentClient
.
initiatePayment(Payment payment)
initiateRequest(Request request)
A Payment
is used when some client-defined amount of money is being transferred as a result of it. Examples are sale (purchase / payment), refund, pre-authorisation, etc. These all involve money being moved from one entity to another - customer to merchant or vice versa, as defined by the client application. The Payment
model has been defined to support these standard point of sale scenarios.
A Request
on the other hand can be used for a wide range of scenarios whether it involves money or not, such as tokenisation, batch closure, receipt or printing requests, reversals/voids, etc. In order to support these different types of functions, the Request
/ Response
model pair was created to allow for fully custom data being passed in either direction. Via this approach, we can also allow flow services to define their own custom request types, which they can advertise via the PaymentFlowServiceInfo
for clients to review and initiate.
What data is relevant for any given request is defined by its request type. Please see the documentation in Request Types for a full list of types and associated data.
The most common scenario of initiating flows is where the POS app supports a set of defined types up front and checks for availability of these before presenting options to merchant and/or initiating a flow.
This is done via the FlowConfigurations
model which was described earlier, via a call to the isFlowTypeSupported(String flowType)
method.
As an example, if you want to check that there is a flow for refunds, you can use,
if (flowConfigurations.isFlowTypeSupported("refund")) {
// Present or initiate refund
} else {
// Handle refunds not supported
}
The second, generally less useful option, is to query programmatically for all the supported types. As most of the types require specific code for building the request and handling the response, this is only really useful for prototyping, testing, etc.
In order to make a payment, a Payment
object needs to be built. This is done via using the PaymentBuilder
class. At a minimum a Payment
must have a flow type and amounts set. Depending on the type of flow being initiated, other parameters may be defined as well. See Request Types for examples for each type.
Please note that the base amount set in Amounts
should always be inclusive of tax (GST, VAT, etc).
Amounts amounts = new Amounts(1000, "GBP");
PaymentBuilder paymentBuilder = new PaymentBuilder()
.withPaymentFlow("sale")
.withAmounts(amounts);
If the flow name is known (in scenarios where there are multiple flows per type), it is recommended that the flow name is specifically given by using the following method to specify the exact flow required
withPaymentFlow(type, name)
.
You can define and provide basket details, so that other applications in the flow can use that information to provide value added services, like applying item based discounts, or print as line items on a receipt.
As per the Amounts
base amount, note that the basket item amount must be inclusive of tax.
Basket basket = new Basket("myBasket");
basket.addItems(new BasketItemBuilder().withLabel("myBasketItem").withCategory("itemCategory").withAmount(500).build());
paymentBuilder.withBasket(basket);
You can ask for the payment to be split across multiple transactions. This is typically used to split a bill across multiple customers in a restaurant for example.
paymentBuilder.withSplitEnabled(true);
If you have access to a card token for the payment application to use, you can provide it via
paymentBuilder.withCardToken(token);
In general for payment requests, you can also specify customer details, and set any custom data you like.
Customer customer = getCustomerDetails();
paymentBuilder.withCustomer(customer);
// Add some bespoke data (value can be of any type)
paymentBuilder.addAdditionalData("myDataKey", "myDataValue");
Once you have finished building your payment, you can send it to FPS using the initiatePayment
method and subscribe to receive information whether the request was accepted or rejected by FPS. Responses are (no longer) returned via the initiation rx stream due to the nature of Android component lifecycles where there is no guarantee that the component (activity or service) that initiated the request is still alive by the time the flow has completed. See Handling Responses for more info.
// Assign the subscribe return value to a field and ensure to dispose in activity onDestroy()
paymentClient.initiateRequest(paymentBuilder.build()).subscribe(new Action() {
@Override
public void run() throws Exception {
Log.i(TAG, "Request accepted");
finish(); // If in an activity, it should be finished at this stage
}
}, new Consumer<Throwable>() {
@Override
public void accept(Throwable throwable) throws Exception {
if (throwable instanceof FlowException) {
FlowException flowException = (FlowException) throwable;
// Check flowException.getErrorCode() against ErrorConstants
// Log flowException.errorMessage() for further detail
} else {
// Unexpected exception
}
}
});
It is possible that errors will be propagated back to your onError
subscription method if the request is rejected by FPS, or some other error occurs. Please see Handling Errors for more information on how to manage this.
See Handling Responses.
As mentioned above, examples of generic requests are tokenisation, reversal or dynamic types as supported by flow services.
For these types of requests, all applications must know what data to expect for any given request type. Please see Request Types for common requests and code snippets for creating requests and responses.
Once you have finished building your request, you can send it to FPS using the initiateRequest
method and subscribe to receive information whether the request was accepted or rejected by FPS. Responses are (no longer) returned via the initiation rx stream due to the nature of Android component lifecycles where there is no guarantee that the component (activity or service) that initiated the request is still alive by the time the flow has completed. See Handling Responses for more info.
Request request = new Request(flowType);
request.setFlowName(flowName); // If the flow name is known, it should be set for multi-flow scenarios
request.addAdditionalData("dataKey", "dataValue");
paymentClient.initiateRequest(request).subscribe(new Action() {
@Override
public void run() throws Exception {
Log.i(TAG, "Request accepted");
finish(); // If in an activity, it should be finished at this stage
}
}, new Consumer<Throwable>() {
@Override
public void accept(Throwable throwable) throws Exception {
if (throwable instanceof FlowException) {
FlowException flowException = (FlowException) throwable;
// Check flowException.getErrorCode() against ErrorConstants
// Log flowException.errorMessage() for further detail
} else {
// Unexpected exception
}
}
});
A flow service can define custom request types, for which FPS will create an ad-hoc flow configuration, allowing client applications to initiate them. These custom request types are initiated and handled in the same was as the generic requests, meaning the previous section is valid for custom types as well. The main difference is how they are defined and documented. It is possible that no extra data is passed to or from the the flow service for their custom type. If however such data is required, see Bespoke Data for more information.
Note! Multi-device, whilst technically supported, is unfortunately not enabled in AppFlow v1 and v2.
In a multi-device (sometimes referred to as integrated) scenario, it is possible for the client to specify which device it wants to target for the request. This is useful when there are multiple customer facing devices and you want the request directed to the device the customer is closest to. You can get the id from the Device
object and then set that in the Request
model, presumably from the merchant making a choice via device name.
paymentClient.getDevices().subscribe(new Consumer<List<Device>>() {
@Override
public void accept(List<Device> devices) throws Exception {
// Parse list
}
});
As previously explained, the way Android components work with lifecycles and that AppFlow is service driven, means that there is no guarantee an activity will still be alive to receive a response once the flow is completed.
This separation (in rx terms, a broken chain) should not be a major issue for client applications provided that separate activities are used for the initiation and the response handling. Our recommendation is that there is one activity responsible for initiating the flow and another activity responsible for handling the response launched from the response listener service. Any state that is required for both the activities can easily be managed via dependency injection, singletons, etc. Do note that the responses contain the originating request data.