Payments and mandates with the Android SDK

Install the Android SDK, process payments and handle success or error responses.

Android SDK introduction

With the TrueLayer SDK for Android, you can quickly add a user interface to your app to initiate an open banking payment, or to create a recurring payment mandate. This page guides you through how to set up and use the TrueLayer SDK in your Android app.

1920

Image of what the native screens look like on an Android device. There are two screenshots in the image, one shows the bank selection screen and the other shows the payment confirmation screen.

The Android SDK presents native screens that allows your users to select their bank and consent to the payment or mandate. Then, it redirects them to their banking app or website to authorise the payment or mandate. It also handles network requests and errors and gives you some options to customise the user interface.

Compatibility

The SDK is designed to work with Android 5.0 (API level 21) and above. It is currently optimised for the UK, Ireland, France, Germany, Spain, the Netherlands, and Lithuania.

Beta testers can also use the Android SDK for banks in Austria, Belgium, Finland, Poland and Portugal.

The user interface can be displayed in English, Spanish, French, German, Dutch, Portuguese, Polish, Finnish and Lithuanian.

The Android SDK payment journey

When a customer makes a payment through your Android SDK integration:

  1. The user chooses to pay with an instant bank payment, or to set up a recurring payment mandate.
  2. Your backend creates a payment or mandate with TrueLayer. In return you receive a token.
  3. Your app uses the id and resource_token and initialises the SDK.
  4. Your user selects and confirms their provider on the provider selection screen. If the bank needs additional information from the user, such as IBAN or branch, then the SDK asks the user to enter these. Additionally:
    • If a bank is unavailable it's greyed out on the provider selection screen, so the user can attempt to use a different bank.
    • If your user is paying internationally from certain French or Finnish banks, a screen displays that explains how to enable international payments.
    • If paying via a European provider that supports it, the user can select to pay with Sepa Instant or Sepa Credit.
  5. How the user authorises the payment depends on the authorisation flow the banking provider uses:
    • For providers that use a redirect flow, the mobile SDK redirects your user to their bank's website or app. Your user authorises the payment in their bank's website or app.
    • For providers that use an embedded flow, the mobile SDK collects the user's credentials and handles strong customer authentication in order to authorise the payment.
  6. Once the authorisation is complete, the user is redirected to your return URI. You can use our payments results screen to confirm the result to your user.

Create a payment or mandate with the Android SDK

The integration process with the Android SDK is largely the same for payments and mandates. Where there are differences, you will see two different code blocks below.

Before you start

Register a return URI in Console. Go to Console > Settings > Allowed redirect URIs to do this.

Your user is redirected back to your return URI, typically your website or application, at the end of the payment or mandate journey.

Configuration overview

There are four steps to configuring the Android SDK with the Payments API v3:

  1. Install the SDK.
  2. Start the SDK.
  3. Process a payment or mandate.
    There are three integration options for this:
    • AndroidX activity
    • Android activity
    • Jetpack Compose
  4. Confirm the result.

Install the SDK

Before you add the SDK to your project, ensure your build.gradle configuration is configured to support minSdk 26 (Android 8.0) or higher, and configure the packagingOptions below:

android {
    defaultConfig {
        applicationId "com.example.myapp"
        minSdk 26 // Sdk 26 or higher supported
    }


    packagingOptions {
        resources {
            pickFirsts += ['META-INF/LICENSE-MIT']
        }
    }
}

To add the SDK to your project, include the TrueLayer Payments SDK in your dependencies:

// Add to your projects `build.gradle`.
implementation "com.truelayer.payments:ui:2.0.0"

If you plan to support Android versions below 8.0, then you need to make sure your project has desugaring enabled. This is important because without desugaring, in specific scenarios, the SDK may crash on devices running Android API versions below 26.

The following code example can help you to enable desugaring in your project.

```groovy
android {
    // this part will enable core library desugaing
    compileOptions {
        coreLibraryDesugaringEnabled true
    }
    // this part will remove excess LICENSE-MIT files
    packagingOptions {
        resources {
            pickFirsts += [‘META-INF/LICENSE-MIT’]
        }
    }
}
dependencies {
    // Add to your projects `build.gradle`.
    // We are currently using following version of desuga libraries
    coreLibraryDesugaring “com.android.tools:desugar_jdk_libs:1.2.2”
}
```

Initialise the SDK

To use the SDK, you have to initialise it first before invoking any other SDK method.

Below is an example:

import com.truelayer.payments.core.domain.configuration.Environment
import com.truelayer.payments.core.domain.configuration.HttpConnectionConfiguration
import com.truelayer.payments.core.domain.configuration.HttpLoggingLevel
import com.truelayer.payments.ui.TrueLayerUI

