Mobile SDK

Android

With a Merchant Backend in place, we can start developing a mobile application with Swedbank Pay payments. Let us begin with Android.

Edit "Android" on GitHub

This guide assumes that you are using the Merchant Backend Configuration and your backend implements the Merchant Backend API. If you are using a custom backend instead, the meaning of PaymentFragment arguments will be different, as well as any errors reported, but the basic process is the same. The differences will be highlighted in the chapter on custom backends.

Installation

The Android component of the Swedbank Pay Mobile SDK is distributed through Maven Central. It is split into two libraries in the com.swedbankpay.mobilesdk group:

If you are not using the Merchant Backend API in your backend, you only need to use the first one. Otherwise, you should add both libraries to your project to get utilities for interfacing with your Merchant Backend server.

Most applications can integrate the SDK by simply adding the dependency in the build.gradle file:

1
2
3
4
dependencies {
    implementation 'com.swedbankpay.mobilesdk:mobilesdk:4.1.1'
    implementation 'com.swedbankpay.mobilesdk:mobilesdk-merchantbackend:4.1.1'
}

Please refer to Maven Central for the latest versions of the libraries.

Usage

sequenceDiagram
    participant App
    participant SDK
    participant Merchant
    participant SwedbankPay as Swedbank Pay
    participant Ext as External App

    rect rgba(238, 112, 35, 0.05)
        note left of App: Configuration
        App ->> SDK: MerchantBackendConfiguration.Builder("https://example.com/swedbank-pay-mobile/").build()
        SDK -->> App: configuration
        App ->> SDK: PaymentFragment.defaultConfiguration = configuration
    end

    opt Unless Guest Payment
        App ->> SDK: Consumer(language = ..., shippingAddressRestrictedToCountryCodes = ...)
        SDK -->> App: consumer
    end

    rect rgba(138, 205, 195, 0.1)
        note left of App: Prepare Payment
        App ->> SDK: PaymentOrderUrls(context, "https://example.com/swedbank-pay-mobile/")
        SDK -->> App: paymentOrderUrls
        App ->> SDK: PaymentOrder(urls = paymentOrderUrls, ...)
        SDK -->> App: paymentOrder
    end

    App ->> SDK: activity.paymentViewModel.[rich]state.observe(...)
    App ->> SDK: PaymentFragment.ArgumentsBuilder().consumer(consumer).paymentOrder(paymentOrder).build()
    SDK -->> App: arguments
    App ->> SDK: PaymentFragment()
    SDK -->> App: paymentFragment
    App ->> SDK: paymentFragment.arguments = arguments
    App ->> App: Show paymentFragment

    rect rgba(138, 205, 195, 0.1)
        note left of App: Discover Endpoints
        SDK ->> Merchant: GET /swedbank-pay-mobile/
        Merchant -->> SDK: { "consumers": "/swedbank-pay-mobile/consumers", "paymentorders": "/swedbank-pay-mobile/paymentorders" }
    end

    opt Unless Guest Payment
        SDK ->> Merchant: POST /swedbank-pay-mobile/consumers
        Merchant ->> SwedbankPay: POST /psp/consumers
        SwedbankPay -->> Merchant: rel: view-consumer-identification
        Merchant -->> SDK: rel: view-consumer-identification
        SDK ->> SDK: Show html page with view-consumer-identification
        SwedbankPay ->> SDK: Consumer identification process
        SDK ->> SwedbankPay: Consumer identification process
        SwedbankPay ->> SDK: consumerProfileRef
        SDK ->> SDK: paymentOrder.payer = { consumerProfileRef }
    end

    rect rgba(138, 205, 195, 0.1)
        note left of App: Payment Menu
        SDK ->> Merchant: POST /swedbank-pay-mobile/paymentorders
        Merchant ->> SwedbankPay: POST /psp/paymentorders
        SwedbankPay -->> Merchant: rel: view-paymentorder
        Merchant -->> SDK: rel: view-paymentorder
        SDK ->> SDK: Show html page with view-paymentorder
        SwedbankPay ->> SDK: Payment process
        SDK ->> SwedbankPay: Payment process
        opt Redirect to Third-Party Page
            SDK ->> SDK: Show third-party page
            SDK ->> SDK: Intercept navigation to paymentUrl
            SDK ->> SDK: Reload html page with view-paymentorder
        end
        opt Launch External Application
            SDK ->> Ext: Start external application
            Ext ->> Merchant: Open paymentUrl
            Merchant ->> Ext: Redirect intent://<...>action=com.swedbankpay.mobilesdk.VIEW_PAYMENTORDER
            Ext ->> SDK: Start activity\naction=com.swedbankpay.mobilesdk.VIEW_PAYMENTORDER
            SDK ->> SDK: Reload html page with view-paymentorder
        end
        SDK ->> SDK: Intercept navigation to completeUrl
        SDK ->> SDK: paymentViewModel.state <- SUCCESS
        SDK ->> App: observer.onChanged(SUCCESS)
    end

    App ->> App: Remove paymentFragment

