Information

Android specs:

compile_sdk           : 31
min_sdk : 22
target_sdk : 30

Guide

1. Setup SDK

ClintFactory used to create Client interface instance with specified parameters

import pl.novelpay.retailer.factory.ClientFactory
import pl.novelpay.retailer.factory.RetailerConfiguration

ClientFactory.bindConfiguration(
SDKConfiguration.defaultConfiguration(
clientApplicationInfo = PackageInfo(
applicationName = applicationInfo.name,
applicationPackage = packageName
)
)
).bindContext(
context = applicationContext
).bindProtocolConfiguration(
protocolConfiguration = RetailerProtocolFactory.createProtocol(
RetailerConfiguration(
saleID = "SALE_ID",
operatorId = "OPERATOR_IDENTIFIER",
softwareComponent = SoftwareComponent(
providerCompanyIdentifier = "YourCompany",
name = "YourAppName",
softwareVersion = "1.0.0",
description = "Your App Description"
)
)
)
).bindInterceptor(
interceptor = { message: DomainMessage ->
when (message) {
// Check message type
is SuccessRetailerLoginResponse,
is ErrorRetailerLoginResponse,
is SuccessRetailerLogoutResponse -> {
//Handle message
}
}
}
).bindEventObserver { event: ObservableEvent ->
when (event) {
is ObservableEvent.ServerEvent -> Logger.d("New server event")
is ObservableEvent.TransactionStateChanged ->
when (event.state) {
is IdleTransactionState -> bringToForeground(MainActivity::class)
else -> {
// Ignore
}
}
}
}.build().provideCommunicationInterface(RetailerCommunicationInterface::class)

1.1 SDKConfiguration stands here for interprocess communication parameters.

This method allows to setup parameters of inter-process communication with Payment application

clientApplicationInfo parameter stands for "Your" application package info. It's being shared with PaymentApplication but doesn't affects communication itself.

paymentApplicationPackage is used to set up a package info of Payment application package. It does affect a communication process. If your environment is PAX device and PayOnSite application, don't provide explicitly this parameter.

bindConfiguration(
SDKConfiguration.defaultConfiguration(
clientApplicationInfo = PackageInfo(
applicationName = applicationInfo.name,
applicationPackage = packageName
)
)
)

1.2 ProtocolConfiguration stands for specific protocol parameters.

Our case is Nexo Retailer Protocol v3. Your application's parameters as ECR might be specified here. It's important to provide information about your application's software component. This is done by creating a SoftwareComponent object and passing it to the RetailerConfiguration.

See the ../../protocol/protocol-retailer/src/main/java/pl/novelpay/retailer/configuration/SoftwareComponent.kt description

import pl.novelpay.retailer.configuration.SoftwareComponent

bindProtocolConfiguration(
protocolConfiguration = RetailerProtocolFactory.createProtocol(
RetailerConfiguration(
saleID = "SALE_ID",
operatorId = "OPERATOR_IDENTIFIER",
softwareComponent = SoftwareComponent(
providerCompanyIdentifier = "YourCompany",
name = "YourAppName",
softwareVersion = "1.0.0",
description = "Your App Description"
)
)
)
)

1.3 Interceptor class which will handle all of the responses from Payment Application.

../../protocol/protocol-retailer/src/main/java/pl/novelpay/retailer/RetailerProtocol.kt class contains a RetailerResponse interface, all the Retailer messages are inherit from it.

bindInterceptor(
interceptor = { message: DomainMessage ->
when (message) {
// Check message type
is SuccessRetailerLoginResponse,
is ErrorRetailerLoginResponse,
is SuccessRetailerLogoutResponse -> {
//Handle message
}
}
}
)

Incoming parameter has generic type DomainMessage all of the messages in app2app communication are subtypes of this class. to specify handling strategy for each response typecheck is recommended. In this example, reaction on SuccessRetailerLoginResponse, ErrorRetailerLoginResponse, SuccessRetailerLogoutResponse is illustrated.

Each operation as Payment/Reversal/Refund etc has it's own message type. It could be found manually through documentation package retailer-protocol.

1.3.1 Here is map of requests and responses for main operations:

Login:

To send request use - Client.sendLoginRequest()

Request Response Response Subtypes
RetailerLoginRequest RetailerLoginResponse SuccessRetailerLoginResponse, FailedRetailerLoginResponse
Payment:

To send request use - Client.sendPaymentRequest()