// Initialize the SDK with your application context.
TrueLayerUI.init(context = applicationContext) {
    // optionally choose which environment you want to use: PRODUCTION or SANDBOX
    environment = Environment.PRODUCTION
    // Make your own custom http configuration, stating custom timeout and http request logging level
    httpConnection = HttpConnectionConfiguration(
        httpDebugLoggingLevel = HttpLoggingLevel.None
    )
}

Initialise with AppStartup

If you are using AppStartup for your integration, you need to also add WorkManagerInitializer to your dependencies:

import androidx.work.WorkManagerInitializer
(...)
override fun dependencies(): List<Class<out Initializer<*>>> {
    return listOf(WorkManagerInitializer::class.java)

Process a payment

Depending on your requirements, you can integrate the Android SDK into your payment process with either AndroidX Activity, the older Android Activity or with Jetpack Compose.

Whichever approach you use, you need to provide the id and the resourceToken obtained from the backend at payment creation. Pass the id and resourceToken to the SDK via PaymentContext or MandateContext.

The following kdoc explains all the available parameters:

/**
 * ProcessorContext to identify payment or mandate to be processed
 * [Payment API](https://docs.trulayer.com/)
 *
 * @property id payment or mandate identifier obtained from
 *                     create payment API [crate payment API](https://docs.truelayer.com/reference/create-payment)
 *                     or create mandate API [crate payment API](https://docs.truelayer.com/reference/create-mandate)
 * @property resourceToken resource token obtained from
 *                     create payment API [crate payment API](https://docs.truelayer.com/reference/create-payment)
 *                     or create mandate API [crate payment API](https://docs.truelayer.com/reference/create-mandate)
 * @property redirectUri uri that will be invoked once payment authorization is completed
 */
@Parcelize
sealed class ProcessorContext : Parcelable {
    abstract val id: String
    abstract val resourceToken: String
    abstract val redirectUri: String

    /**
     * PaymentContext to identify payment to be processed
     * [Payment API](https://docs.trulayer.com/)
     *
     * @property id payment identifier obtained from
     *                     create payment API [crate payment API](https://docs.truelayer.com/reference/create-payment)
     * @property resourceToken resource token obtained from
     *                     create payment API [crate payment API](https://docs.truelayer.com/reference/create-payment)
     * @property redirectUri uri that will be invoked once payment authorization is completed
     * @property preferences object containing various preferences that allow to override default SDK
     *                     behaviour in certain aspects.
     */
    @Parcelize
    data class PaymentContext(
        override val id: String,
        override val resourceToken: String,
        override val redirectUri: String,
        val preferences: PaymentPreferences? = null
    ) : ProcessorContext()

    /**
     * MandateContext to identify mandate to be processed
     * [Payment API](https://docs.trulayer.com/)
     *
     * @property id mandate identifier obtained from
     *                     create mandate API [crate payment API](https://docs.truelayer.com/reference/create-mandate)
     * @property resourceToken resource token obtained from
     *                     create mandate API [crate payment API](https://docs.truelayer.com/reference/create-mandate)
     * @property redirectUri uri that will be invoked once payment authorization is completed
     * @property preferences object containing various preferences that allow to override default SDK
     *                     behaviour in certain aspects.
     */
    @Parcelize
    data class MandateContext(
        override val id: String,
        override val resourceToken: String,
        override val redirectUri: String,
        val preferences: MandatePreferences? = null
    ) : ProcessorContext()

    /**
     * Preferences class that allows to set preferences and override default SDK behaviour
     * in certain aspects.
     *
     * @property preferredCountryCode (optional) in case there are available payment providers from
     *                      multiple countries the SDK will try select most appropriate one automatically.
     *                      You may want to override that behaviour.
     *                      By setting the preferredCountryCode (ISO 3166-1 alpha-2, example: "GB" or "FR"),
     *                      the SDK will try first to select preferred country before falling back to the
     *                      auto selection.
     * @property paymentUseCase allows to override the default use case. The result of the override will cause
     *                      the text on the payment review screen to update accordingly
     */
    @Parcelize
    data class PaymentPreferences(
        val preferredCountryCode: String? = null,
        val paymentUseCase: PaymentUseCase = PaymentUseCase.default
    ) : Parcelable {
        internal fun intoInternalPreferences() =
            com.truelayer.payments.ui.models.PaymentContext.Preferences(
                preferredCountryCode = preferredCountryCode,
                paymentUseCase = paymentUseCase
            )
    }

    /**
     * Preferences class that allows to set preferences and override default SDK behaviour
     * in certain aspects.
     *
     * @property preferredCountryCode (optional) in case there are available mandate providers from
     *                      multiple countries the SDK will try select most appropriate one automatically.
     *                      You may want to override that behaviour.
     *                      By setting the preferredCountryCode (ISO 3166-1 alpha-2, example: "GB" or "FR"),
     *                      the SDK will try first to select preferred country before falling back to the
     *                      auto selection.
     */
    @Parcelize
    data class MandatePreferences(
        val preferredCountryCode: String? = null
    ) : Parcelable {
        internal fun intoInternalPreferences() =
            com.truelayer.payments.ui.models.PaymentContext.Preferences(
                preferredCountryCode = preferredCountryCode
            )
    }
}

AndroidX Activity integration

This example shows how to process a payment with an AndroidX Activity integration:

import android.widget.Toast
import com.truelayer.payments.ui.screens.processor.ProcessorContext.PaymentContext
import com.truelayer.payments.ui.screens.processor.ProcessorActivityContract
import com.truelayer.payments.ui.screens.processor.ProcessorResult

// Register for the end result.
val contract = ProcessorActivityContract()
val flowResult = registerForActivityResult(contract) {
    val text = when (it) {
        is ProcessorResult.Failure -> {
            "Failure ${it.reason}"
        }
        is ProcessorResult.Successful -> {
            "Successful ${it.step}"
        }
    }
    // present the final result
    Toast.makeText(this, text, Toast.LENGTH_LONG).show()
}

// Obtain your payment context from your backend
val paymentContext = PaymentContext(
    id =  "your-payment-identifier",
    resourceToken = "payment-resource-token",
    redirectUri = "redirect-uri-that-will-be-invoked-when-coming-back-from-bank"
)

// 🚀 Launch the payment flow.
flowResult.launch(paymentContext)

Android Activity integration

This example shows how to process a payment with an Android Activity integration:

import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import com.truelayer.payments.core.domain.configuration.Environment
import com.truelayer.payments.ui.TrueLayerUI
import com.truelayer.payments.ui.screens.processor.ProcessorContext.PaymentContext
import com.truelayer.payments.ui.screens.processor.ProcessorActivityContract
import com.truelayer.payments.ui.screens.processor.ProcessorResult

class ActivityIntegration : Activity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // Initialise the payments configuration
        TrueLayerUI.init(context = applicationContext) {
            environment = Environment.PRODUCTION
            httpConnection = HttpConnectionConfiguration(
                httpDebugLoggingLevel = HttpLoggingLevel.None
            )
        }
        
        // Obtain your payment context from your backend
        val paymentContext = PaymentContext(
            id =  "your-payment-identifier",
            resourceToken = "payment-resource-token",
            redirectUri = "redirect-uri-that-will-be-invoked-when-coming-back-from-bank"
        )
        // Create an intent to launch the SDK
        val intent = ProcessorActivityContract().createIntent(this, paymentContext)
        // 🚀 Launch the SDK
        startActivityForResult(intent, 0)
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        // Act on the SDK result
        val text = when (val result = ProcessorResult.unwrapResult(data)) {
            is ProcessorResult.Failure -> "Failure ${result.reason}"
            is ProcessorResult.Successful -> "Successful ${result.step}"
            null -> "Activity result failed."
        }
        Toast.makeText(this, text, Toast.LENGTH_LONG).show()
    }
}