The public API of the Android SDK is in the package com.swedbankpay.mobilesdk. The main component is PaymentFragment, a Fragment that handles a single payment order. To use a PaymentFragment, it must have a Configuration. In most cases it is enough to construct a single Configuration and set it as the default. In more advanced cases you will need to subclass PaymentFragment and override getConfiguration.

For using a backend implementing the Merchant Backend API, the SDK also provides utility classes in the package com.swedbankpay.mobilesdk.merchantbackend. The examples on this page make use of these, including the Configuration implementation MerchantBackendConfiguration.

1
2
3
4
5
val backendUrl = "https://example.com/swedbank-pay-mobile/"

val configuration = MerchantBackendConfiguration.Builder(backendUrl)
    .build()
PaymentFragment.defaultConfiguration = configuration

To start a payment, you need a PaymentOrder, and, unless making a guest payment, a Consumer. Using a Consumer makes future payments by the same payer easier.

The semantics of Consumer properties are the same as the fields of the POST/psp/consumers request. There are default values for the operation and language properties (ConsumerOperation.INITIATE_CONSUMER_SESSION and Language.ENGLISH, respectively).

1
2
3
4
val consumer = Consumer(
    language = Language.SWEDISH,
    shippingAddressRestrictedToCountryCodes = listOf("NO", "SE", "DK")
)

Similarly, the semantics of PaymentOrder properties are the same as the fields of the POST /psp/paymentorders request. Sensible default values are provided for many of the properties. The urls property has no default per se, but there are convenience constructors available for it, and it is recommended that you use them. Assuming you have the Android Payment Url Helper endpoint set up with the specified static path relative to your backend url (i.e. sdk-callback/android-intent), then using the one of the PaymentOrderUrls(context: Context, backendUrl: String) variants will set the paymentUrl correctly.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
val paymentOrder = PaymentOrder(
    currency = Currency.getInstance("SEK"),
    amount = 1500L,
    vatAmount = 375L,
    description = "Test Purchase",
    language = Language.SWEDISH,
    urls = PaymentOrderUrls(context, backendUrl),
    payeeInfo = PayeeInfo(
        // ①
        payeeName = "Merchant1",
        productCategory = "A123",
        orderReference = "or-123456",
        subsite = "MySubsite"
    ),

    orderItems = listOf(
        OrderItem(
            reference = "P1",
            name = "Product1",
            type = ItemType.PRODUCT,
            `class` = "ProductGroup1",
            itemUrl = "https://example.com/products/123",
            imageUrl = "https://example.com/product123.jpg",
            description = "Product 1 description",
            discountDescription = "Volume discount",
            quantity = 4,
            quantityUnit = "pcs",
            unitPrice = 300L,
            discountPrice = 200L,
            vatPercent = 2500,
            amount = 1000L,
            vatAmount = 250L
        )
    )
)
  • ① payeeId and payeeReference are required fields, but default to the empty string. The assumption here is that your Merchant Backend will override the values set here. If your system works better with the Mobile Client setting them instead, they are available here also.

To start a payment, create a PaymentFragment and set its arguments according to the payment. The PaymentFragment.ArgumentsBuilder class is provided to help with creating the argument bundle. In most cases you only need to worry about the paymentOrder property. The payment process starts as soon as the PaymentFragment is visible. Note that Digital Payments is currently opt-in, so that merchants can upgrade without too much breaking changes and start using the new Digital Payments when ready.

1
2
3
4
5
6
7
8
9
10
11
12
val arguments = PaymentFragment.ArgumentsBuilder()
    .checkoutV3(true)
    .paymentOrder(paymentOrder)
    .build()

val paymentFragment = PaymentFragment()
paymentFragment.arguments = arguments

// Now use FragmentManager to show paymentFragment.
// You can also make a navigation graph with PaymentFragment
// and do something like
// findNavController().navigate(R.id.showPaymentFragment, arguments)

Note that the SDK only supports customer-checkin for version 2, and provides fallback for merchants in need of this. Then you need to supply a consumer and the Checkout v3 setting becomes irrelevant.

1
2
3
4
5
6
7
8
9
val arguments = PaymentFragment.ArgumentsBuilder()
    .consumer(consumer)
    .paymentOrder(paymentOrder)
    .build()

val paymentFragment = PaymentFragment()
paymentFragment.arguments = arguments

// Now handle the fragment the same way as previously.

To observe the payment process, use the PaymentViewModel of the containing Activity. When the PaymentViewModel signals that the payment process has reached a final state, you should remove the PaymentFragment and inform the user of the result.

1
2
3
4
5
6
7
paymentViewModel.state.observe(this, Observer {
    if (it.isFinal == true) {
        // Remove PaymentFragment
        // Check payment status from your backend
        // Notify user
    }
})

Note that checking the payment status after completion is outside the scope of the Mobile SDK. Your backend should collect any information it needs to perform this check when it services the request to the Payment Orders endpoint made by the PaymentFragment.