Request Response Response Subtypes
RetailerPaymentRequest RetailerPaymentResponse, RetailerDisplayRequest SuccessRetailerPaymentResponse, ErrorRetailerPaymentResponse

Payment request is used for Refunds, Pre-Authorisations and Pre-Authorisation Completions as well. There is no separate subtypes for those operations. DisplayRequest will notify during processing which operation is being executed.

CardAcquisition:

To send request use - Client.sendCardAcquisitionRequest()

Request Response Response Subtypes
RetailerCardAcquisitionRequest RetailerCardAcquisitionResponse, RetailerDisplayRequest SuccessRetailerAcquisitionResponse, ErrorRetailerAcquisitionResponse

CardAcquisition request used for Dual Transaction and Acquisition Operation (reading card data).

Notice!

If you are using it in Dual Transaction mode, Payment application will notify you with processing state as it will expect Dual Transaction "Completion" with PaymentOperation referring this CardAcquisitionTransactionResponse

Abort:

To send request use - Client.sendAbortRequest()

Request Response Response Subtypes
RetailerAbortRequest RetailerPaymentResponse, ErrorRetailerPaymentResponse

If AbortRequest was sent after payment app sent authorisation request to host, it will be ignored.

Reversal:

To send request use - Client.sendReversalRequest()

Request Response Response Subtypes
RetailerReversalRequest RetailerReversalResponse, RetailerDisplayRequest SuccessRetailerReversalResponse, ErrorRetailerReversalResponse
Transaction Status:

To send request use - Client.sendTransactionStatusRequest()

Request Response Response Subtypes
RetailerTransactionStatusRequest RetailerTransactionStatusResponse, RetailerDisplayRequest SuccessRetailerTransactionStatusResponse, ErrorRetailerTransactionStatusResponse

1.4 EventObserver class which will handle all of PaymentApplication states changes and events.

Observer receives ObservableEvent instances. There are two subtypes of it:

  1. ObservableEvent.ServerEvent - stands for some events happend in Payment Application

  2. TransactionStateChanged - stands for state changes. For example, when Payment Application starts PaymentProcessing, EventObserver will receive PaymentTransactionState instance. When PaymentProcessing considered finished by Payment Application, EventObserver will receive IdleTransactionState instance. Transitions between states might be recognized through saving previously notified state's instances.

bindEventObserver { event: ObservableEvent ->
when (event) {
is ObservableEvent.ServerEvent -> Logger.d("New server event")
is ObservableEvent.TransactionStateChanged ->
when (event.state) {
is IdleTransactionState -> bringToForeground(MainActivity::class)
else -> {
// Ignore
}
}
}
}

bringToForeground(MainActivity::class) - is function, responsible for "dragging" to foreground your application after state changes in Payment Application. MainActivity:class is placeholder for your currently running application's activity. If your application is Single-Activity you can don't worry about activity transitions so just provide your Main Activity class as in example.

For example: You'd like show advertisement in your application or custom transaction details during payment processing instead of Payment Application's spinner. All you have to do to achive it - specify path for PaymentTransactionState like here:

bindEventObserver { event: ObservableEvent ->
when (event) {
is ObservableEvent.ServerEvent -> Logger.d("New server event")
is ObservableEvent.TransactionStateChanged ->
when (event.state) {
is PaymentTransactionState -> bringToForeground(MainActivity::class)
else -> {
// Ignore
}
}
}
}

now, when Payment Processing started in Payment Application your will be brought to foreground.

If you also would like to transit to foreground your application after Payment Applications finish transaction, add coresponding path for IdleTransactionState:

bindEventObserver { event: ObservableEvent ->
when (event) {
is ObservableEvent.ServerEvent -> Logger.d("New server event")
is ObservableEvent.TransactionStateChanged ->
when (event.state) {
is IdleTransactionState -> bringToForeground(MainActivity::class)
is PaymentTransactionState -> bringToForeground(MainActivity::class)
else -> {
// Ignore
}
}
}
}

2. SDK Usage

Once you've done with SetUp, you can use instance returned by ClientFactory::build function to initiate communication with Payment Application.

SDK Lifecycle ideally should be linked to your application. However, if you'd like to recreate SDK instance, you might use specific functions new() and create().

Unlike build() function create() insures a new instance creation, while build() might return a previous instance.

Function new() ensures clear of previously bound resources.