Jetpack Compose integration

This example shows how to process a payment with a Jetpack Compose integration:

// Your payments custom theme or use the provided defaults as below
val theme = TrueLayerTheme(
    lightPalette = LightColorDefaults,
    darkPalette = DarkColorDefaults,
    typography = TypographyDefaults
)

// Obtain your payment context from your backend
val paymentContext = PaymentContext(
    id =  "your-payment-identifier",
    resourceToken = "payment-resource-token",
    redirectUri = "redirect-uri-that-will-be-invoked-when-coming-back-from-bank"
)

setContent {
    Theme(
        theme = theme
    ) {
        Processor(
            context = paymentContext,
            onSuccess = { successStep ->
                // action on success
            },
            onFailure = { failureReason ->
                // action on failure
            },
        )
    }
}

Handle the success or error response

Whichever integration route you select, you will always receive a success or error response.

Error responses

A payment can fail for the following reasons:

Payment resultDescription
NoInternetThere was a problem connecting to the internet. This usually happens when the user has no internet connection. This can also be caused by a network configuration, if no route to TrueLayer services can be established.
UserAbortedThe user purposefully ended the payment process.
UserAbortedFailedToNotifyBackendThe user did not continue with the payment but the application failed to notify the backend, or the backend responded with an error.
CommunicationIssueCommunication with the backend has failed. It may be an invalid payment token, or just the payment service responding in an unexpected manner.
ConnectionSecurityIssueA secure connection to payment services could not be established.
PaymentFailedThe payment is in the failed state. A recovery from this state is not possible.
WaitAbandonedThe user purposefully abandoned the payment flow while the flow was awaiting authorisation from the bank. The authorisation from the bank may or may not depend on a user action, so the payment may still go through.
WaitTokenExpiredThe authorisation waiting time was so long that the resource token has expired. We are no longer able to query the payment status.
ProcessorContextNotAvailableA technical failure. It was not possible to read the ProcessorContext in which you set the payment/mandate id and resource_token. If you see this error, check that you're following the steps in this guide correctly, or raise a support ticket with us if you need further help.
UnknownThe SDK was unable to identify a reason for the failure.