Errors

If any errors happen in the payment, the PaymentViewModel will report a state of either FAILURE or RETRYABLE_ERROR. If the error is retryable, the PaymentFragment will show an error message and a retry control (this is configurable), but you can also trigger a retry by calling retryPreviousAction on the PaymentViewModel.

When the state is FAILURE or RETRYABLE_ERROR, and the error condition was caused by an exception thrown from the Configuration, that exception is available in PaymentViewModel.richState.exception. The exception will be of any type throw by your Configuration. When using MerchantBackendConfiguration, this means it will be an IOException if there was a problem communicating with the backend, and an IllegalStateException if you have made a programming error (consult the exception message). A particular IOException to check for is RequestProblemException, which signals that the backend responded with a Problem message. Another one is UnexpectedResponseException, which signals that the SDK did not understand the backend response.

Problems

If errors are encountered in the payment process, the Merchant Backend is expected to respond with a Problem Details for HTTP APIs (RFC 7807) message. If a problem occurs, the application can receive it by observing the richState of the PaymentViewModel. If a problem has occurred, the exception property of the RichState will contain a RequestProblemException. The problem is then accessible as exception.problem. The Android SDK will parse any RFC 7807 problem, but it has specialized data types for known problem types, namely the Common Problems and the Merchand Backend Problems.

Problems are presented as a class hierarchy representing different problem categories. All problems parsed from RFC 7807 messages are classified as either Client or Server problems. A Client problem is one caused by client behavior, and is to be fixed by changing the request made to the server. Generally, a Client problem is a programming error, with the possible exception of Problem.Client.MobileSDK.Unauthorized. A Server problem is one caused by a malfunction or lack of service in the server evironment. A Server problem is fixed by correcting the behavior of the malfunctioning server, or simply trying again later.

Further, both Client and Server problems are categorized as MobileSDK, SwedbankPay, or Unknown. MobileSDK problems are ones with Merchant Backend problem types, while SwedbankPay problems have Swedbank Pay API problem types. Unknown problems are of types that the SDK has no knowledge of. There is also the interface SwedbankPayProblem, which encompasses both Client and Server type SwedbankPay problems.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
paymentViewModel.richState.observe(this, Observer {
    if (it.state.isFinal == true) {
        val exeption = it.exception as? RequestProblemException
        if (exception != null) (
            when (val problem = exception.problem) {
                is MerchantBackendProblem.Client.MobileSDK.Unauthorized ->
                    Log.d(TAG, "Credentials invalidated: ${problem.message}")

                if MerchantBackendProblem.Client.MobileSDK ->
                    Log.d(TAG, "Other client error at Merchant Backend: ${problem.raw}")

                is MerchantBackendProblem.Client.SwedbankPay.InputError ->
                    Log.d(TAG, "Payment rejected by Swedbank Pay: ${problem.detail}; Fix: ${problem.action}")

                is MerchantBackendProblem.Client.Unknown ->
                    if (problem.type == "https://example.com/problems/special-problem") {
                        Log.d(TAG, "Special problem occurred: ${problem.detail}")
                    } else {
                        Log.d(TAG, "Unexpected problem: ${problem.raw}")
                    }

                is MerchantBackendProblem.Server.MobileSDK.BackendConnectionTimeout ->
                    Log.d(TAG, "Swedbank Pay timeout: ${problem.message}")

                is MerchantBackendProblem.Server.SwedbankPay.SystemError ->
                    Log.d(TAG, "Generic server error at Swedbank Pay: ${problem.detail}")

                is SwedbankPayProblem ->
                    Log.d(TAG, "Other problem at Swedbank Pay: ${problem.detail}; Fix: ${problem.action}")

                else ->
                    Log.d(TAG, "Unexpected problem: ${problem.raw}")
            }
        }
    }
})

Payment URL And External Applications

The payment process may involve navigating to third-party web pages, or even launching external applications. To resume processing the payment in the payment menu, each payment order must have a Payment Url. As mentioned above, the SDK has convenience constructors to set up a payment url for you, and as the SDK handles showing third-party web pages inside the PaymentFragment, it automatically intercepts any navigation to the payment url, and reloads the payment menu. This requires no additional setup.

If a third party application is launched, it will signal the return to the payment menu by opening the payment url, using a standard ACTION_VIEW Intent. The payment url is built such that it uses the Android Payment Url Helper, which serves an html page that converts the url to an intent url and redirects to it. The SDK has an intent filter for that intent, so the SDK will receive it, bringing the containing application to the foreground, and reloading the payment menu. If your Merchant Backend serves the Android Payment Url Helper endpoint at the specified path, no further setup is needed.

Note that there is an argument for debugging purposes that cause third-party web pages to be opened in an external application. In that case the process continues analogously to the external application case. Using this argument should not be necessary, however. If you do find a case that does not work inside the PaymentFragment, but does work when using the browser for third-party sites, please file a bug on the Android SDK.