ClientFactory.new().bindProtocolConfiguration(
RetailerProtocolFactory.createProtocol(
RetailerConfiguration(
saleID = "Your_Sale_Identification",
poiID = "Your_Identification_Of_POI",
operatorLanguage = "EN",
softwareComponent = SoftwareComponent(
softwareVersion = "1.1",
name = "SDKTests",
providerCompanyIdentifier = "Novelpay",
description = "Test environment"
),
operatorId = "TestOperator"
)
)
).create()

2.1 Client interface:

../../protocol/protocol-retailer/src/main/java/pl/novelpay/retailer/RetailerProtocol.kt contains also declaration of CommunicationInterface

It used to send requests and response for those requests will be delivered to previously registered in 1.3 Interceptor.

2.2 Nexo Retailer Protocol sepcific:

Every communication session should start with RetailerLoginRequest - Client::sendLoginRequest. If session was interrupted due to:

  • Payment Application update

  • Payment Application restart

  • Client's (your) application restart

  • Connection between applications was lost

  • RetailerLogoutRequest was sent

New Login procedure should be triggered to instantiate the session. Otherwise requests from your application will be considered unauthorised and will be ignored by Payment Application.

2.3 Common flows

The ../../protocol/protocol-retailer/src/main/java/pl/novelpay/retailer/client/RetailerCommunicationInterfaceImpl.kt is the API available to the client application. It provides methods to send requests to the payment application, such as sendLoginRequest and sendPaymentRequest.

The ../../client/src/main/java/pl/novelpay/client/sdk/interceptor/ClientInterceptor.kt allows you to observe incoming and outgoing messages. You can use it to handle responses from the payment application.

Here are some examples of common flows:

Login flow

To initiate a session with the payment application, you need to send a login request. The sendLoginRequest method from the RetailerCommunicationInterface is used for this purpose.

Here is an example of how to send a login request and handle the response:

// Send the login request
clientSDK.sendLoginRequest()

You can then handle the login response in your ClientInterceptor:

bindInterceptor(
interceptor = { message: DomainMessage ->
when (message) {
is SuccessRetailerLoginResponse -> {
// Handle successful login
}
is ErrorRetailerLoginResponse -> {
// Handle login error
}
}
}
)

Notice

  1. All the messaging with Payment application should be done within session. Once you've received successful Login response it points that session was created. Basically there is no need to send Logout request (if you don't want to clear the session explicitly).

  2. If Login operation wasn't successful it might be caused by several reasons:

A. Payment Application is in initialization stage. Then you'll observe a BUSY error condition

B. Login Request has incorrect data in its fields. It will be responded with LoginResponse containing problem description

If Login procedure was failed it's worth to perform a retry, without login, no operations would be available in Payment application.

Payment flow

To perform a payment, you need to send a payment request. The sendPaymentRequest method from the RetailerCommunicationInterface is used for this purpose.

Here is an example of how to send a payment request and handle the response:

// Send the payment request
clientSDK.sendPaymentRequest(
RetailerMessageArguments.PaymentRequestMessageArguments.RegularPaymentRequestMessageArguments(
saleTransactionId = "your_transaction_identification",
paymentAmounts = PaymentAmounts(
currency = "EUR",
amount = BigDecimal("10.00")
),
paymentType = PaymentType.NORMAL
)
)

Notice

Your transaction identification should be alphanumeric, and it's length should be <= 35 symbols

You can then handle the payment response in your ClientInterceptor:

bindInterceptor(
interceptor = { message: DomainMessage ->
when (message) {
is SuccessRetailerPaymentResponse -> {
// Handle successful payment
}
is ErrorRetailerPaymentResponse -> {
// Handle payment error
}
is RetailerDisplayRequest -> {
// Handle display request from the payment application
}
}
}
)

Blocking communication

For cases where you need to wait for a specific response before proceeding, you can use the BlockingMessageGateway utility. This allows you to send a request and suspend the current coroutine until a matching response is received.

Here's how to use it:

First, create an instance of BlockingMessageGateway:

val blockingMessageGateway = BlockingMessageGateway()

Then, you need to pass the incoming messages from your ClientInterceptor to the BlockingMessageGateway:

bindInterceptor(
interceptor = { message: DomainMessage ->
// Pass the message to the gateway
blockingMessageGateway.onNewMessage(message)

// You can still handle messages in a non-blocking way here
when (message) {
is RetailerDisplayRequest -> {
// Handle Display Request
}
// ... other non-blocking handlers
}
}
)