Success responses

A success result means that the SDK has completed its role in initiating the payment. However, it doesn't necessarily mean that the payment itself is complete.

The table below shows all the possible responses:

ResponseDescription
RedirectThe user was redirected to their provider to authorise the payment.
WaitThe SDK flow is complete, but a decoupled authorisation action is still pending with the user and/or their provider.
AuthorizedThe user has authorised the payment with their provider.
SuccessfulThe bank has confirmed that they have accepted the payment request, and they intend to execute it.
SettledThe payment has reached its destination and the money is in the payee's account.

Get the payment or mandate status

As a best practice, we recommend that you obtain the payment status or mandate status from your backend through webhook notifications.

However, the SDK also enables you to fetch the current status of a payment or mandate.

val outcome = TrueLayerUI.getPaymentStatus("your-payment-identifier", "payment-resource-token")
val outcome = TrueLayerUI.getMandateStatus("your-mandate-identifier", "mandate-resource-token")

For more details on these statuses, see the API reference pages for payments and mandates.

Handle redirects from the provider

At the end of a redirect flow the bank app will relaunch your app with the return URI you provided in Console.

In your activity that launches when a deep link is triggered, fetch the redirect parameters with the method below.

val params = intent.data.extractTrueLayerRedirectParams()

This can include either the payment_id or mandate_id. For more information on handling deep links, follow the documentation here.

Display the payment result screen

The Android SDK provides a screen that you can use to display the result of a processed payment to your user. After you have fetched the redirect parameters in your activity, you can launch the payments result screen using one of the methods below.

An example of the payment result screen for a successful and failed payment.

An example of the payment result screen for a successful and failed payment.

Payment result screen with AndroidX Activity

Here is an example of how you can launch the payments result screen in an AndroidX Activity integration:

// the payment id is obtained from the redirect parameters
val params = intent.data.extractTrueLayerRedirectParams()
val paymentId = params["payment_id"]

val contract = ResultProcessorActivityContract()
val flowResult = registerForActivityResult(contract) {
    val text = when (it) {
        is ProcessorResult.Failure -> {
            "Failure ${it.reason}"
        }
        is ProcessorResult.Successful -> {
            "Successful ${it.step}"
        }
    }
    // present the final result
    Toast.makeText(this, text, Toast.LENGTH_LONG).show()
}

// Create a result context from the payment id
val resultContext = ResultProcessorContext(
    id = paymentId
)

// 🚀 Launch the payment flow.
flowResult.launch(resultContext)

Payment result screen with Android Activity

Here is an example of how you can launch the payments result screen in an Android Activity integration:

class ResultActivityIntegration : Activity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // the payment id is obtained from the redirect parameters
        val params = intent.data.extractTrueLayerRedirectParams()
        val paymentId = params["payment_id"]
        
        // Create a result context from the payment id
        val resultContext = ResultProcessorContext(
            id =  paymentId
        )
        
        // Create an intent to launch the SDK
        val intent = ResultProcessorActivityContract().createIntent(this, resultContext)
        // 🚀 Launch the SDK
        startActivityForResult(intent, 0)
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        // Act on the SDK result
        val text = when (val result = ProcessorResult.unwrapResult(data)) {
            is ProcessorResult.Failure -> "Failure ${result.reason}"
            is ProcessorResult.Successful -> "Successful ${result.step}"
            null -> "Activity result failed."
        }
        Toast.makeText(this, text, Toast.LENGTH_LONG).show()
    }
}

Payment result screen with Jetpack Compose

Here is an example of how you can launch the payments result screen in a Jetpack Compose integration:

// the payment id is obtained from the redirect parameters
val params = intent.data.extractTrueLayerRedirectParams()
val paymentId = params["payment_id"]

ResultProcessor(
    resultContext = ResultProcessorContext(paymentId),
    onSuccess = {
        // action on success
    },
    onFailure = {
        // action on dismiss
    }
)