From 4a01d6d6d38379fec26d40129256afe25779bda4 Mon Sep 17 00:00:00 2001
From: tl-lukasz-franieczek
<79842312+tl-lukasz-franieczek@users.noreply.github.com>
Date: Wed, 6 Nov 2024 14:20:59 +0000
Subject: [PATCH] PX-1826 3.8.0 release. (#17)
---
MigrateTo3.8.0.md | 644 ++++++++++++++++++
README.md | 3 +
app/build.gradle | 8 +-
app/src/main/AndroidManifest.xml | 10 +-
.../ActivityIntegrationActivity.kt | 43 +-
.../ActivityXIntegrationActivity.kt | 66 +-
.../ComposeIntegrationActivity.kt | 5 +-
.../integrations/JavaIntegrationActivity.java | 87 ++-
.../demo/payments/PaymentStatusActivity.kt | 64 +-
.../com/truelayer/demo/utils/PrefUtils.kt | 26 +
build.gradle | 20 +-
11 files changed, 882 insertions(+), 94 deletions(-)
create mode 100644 MigrateTo3.8.0.md
diff --git a/MigrateTo3.8.0.md b/MigrateTo3.8.0.md
new file mode 100644
index 0000000..87d779f
--- /dev/null
+++ b/MigrateTo3.8.0.md
@@ -0,0 +1,644 @@
+# Version 3.8.0 Changes
+
+## Table of contents
+
+
+* [Version 3.8.0 Changes](#version-380-changes)
+ * [Table of contents](#table-of-contents)
+ * [New Features](#new-features)
+ * [Underlying library updates](#underlying-library-updates)
+ * [Known Issues](#known-issues)
+ * [Breaking Changes](#breaking-changes)
+ * [The updated payment flow diagram](#the-updated-payment-flow-diagram)
+ * [Integration steps - the diagram explained](#integration-steps---the-diagram-explained)
+ * [ReactNative integration problems](#reactnative-integration-problems)
+* [Processing payment step by step](#processing-payment-step-by-step)
+ * [Step 1: Install the SDK](#step-1-install-the-sdk)
+ * [Core Library Desugaring](#core-library-desugaring)
+ * [Step 2: Initialise the SDK](#step-2-initialise-the-sdk)
+ * [If you are using AppStartup](#if-you-are-using-appstartup)
+ * [Step 3: Process a payment](#step-3-process-a-payment)
+ * [Option 1: Process a payment with an AndroidX Activity integration](#option-1-process-a-payment-with-an-androidx-activity-integration)
+ * [Option 2: Process a payment with an Android Activity integration](#option-2-process-a-payment-with-an-android-activity-integration)
+ * [Option 3: Process a payment with a Jetpack Compose integration](#option-3-process-a-payment-with-a-jetpack-compose-integration)
+ * [Step 4: Handle redirects from the bank](#step-4-handle-redirects-from-the-bank)
+ * [Step 5: Displaying the payment result](#step-5-displaying-the-payment-result)
+ * [Step 6: Handle final result](#step-6-handle-final-result)
+* [Processor Results - Success or Error](#processor-results---success-or-error)
+ * [Error](#error)
+ * [Success](#success)
+
+
+## New Features
+
+The SDK is no longer sending `success` message with reason `redirect`. This is to make the SDK result
+more useful for the client app. The `succeess/redirect` message was most of the time just ignored.
+
+Another, most important reason, is the ability for the user to retry the redirect to the bank app.
+This is useful in, not so uncommon, case when the bank app will not produce correct screen. The user
+now will have possibility to get back to merchant app and click the `go to bank` button again.
+
+This change have a chance of reducing the number of abandoned/timed-out payments.
+
+Now the SDK will refresh payment state at any point when user will switch back to the merchant app.
+This will also be helpful in the situation when the redirect back from the bank will fail for any reason.
+When user will get back to the app, the SDK will fetch the latest payment status and will act
+accordingly.
+
+## Underlying library updates
+
+- `kotlin` : `1.9.25`
+- `gradle-version` : `8.7.0`
+- `com.android.tools.build:gradle` : `8.7.0`
+- `compose-bom` : `2024.09.02`
+- `org.jetbrains.kotlinx.kotlinx-coroutines-core` : `1.9.0`
+- `androidx.fragment.fragment-ktx` : `1.8.4`
+- `androidx.navigation.navigation-compose` : `2.8.2`
+- `androidx.startup.startup-runtime` : `1.2.0`
+- `androidx.lifecycle.lifecycle-runtime-compose-android` : `2.8.6`
+
+## Known Issues
+
+With the `compose-bom` : `2024.09.03` there is a change of `focusable` attribute in the `TextField` and `OutlinedTextField`.
+This is causing lack of automatic hiding the keyboard when the user clicks `done` IME button on the keyboard. The keyboard
+can still be hidden by click hide keyboard button on the keyboard itself.
+This is only affecting some EUR payments.
+
+
+## Breaking Changes
+
+The SDK will no longer send `success` message with reason `redirect`.
+The implication is that the SDK will not be dismissed after the redirect to the bank app.
+The SDK screen has to be brought back to the front when redirect back from the bank app.
+
+The SDK will now only return with the one of the final statuses for the payment:
+`failed`, `cancelled`, `executed`, `settled`, `authorized`
+
+The following change will have to be made in the client app:
+
+> :warning:
+> For the non-Compose integrations Activity that starts the SDK should be `android:launchMode="standard"`.
+> This is to make sure that the SDK Activity is not recreated when the redirect back
+from the bank app happens. This is important so that the result screen is not shown multiple times.
+>
+> Please take a look at the demo code how to handle the redirect back from bank.
+
+ ## The updated payment flow diagram
+
+```mermaid
+sequenceDiagram
+ autonumber
+ participant a as Client
+ participant b as SDK
+ participant c as PaymentService
+ participant bank as Bank
+
+ a->>a: Obtain and store paymentContext
+ a->>b: Processor(paymentContext)
+ activate b
+ loop until the final payment state or redirect to bank
+ b->>c: execute payment step
+ c->>b: notify next step
+ end
+ alt redirect
+
+ b-->> bank: Start the Bank App or website
+ activate bank
+
+ bank->>bank: Authorizing payment
+ alt direct return supported
+ bank-->>a: Redirect URL
+ else direct return unavailable
+ bank-->>c: Redirect URL
+ c-->>a: Redirect URL (eg. truelayer://demo?payment_id=f57cfac6-d3b9)
+ end
+ deactivate bank
+
+ a-->>b: Move the SDK to front
+ note over b: The SDK will continue the necessary work,
either jumping to step 3 to repeat the sequence
or moving to step 11, depending on the payment status
+
+ else final payment state
+
+ opt shouldPresentResultScreen
+ b->>b: Present the result screen
+ end
+ b->>a: Result - success or failure
+
+ end
+ deactivate b
+```
+
+### Integration steps - the diagram explained
+
+1. (1)The Merchant app needs to create a payment.
+2. (2)Once the payment is created, The Merchant app needs to launch the SDK with the paymentContext.
+3. (3,4)The SDK will execute appropriate steps.
+
+Now there are 2 possibilities for the flow to continue:
+- (5, 6)For UK and some EU banks: the SDK will redirect user to their bank and notify result of (`success`, `redirect`)
+- (11, 12)For some EU banks: SDK will just complete entire payment journey. In the end it will return result to the Merchant app.
+
+With the possibility #1 the following steps will happen.
+4. (8 or 9,10)The Bank will redirect back to the Merchant App.
+5. (2) The Merchant app needs to invoke the SDK again.
+6. (11, 12) The SDK will optionally present the result screen and then return result to the Merchant App.
+
+At the redirect step there is no need for the Merchant App to check the status of the payment. The SDK will handle
+all relevant statuses and act accordingly.
+Starting the SDK only when the payment is in the `AuthorizationRequired` status is not sufficient as the status `Authorizing`
+also requires SDK to be launched.
+
+### ReactNative integration problems
+
+We have observed that there is a problem with React Native library that handles redirects. Quite often
+developers registering the event listener multiple times, without realizing it. Then the redirect event
+is triggered multiple times (once per each listener).
+
+To prevent SDK being launched multiple times we propose the following solution:
+
+```typescript jsx
+
+ // Make sure you will subscribe to the event listener only once
+ // by checking the listenerCount.
+ if (Linking.listenerCount("url") == 0) {
+ Linking.addEventListener("url", (event) => {
+ console.log("eventListener url: " + event.url)
+ handleRedirect(event.url)
+ })
+ }
+
+```
+
+# Processing payment step by step
+
+## Step 1: Install the SDK
+Before you add the SDK to our project, make sure that your `build.gradle` configuration is configured to support `minSdk` 24 (Android 7.0) or higher as well the `packagingOptions` below:
+
+### Core Library Desugaring
+In order to be able to run on API level below 26 the SDK requires your application to have core library desugaring enabled. Without this the SDK will crash.
+
+```groovy
+android {
+ defaultConfig {
+ applicationId "com.example.myapp"
+ minSdk 24 // Sdk 24 or higher supported
+ }
+
+ compileOptions {
+ coreLibraryDesugaringEnabled true
+ }
+
+ packagingOptions {
+ resources {
+ pickFirsts += ['META-INF/LICENSE-MIT']
+ }
+ }
+}
+```
+
+To add the SDK to your project, simply include TrueLayer Payments SDK to your dependencies.
+
+```groovy
+coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:2.1.2"
+// Add to your projects `build.gradle`.
+implementation "com.truelayer.payments:ui:3.8.0"
+```
+
+## Step 2: Initialise the SDK
+
+To use the SDK, you have to first initialise it before invoking any other SDK method. The following code sample is an example of initialising the SDK:
+
+```kotlin
+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(
+ timeoutMs = 45000,
+ httpDebugLoggingLevel = HttpLoggingLevel.None
+ )
+}
+```
+
+### If you are using AppStartup
+
+If you are using [AppStartup](https://developer.android.com/topic/libraries/app-startup) you will need to add WorkManagerInitializer to list of your dependencies:
+```kotlin
+import androidx.work.WorkManagerInitializer
+(...)
+override fun dependencies(): List>> {
+ return listOf(WorkManagerInitializer::class.java)
+}
+```
+
+## Step 3: Process a payment
+
+Depending on your preferred approach, you can choose to integrate the SDK into your payment process with either **AndroidX Activity**, with the old-fashioned **Android Activity** or with **Jetpack Compose**.
+
+In both cases, you'll need the `id` and the `resourceToken` obtained from the backend. The fields mentioned earlier will be passed to the SDK via `PaymentContext` or `MandateContext`. There is little more to know about this object. Here is the `kdoc` that explains all available parameters
+
+```kotlin
+/**
+ * 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 shouldPresentResultScreen (optional) true if the result screen should be presented
+ * before the final redirect to the merchant app. Default is true.
+ * @property waitTimeMillis the total time the result screen will wait to get a final status of the payment
+ * Default is 3 seconds. Minimum is 2 seconds. Maximum is 10 seconds.
+ */
+ @Parcelize
+ data class PaymentPreferences(
+ val preferredCountryCode: String? = null,
+ val shouldPresentResultScreen: Boolean = true,
+ val waitTimeMillis: Long = 3_000
+ ) : Parcelable {
+ internal fun intoInternalPreferences() =
+ com.truelayer.payments.ui.models.PaymentContext.Preferences(
+ preferredCountryCode = preferredCountryCode,
+ shouldPresentResultScreen = shouldPresentResultScreen
+ )
+ }
+
+ /**
+ * 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.
+ * @property shouldPresentResultScreen (optional) true if the result screen should be presented
+ * before the final redirect to the merchant app. Default is true.
+ * @property waitTimeMillis the total time the result screen will wait to get a final status of the payment
+ * Default is 3 seconds. Minimum is 2 seconds. Maximum is 10 seconds.
+ */
+ @Parcelize
+ data class MandatePreferences(
+ val preferredCountryCode: String? = null,
+ val shouldPresentResultScreen: Boolean = true,
+ val waitTimeMillis: Long = 3_000
+ ) : Parcelable {
+ internal fun intoInternalPreferences() =
+ com.truelayer.payments.ui.models.PaymentContext.Preferences(
+ preferredCountryCode = preferredCountryCode,
+ shouldPresentResultScreen = shouldPresentResultScreen
+ )
+ }
+}
+```
+
+### Option 1: Process a payment with an AndroidX Activity integration
+
+```kotlin
+import android.content.Intent
+import android.os.Bundle
+import android.widget.Toast
+import androidx.activity.result.ActivityResultLauncher
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.util.Consumer
+import com.truelayer.payments.core.domain.configuration.HttpConnectionConfiguration
+import com.truelayer.payments.core.domain.configuration.HttpLoggingLevel
+import com.truelayer.payments.core.domain.utils.Fail
+import com.truelayer.payments.core.domain.utils.Ok
+import com.truelayer.payments.core.utils.extractTrueLayerRedirectParams
+import com.truelayer.payments.ui.TrueLayerUI
+import com.truelayer.payments.ui.screens.processor.ProcessorActivityContract
+import com.truelayer.payments.ui.screens.processor.ProcessorContext
+import com.truelayer.payments.ui.screens.processor.ProcessorResult
+
+class ActivityXIntegrationActivity : AppCompatActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ initializeSdk()
+
+ val flow = registerFlow()
+ // here try to handle the redirect from bank intent
+ // if this activity is just started not as a redirect result
+ // then the following function should do nothing
+ tryHandleIntentWithRedirectFromBankData(intent, flow)
+
+ startPaymentButton.setOnClickListener {
+ startPayment(flow)
+ }
+ }
+
+ fun initializeSdk() {
+ // 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(
+ timeoutMs = 30_000,
+ httpDebugLoggingLevel = HttpLoggingLevel.None
+ )
+ }
+ }
+
+ /**
+ * This function will only execute flow if the start intent
+ * contains correct payment or mandate ID and if the corresponding
+ * context for the payment or mandate is available
+ */
+ fun tryHandleIntentWithRedirectFromBankData(
+ intent: Intent,
+ flow: ActivityResultLauncher
+ ) {
+ val params = intent.data.extractTrueLayerRedirectParams()
+ // restore previously stored payment context
+ // that matches the ID from the redirect url
+ val storedProcessorContext = LocalStore.getStoredPaymentContext()
+ if (params.isNotEmpty() && storedProcessorContext != null &&
+ (storedProcessorContext.id == params["payment_id"] || storedProcessorContext.id == params["mandate_id"])
+ ) {
+ // The user is returning from the provider app
+ // and the payment/mandate ID matches the one we have stored
+ // so we can fetch the payment status
+ flow.launch(storedProcessorContext)
+ }
+ }
+
+ fun registerFlow(): ActivityResultLauncher {
+ // Register for the end result.
+ val contract = ProcessorActivityContract()
+ return registerForActivityResult(contract) {
+ val text = when (it) {
+ is ProcessorResult.Failure -> {
+ "Failure ${it.reason}, ${it.resultShown}"
+ }
+ is ProcessorResult.Successful -> {
+ "Successful ${it.step}, ${it.resultShown}"
+ }
+ }
+ // present the final result
+ Toast.makeText(this, text, Toast.LENGTH_LONG).show()
+ }
+ }
+
+ fun startPayment(flow: ActivityResultLauncher) {
+ // 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"
+ )
+ // store the payment context as it will be needed when you will be handling redirect back from bank
+ LocalStore.storePaymentContext(paymentContext)
+ // 🚀 Launch the payment flow.
+ processorResult.launch(paymentContext)
+ }
+}
+```
+
+### Option 2: Process a payment with an Android Activity integration
+
+```kotlin
+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)
+
+ initializeSdk()
+
+ tryHandleIntentWithRedirectFromBankData(intent)
+
+ startPaymentButton.setOnClickListener {
+ startPayment()
+ }
+ }
+
+ fun initializeSdk() {
+ // 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(
+ timeoutMs = 30_000,
+ httpDebugLoggingLevel = HttpLoggingLevel.None
+ )
+ }
+ }
+
+ /**
+ * This function will only execute flow if the start intent
+ * contains correct payment or mandate ID and if the corresponding
+ * context for the payment or mandate is available
+ */
+ fun tryHandleIntentWithRedirectFromBankData(
+ intent: Intent
+ ) {
+ val params = intent.data.extractTrueLayerRedirectParams()
+ // restore previously stored payment context
+ // that matches the ID from the redirect url
+ val storedProcessorContext = LocalStore.getStoredPaymentContext()
+ if (params.isNotEmpty() && storedProcessorContext != null &&
+ (storedProcessorContext.id == params["payment_id"] || storedProcessorContext.id == params["mandate_id"])
+ ) {
+ // The user is returning from the provider app
+ // and the payment/mandate ID matches the one we have stored
+ // so we can fetch the payment status
+ startPaymentProcessorActivity(storedProcessorContext)
+ }
+ }
+
+ private fun startPaymentProcessorActivity(processorContext: ProcessorContext) {
+ // Create an intent with the payment context to start the payment flow
+ val intent = ProcessorActivityContract().createIntent(this, processorContext)
+ // Start activity for result to receive the results of the payment flow
+ startActivityForResult(intent, 0)
+ }
+
+ fun startPayment() {
+ // 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"
+ )
+ // store the payment context as it will be needed when you will be handling redirect back from bank
+ LocalStore.storePaymentContext(paymentContext)
+ // 🚀 Launch the payment flow.
+ startPaymentProcessorActivity(paymentContext)
+ }
+
+ 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()
+ }
+}
+```
+
+### Option 3: Process a payment with a Jetpack Compose integration
+
+The following example shows how to process a payment with a Jetpack Compose integration:
+
+```kotlin
+// 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
+ },
+ )
+ }
+}
+```
+## Step 4: Handle redirects from the bank
+
+There are banks that do require more than one redirect to complete the payment.
+
+Once the redirect will reach your application you need to relaunch the SDK with the same `PaymentContext` or `MandateContext` that the corresponding payment was started with. The SDK will inspect the state of the payment and will act accordingly.
+
+More information on how to do it can be found in section [Handle redirects](#handle-redirects-from-the-bank).
+
+## Step 5: Displaying the payment result
+The SDK provides a screen that can be used to display the result of a processed payment to the user.
+If you don't want to use that screen you need to opt-out by setting a `shouldPresentResultScreen = false` on the `PaymentPreferences` or `MandatePreferences`.
+
+## Step 6: Handle final result
+
+Handle the [processor result](#processor-results---success-or-error) returned by the SDK in your app.
+
+# Processor Results - Success or Error
+
+Regardless of the integration route you are going to select in the end you will be presented with a result. It can be `success` or `error`.
+
+## Error
+In case of error you will have a failure reason. It will be one of the following:
+
+- `NoInternet` There was a problem connecting to the internet. Most likely when the user is not connected to the internet. It can also be caused by a network configuration, if no route to TrueLayer services can be established.
+- `UserAborted` When the user purposefully aborted the payment process.
+- `UserAbortedProviderTemporarilyUnavailable` When user aborted payment process because preselected provider was temporarily unavailable.
+- `UserAbortedFailedToNotifyBackend` User aborted but the application failed to notify the backend, or the backend responded with an error. In rare cases user may try to cancel the payment process after the payment has been authorized/completed. Please check the status of the payment at the backend when you see this error.
+- `UserAbortedProviderTemporarilyUnavailableFailedToNotifyBackend` When user aborted payment process because preselected provider was temporarily unavailable but application failed to notify backend or backend responded with an error.
+- `CommunicationIssue` Communication issue. When something goes wrong with the communication with the backend. It may be an invalid payment token, or just the payment service responding in unexpected manner.
+- `ConnectionSecurityIssue` This will be returned when secure a connection to payment services could not be established.
+- `PaymentFailed` When the payment is in failed state. A recovery from this state is not possible.
+- `WaitAbandoned` The user purposefully abandoned the payment flow at the point when the flow was awaiting authorization from the bank. The authorization from the bank may or may not depend on a user action, and the payment may still complete.
+- `WaitTokenExpired` The authorization waiting time was so long that the resource token has expired. We are no longer able to query the payment status.
+- `ProcessorContextNotAvailable` A technical failure. It was not possible to read the `ProcessorContext` in which you've 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.
+- `InvalidResource` When an unsupported resource is used to launch the result screen. This is in case the SDK would be used to process payment of a preauthorized mandate, which should be happening as a backend to backend process.
+- `Unknown` The SDK was unable to identify a reason for the failure.
+
+## Success
+
+When a success result is presented, it means that the SDK has completed its role in initiating the payment. However, it doesn't necessarily mean that the payment itself is complete. To help understand the context in which the SDK completes its task, it also provides the step at which the flow completed:
+
+- **(Removed in 3.8.0)** `Redirect` When the user was redirected to their bank to authorize the payment.
+- `Wait` When the SDK flow is complete, but a decoupled authorisation action is still pending with the user and/or the bank.
+- `Authorized` When the user authorized the payment with the bank.
+- `Successful` When the bank has confirmed that they have accepted the payment request, and they intend to execute it.
+- `Settled` When the funds have reached the destination.
+
+Before acting on the result of the SDK completing its process, it's important to understand [the status of the payment](https://docs.truelayer.com/docs/payment-statuses-for-payments-v3) or [mandate](https://docs.truelayer.com/docs/mandate-statuses).
diff --git a/README.md b/README.md
index 29bab5f..a1e6126 100644
--- a/README.md
+++ b/README.md
@@ -27,6 +27,9 @@ your environment from the dropdown.
You can also select between launching flows for payments in different currencies or mandates.
+
+## :warning: **For SDK version 3.8.0+ go [here](./MigrateTo3.8.0.md)** :warning:
+
## How does the payment flow with the SDK works?
```mermaid
diff --git a/app/build.gradle b/app/build.gradle
index cf1768b..0f077bb 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -31,11 +31,11 @@ android {
}
compileOptions {
coreLibraryDesugaringEnabled true
- sourceCompatibility JavaVersion.VERSION_17
- targetCompatibility JavaVersion.VERSION_17
+ sourceCompatibility JavaVersion.VERSION_19
+ targetCompatibility JavaVersion.VERSION_19
}
kotlinOptions {
- jvmTarget = '17'
+ jvmTarget = '19'
}
buildFeatures {
compose true
@@ -72,5 +72,5 @@ dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:$json_serialization_version"
implementation "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:$retrofit2_kotlinx_serialization"
- implementation "com.truelayer.payments:ui:3.2.1"
+ implementation "com.truelayer.payments:ui:3.8.0"
}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 2391b6e..fbdb61e 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -2,6 +2,8 @@
+
+
+ tools:targetApi="34">
+ android:exported="true"
+ android:label="@string/integration_activityx">
+
{
- // Create an intent with the payment context to start the payment flow
- val intent = ProcessorActivityContract().createIntent(
- this@ActivityIntegrationActivity,
- processorContext.value
- )
- // Start activity for result to receive the results of the payment flow
- startActivityForResult(intent, 0)
+ PrefUtils.setIntegrationType(PrefUtils.IntegrationType.ACTIVITY, this@ActivityIntegrationActivity)
+ startPaymentActivity(processorContext.value)
}
is Fail -> withContext(Dispatchers.Main) {
// Display error if payment context creation failed
diff --git a/app/src/main/java/com/truelayer/demo/integrations/ActivityXIntegrationActivity.kt b/app/src/main/java/com/truelayer/demo/integrations/ActivityXIntegrationActivity.kt
index e97c5ac..1b755ce 100644
--- a/app/src/main/java/com/truelayer/demo/integrations/ActivityXIntegrationActivity.kt
+++ b/app/src/main/java/com/truelayer/demo/integrations/ActivityXIntegrationActivity.kt
@@ -1,9 +1,12 @@
package com.truelayer.demo.integrations
+import android.content.Intent
import android.os.Bundle
+import android.util.Log
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.appcompat.app.AppCompatActivity
+import androidx.core.util.Consumer
import com.truelayer.demo.R
import com.truelayer.demo.databinding.ActivityIntegrationBinding
import com.truelayer.demo.payments.ProcessorContextProvider
@@ -12,6 +15,7 @@ import com.truelayer.payments.core.domain.configuration.HttpConnectionConfigurat
import com.truelayer.payments.core.domain.configuration.HttpLoggingLevel
import com.truelayer.payments.core.domain.utils.Fail
import com.truelayer.payments.core.domain.utils.Ok
+import com.truelayer.payments.core.utils.extractTrueLayerRedirectParams
import com.truelayer.payments.ui.TrueLayerUI
import com.truelayer.payments.ui.screens.processor.ProcessorActivityContract
import com.truelayer.payments.ui.screens.processor.ProcessorContext
@@ -26,12 +30,11 @@ import kotlinx.coroutines.withContext
class ActivityXIntegrationActivity : AppCompatActivity() {
private val scope = CoroutineScope(Dispatchers.IO)
- private lateinit var processorContextProvider: ProcessorContextProvider
+ private var newIntentConsumer: Consumer? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
-
- processorContextProvider = ProcessorContextProvider(PrefUtils.getQuickstartUrl(this))
+ Log.e("ActivityXIntegrationActivity", "onCreate: $intent")
val binding = ActivityIntegrationBinding.inflate(layoutInflater)
setContentView(binding.root)
@@ -44,26 +47,65 @@ class ActivityXIntegrationActivity : AppCompatActivity() {
)
}
- // Create a contract to receive the results
- val contract = ProcessorActivityContract()
-
- // Handle the result returned from the SDK at the end of the payment flow
- val flow = registerForActivityResult(contract) {
- Toast.makeText(this, it.toString(), Toast.LENGTH_LONG).show()
+ newIntentConsumer = Consumer { intent ->
+ // extract payment id
+ Log.e("ActivityXIntegrationActivity", "newIntentConsumer: $intent")
+ val flow = registerFlow()
+ tryHandleIntentWithRedirectFromBankData(intent, flow)
}
+ newIntentConsumer?.let {
+ addOnNewIntentListener(it)
+ }
+
+ val flow = registerFlow()
+
+ tryHandleIntentWithRedirectFromBankData(intent, flow)
binding.launchButton.setOnClickListener {
scope.launch {
- launchFlow(flow)
+ startNewPayment(flow)
}
}
}
- private suspend fun launchFlow(flow: ActivityResultLauncher) {
+ override fun onDestroy() {
+ super.onDestroy()
+ Log.e("ActivityXIntegrationActivity", "onDestroy")
+ newIntentConsumer?.let { removeOnNewIntentListener(it) }
+ }
+
+ private fun tryHandleIntentWithRedirectFromBankData(intent: Intent, flow: ActivityResultLauncher) {
+ val params = intent.data.extractTrueLayerRedirectParams()
+ val storedProcessorContext = PrefUtils.getProcessorContext(this)
+ if (params.isNotEmpty() && storedProcessorContext != null &&
+ (storedProcessorContext.id == params["payment_id"] || storedProcessorContext.id == params["mandate_id"])) {
+ // The user is returning from the provider app
+ // and the payment/mandate ID matches the one we have stored
+ // so we can fetch the payment status
+ flow.launch(storedProcessorContext)
+ }
+ }
+
+ private fun registerFlow(): ActivityResultLauncher {
+ // Create a contract to receive the results
+ val contract = ProcessorActivityContract()
+ // Handle the result returned from the SDK at the end of the payment flow
+ return registerForActivityResult(contract) {
+ Toast.makeText(this, it.toString(), Toast.LENGTH_LONG).show()
+ Log.e("ActivityXIntegrationActivity", it.toString())
+ }
+ }
+
+ private suspend fun startNewPayment(flow: ActivityResultLauncher) {
val paymentType = PrefUtils.getPaymentType(this)
+
+ val processorContextProvider = ProcessorContextProvider(PrefUtils.getQuickstartUrl(this))
+ val processorContext = processorContextProvider.getProcessorContext(paymentType, this)
+
// Create a payment context
- when (val processorContext = processorContextProvider.getProcessorContext(paymentType, this)) {
+ when (processorContext) {
is Ok -> {
+ PrefUtils.setIntegrationType(PrefUtils.IntegrationType.ACTIVITY_X, this@ActivityXIntegrationActivity)
// Start the payment flow
flow.launch(processorContext.value)
}
diff --git a/app/src/main/java/com/truelayer/demo/integrations/ComposeIntegrationActivity.kt b/app/src/main/java/com/truelayer/demo/integrations/ComposeIntegrationActivity.kt
index c4176c1..3f9ecda 100644
--- a/app/src/main/java/com/truelayer/demo/integrations/ComposeIntegrationActivity.kt
+++ b/app/src/main/java/com/truelayer/demo/integrations/ComposeIntegrationActivity.kt
@@ -77,7 +77,10 @@ class ComposeIntegrationActivity : AppCompatActivity() {
var error by remember { mutableStateOf(null) }
LaunchedEffect(true) {
processorContextProvider.getProcessorContext(paymentType, this@ComposeIntegrationActivity)
- .onOk { processorContext = it }
+ .onOk {
+ processorContext = it
+ PrefUtils.setIntegrationType(PrefUtils.IntegrationType.COMPOSE, this@ComposeIntegrationActivity)
+ }
.onError { error = it.localizedMessage }
}
Theme(
diff --git a/app/src/main/java/com/truelayer/demo/integrations/JavaIntegrationActivity.java b/app/src/main/java/com/truelayer/demo/integrations/JavaIntegrationActivity.java
index 3d052d4..55385a8 100644
--- a/app/src/main/java/com/truelayer/demo/integrations/JavaIntegrationActivity.java
+++ b/app/src/main/java/com/truelayer/demo/integrations/JavaIntegrationActivity.java
@@ -3,7 +3,10 @@
import androidx.activity.result.ActivityResultCallback;
import androidx.activity.result.ActivityResultLauncher;
import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.util.Consumer;
+import android.content.Intent;
+import android.net.Uri;
import android.os.Bundle;
import android.widget.Toast;
@@ -16,28 +19,52 @@
import com.truelayer.payments.core.domain.configuration.HttpLoggingLevel;
import com.truelayer.payments.core.domain.utils.Fail;
import com.truelayer.payments.core.domain.utils.Ok;
+import com.truelayer.payments.core.utils.ExtensionsKt;
import com.truelayer.payments.ui.TrueLayerUI;
import com.truelayer.payments.ui.screens.processor.ProcessorActivityContract;
import com.truelayer.payments.ui.screens.processor.ProcessorContext;
import com.truelayer.payments.ui.screens.processor.ProcessorResult;
+import java.util.Dictionary;
+import java.util.Map;
+
/**
* Example integration of the SDK with Java and the AndroidX Activity
*/
public class JavaIntegrationActivity extends AppCompatActivity {
- private ProcessorContextProvider processorContextProvider;
+ private Consumer newIntentConsumer = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_integration);
- processorContextProvider = new ProcessorContextProvider(PrefUtils.getQuickstartUrl(this));
-
ActivityIntegrationBinding binding = ActivityIntegrationBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
+ // Initialise the payments configuration
+ initPaymentsSdk();
+
+ newIntentConsumer = new Consumer() {
+ @Override
+ public void accept(Intent intent) {
+ ActivityResultLauncher flow = registerFlow();
+ tryHandleIntentWithRedirectFromBankData(intent, flow);
+ }
+ };
+ addOnNewIntentListener(newIntentConsumer);
+
+ ActivityResultLauncher flow = registerFlow();
+
+ tryHandleIntentWithRedirectFromBankData(getIntent(), flow);
+
+ binding.launchButton.setOnClickListener(v ->
+ launchFlow(flow)
+ );
+ }
+
+ private void initPaymentsSdk() {
// Initialise the payments configuration
TrueLayerUI.Builder builder = new TrueLayerUI.Builder()
.environment(PrefUtils.getEnvironment(this))
@@ -47,21 +74,71 @@ protected void onCreate(Bundle savedInstanceState) {
));
TrueLayerUI.init(getApplicationContext(), builder);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+
+ if (newIntentConsumer != null) {
+ removeOnNewIntentListener(newIntentConsumer);
+ }
+ }
+ private ActivityResultLauncher registerFlow() {
// Create a contract to receive the results
ProcessorActivityContract contract = new ProcessorActivityContract();
// Handle the result returned from the SDK at the end of the payment flow
- ActivityResultLauncher flow = registerForActivityResult(contract,
+ return registerForActivityResult(contract,
(ActivityResultCallback< ProcessorResult>) result ->
Toast.makeText(this, result.toString(), Toast.LENGTH_LONG).show()
);
+ }
+
+ private void tryHandleIntentWithRedirectFromBankData(Intent intent, ActivityResultLauncher flow) {
+ Uri data = intent.getData();
+ Map params = ExtensionsKt.extractTrueLayerRedirectParams(data);
+ ProcessorContext storedProcessorContext = PrefUtils.getProcessorContext(this);
+
+ if (!params.isEmpty() && storedProcessorContext != null &&
+ (storedProcessorContext.getId().equals(params.get("payment_id")) || storedProcessorContext.getId().equals(params.get("mandate_id")))) {
+ // The user is returning from the provider app
+ // and the payment/mandate ID matches the one we have stored
+ // so we can fetch the payment status
+ flow.launch(storedProcessorContext);
+ }
+ }
- binding.launchButton.setOnClickListener(v -> launchFlow(flow));
+ private void startPaymentProcessor(ActivityResultLauncher flow, ProcessorContext.PaymentContext paymentContext) {
+ // Start the payment processor
+
+ }
+
+ private void startNewPayment(ActivityResultLauncher flow) {
+ PaymentType paymentType = PrefUtils.getPaymentType(this);
+ ProcessorContextProvider processorContextProvider = new ProcessorContextProvider(PrefUtils.getQuickstartUrl(this));
+ // Create a payment context
+ processorContextProvider.getProcessorContext(paymentType, this, outcome -> {
+ if(outcome instanceof Ok) {
+ // Start the payment flow
+ flow.launch(((Ok) outcome).getValue());
+ }
+ else if(outcome instanceof Fail) {
+ // Display error if payment context creation failed
+ Toast.makeText(
+ this,
+ getString(R.string.processor_context_error, ((Fail>) outcome).getError()),
+ Toast.LENGTH_LONG
+ ).show();
+ }
+ return null;
+ });
}
private void launchFlow(ActivityResultLauncher flow) {
PaymentType paymentType = PrefUtils.getPaymentType(this);
+ ProcessorContextProvider processorContextProvider = new ProcessorContextProvider(PrefUtils.getQuickstartUrl(this));
// Create a payment context
processorContextProvider.getProcessorContext(paymentType, this, outcome -> {
if(outcome instanceof Ok) {
diff --git a/app/src/main/java/com/truelayer/demo/payments/PaymentStatusActivity.kt b/app/src/main/java/com/truelayer/demo/payments/PaymentStatusActivity.kt
index b717dda..c36e30f 100644
--- a/app/src/main/java/com/truelayer/demo/payments/PaymentStatusActivity.kt
+++ b/app/src/main/java/com/truelayer/demo/payments/PaymentStatusActivity.kt
@@ -1,17 +1,14 @@
package com.truelayer.demo.payments
+import android.content.Intent
import android.os.Bundle
-import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
-import androidx.compose.material3.AlertDialog
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
-import androidx.compose.material3.TextButton
+import com.truelayer.demo.MainActivity
+import com.truelayer.demo.integrations.ActivityIntegrationActivity
+import com.truelayer.demo.integrations.ActivityXIntegrationActivity
+import com.truelayer.demo.integrations.ComposeIntegrationActivity
+import com.truelayer.demo.integrations.JavaIntegrationActivity
import com.truelayer.demo.utils.PrefUtils
-import com.truelayer.payments.core.utils.extractTrueLayerRedirectParams
-import com.truelayer.payments.ui.screens.processor.Processor
-import com.truelayer.payments.ui.theme.Theme
-import com.truelayer.payments.ui.theme.stackNavigation
/**
* Activity to receive redirects from the provider app and fetches the payment's status
@@ -21,45 +18,16 @@ class PaymentStatusActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- // Extract the payment/mandate parameters from the URL
- val params = intent.data.extractTrueLayerRedirectParams()
- val processorContext = PrefUtils.getProcessorContext(this)
-
- setContent {
- val resourceId = params["payment_id"] ?: params["mandate_id"]
-
- val activity = this
-
- Theme(
- navigationTransition = { current, transition, direction ->
- stackNavigation(current, transition, direction)
- }
- ) {
- if(resourceId != null && processorContext != null && processorContext.id == resourceId) {
- // Display the payment result screen or handle any subsequent actions
- Processor(
- context = processorContext,
- onSuccess = {
- activity.finish()
- },
- onFailure = {
- activity.finish()
- }
- )
- }
- else {
- AlertDialog(
- title = { Text(text = "Whoops", style = MaterialTheme.typography.titleMedium) },
- text = { Text(text = "Error getting payment result") },
- confirmButton = {
- TextButton(onClick = { activity.finish() }) {
- Text("Close")
- }
- },
- onDismissRequest = { activity.finish() }
- )
- }
- }
+ val activityClass = when (PrefUtils.getIntegrationType(this)) {
+ PrefUtils.IntegrationType.ACTIVITY -> ActivityIntegrationActivity::class.java
+ PrefUtils.IntegrationType.ACTIVITY_X -> ActivityXIntegrationActivity::class.java
+ PrefUtils.IntegrationType.COMPOSE -> ComposeIntegrationActivity::class.java
+ PrefUtils.IntegrationType.JAVA -> JavaIntegrationActivity::class.java
}
+
+ val newIntent = Intent(this, activityClass)
+ newIntent.data = intent.data
+ startActivity(newIntent)
+ finish()
}
}
diff --git a/app/src/main/java/com/truelayer/demo/utils/PrefUtils.kt b/app/src/main/java/com/truelayer/demo/utils/PrefUtils.kt
index ff34460..f3dfa54 100644
--- a/app/src/main/java/com/truelayer/demo/utils/PrefUtils.kt
+++ b/app/src/main/java/com/truelayer/demo/utils/PrefUtils.kt
@@ -4,6 +4,7 @@ import android.content.Context
import com.truelayer.demo.payments.PaymentType
import com.truelayer.payments.core.domain.configuration.Environment
import com.truelayer.payments.ui.screens.processor.ProcessorContext
+import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.lang.IllegalArgumentException
@@ -88,4 +89,29 @@ object PrefUtils {
Json.Default.decodeFromString(it)
}
}
+
+ @Serializable
+ enum class IntegrationType {
+ ACTIVITY,
+ ACTIVITY_X,
+ COMPOSE,
+ JAVA
+ }
+
+ @JvmStatic
+ fun setIntegrationType(integrationType: IntegrationType, context: Context) {
+ val sharedPref = context.getSharedPreferences("Settings", Context.MODE_PRIVATE)
+ with(sharedPref.edit()) {
+ putString("integrationType", Json.Default.encodeToString(integrationType))
+ apply()
+ }
+ }
+
+ @JvmStatic
+ fun getIntegrationType(context: Context): IntegrationType {
+ val sharedPreferences = context.getSharedPreferences("Settings", Context.MODE_PRIVATE)
+ return sharedPreferences.getString("integrationType", null)?.let {
+ Json.Default.decodeFromString(it) ?: IntegrationType.COMPOSE
+ } ?: IntegrationType.COMPOSE
+ }
}
diff --git a/build.gradle b/build.gradle
index 60c15a6..81b572d 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,21 +1,19 @@
buildscript {
ext {
- compose_bom = "2024.09.00"
- activity_compose = "1.9.2"
- compose_compiler_version = "1.5.11"
- appcompat_version = "1.6.1"
- json_serialization_version = "1.6.2"
- kotlin_version = "1.9.23"
- lifecycle_version = "2.8.5"
- material_version = "1.11.0"
- retrofit2_kotlinx_serialization = "1.0.0"
+ compose_bom = "2024.09.02"
+ activity_compose = "1.9.3"
+ compose_compiler_version = "1.5.15"
+ appcompat_version = "1.7.0"
+ json_serialization_version = "1.6.3"
+ kotlin_version = "1.9.25"
+ lifecycle_version = "2.8.6"
material_version = "1.12.0"
- material3_compose_version = "1.2.0"
+ retrofit2_kotlinx_serialization = "1.0.0"
desugar_jdk_libs = "2.1.2"
}
}// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
- id 'com.android.application' version '8.6.0' apply false
+ id 'com.android.application' version '8.7.1' apply false
id 'com.android.library' version '8.3.2' apply false
id 'org.jetbrains.kotlin.android' version "$kotlin_version" apply false
}