Now you can send requests in a blocking way using sendBlocking. This function will suspend until a response of the specified type is received.

// Example of sending a login request and waiting for the response
val loginResponse = blockingMessageGateway.sendBlocking<RetailerLoginResponse>(
timeoutMillis = 30000L // Custom timeout of 30 seconds
) {
clientSDK.sendLoginRequest()
}

when (loginResponse) {
is SuccessRetailerLoginResponse -> {
// Handle successful login
}
is ErrorRetailerLoginResponse -> {
// Handle login error
}
}

The sendBlocking function also accepts an optional timeoutMillis parameter, which defaults to 60,000 milliseconds (60 seconds). You can provide a custom timeout to control how long the function should wait for a response. If the timeout is exceeded, a kotlinx.coroutines.TimeoutCancellationException will be thrown.

Choosing the right response type

When using sendBlocking, it's important to specify the correct super-type for the response you are expecting. This ensures that you capture all possible outcomes (like success or failure).

For example, for a login request, you should await RetailerLoginResponse. This is a sealed class that encompasses both SuccessRetailerLoginResponse and ErrorRetailerLoginResponse. If you were to only await SuccessRetailerLoginResponse, your coroutine would not resume if the login fails, and it would eventually time out. By awaiting RetailerLoginResponse, you can handle both success and error cases as shown in the example above.

3. SDK parameters mappings on Acquirer API (Communication with host)

Function name Input parameters Request name Host field name Host Field Format Remark
sendPaymentRequest saleReferenceId AccptrAuthstnReq SaleRefId Max35Text max lenght 35
transactionID SaleRefNb Max35Text max lenght 35
paymentAmounts -> currency Ccy Code (Example EUR) Should match with terminal currency!
paymentAmounts -> amount TotalAmount Double: totalDigits 18; fractionDigits 5;
paymentAmounts -> tipAmount CashBack Double: totalDigits 18; fractionDigits 5;
paymentAmounts -> cashbackAmount Gratuity Double: totalDigits 18; fractionDigits 5;
paymentAmounts -> paidAmount Not used Not used
paymentType
sendReversalRequest saleReferenceId AccptrAuthstnReq SaleRefId Max35Text max lenght 35
poiTransactionId SaleRefNb Max35Text max lenght 35
transactionData -> forecastedAmount TtlAmt Double: totalDigits 18; fractionDigits 5; Should completely match with amount of transaction!
transactionData -> reversedAmount Not used Not used
transactionData -> currency Not used Not used

App2AppSDK

The process of SDK setting up:

1. Library import:

Add the appropriate repository inside your project's settings.gradle.

From Remote Repository

dependencyResolutionManagement {  
repositories {
// here
}
}

May not work with

   repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)

Comment this line in case it causes problems. Notice that in this case your subprojects would have right to use third-party repositories.

maven {  
credentials {
username "$MAVEN_LOGIN"
password "$MAVEN_PASSWORD"
}
url = "http://public.novelpay.pl:8088/repository/novelpay-android-release/"
allowInsecureProtocol = true
}
maven {
credentials {
username "$MAVEN_LOGIN"
password "$MAVEN_PASSWORD"
}
url = "http://public.novelpay.pl:8088/repository/novelpay_android_repository/"
allowInsecureProtocol = true
}

You may add credentials right in inside maven block or define it in gradle.properties

	MAVEN_LOGIN="login"
MAVEN_PASSWORD="password"

2. Android specs:

compile_sdk           : 31
min_sdk : 22
target_sdk : 30

Modules description:

  • Client - contains point of interaction with SDK classes

  • Protocol-Retailer - contains Retailer domain message classes

  • AIDL-API - is used in terms of backward compatibility and contains code responsible for communication between applications

  • Util - is just a set of tools used across SDK

All modules:

Link copied to clipboard
Link copied to clipboard
Link copied to clipboard

There is a bunch of useful stuff for discovering integration capabilities

Link copied to clipboard
Link copied to clipboard

The protocol-retailer module provides the definitive implementation of the Retailer (POI-PIS) protocol. It is the primary high-level communication language between Sale Systems (ECR) and Payment Terminals (POI) within the App2AppSDK, featuring robust support for both XML and JSON data encapsulations.

Link copied to clipboard

Utilities module describe abstractions used across the SDK