Mobile SDK

Android

schedule 10 min read

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

warning

Unsupported

The SDK is at an early stage of development and is not supported as of yet by Swedbank Pay. It is provided as a convenience to speed up your development, so please feel free to play around. However, if you need support, please wait for a future, stable release.

Installation

The Android component of the Swedbank Pay Mobile SDK is distributed through JCenter, which is a default repository in Android Studio projects. Therefore, most applications can integrate the SDK by simply adding the dependency:

implementation 'com.swedbankpay.mobilesdk:mobilesdk:1.0.0-beta17'

[Development note: There is not yet a stable release of the SDK. In particular, be prepared for potential API changes before the first stable release. That said, the beta17 version of the Android SDK component is fairly stable, and we do not foresee any major changes to it at this point.]

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: Configuration.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: 301 Moved Permanently\nLocation: intent://<...>action=;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.

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

val configuration = Configuration.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. 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 consumer and paymentOrder properties. The payment process starts as soon as the PaymentFragment is visible.

1
2
3
4
5
6
7
8
9
10
11
12
val arguments = PaymentFragment.ArgumentsBuilder()
    .consumer(consumer)
    .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)

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.

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, it will be in the problem property of the RichState. 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 behaviour, 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 behaviour 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 are also the interfaces SwedbankPayProblem, which encompasses both Client and Server type SwedbankPay problems, and UnknownProblem, which contains both Client and Server type Unknown problems.

Lastly, there are the UnexpectedContent problems, which again exist in both Client and Server variants. These are not proper RFC 7807 problems, but they represent a situtation where the Android SDK was unable to parse a response it received. They can be considered problem types of the Android SDK itself. There is also the UnexpectedContentProblem interface, containing both kinds of UnexpectedContent problems. To complement this, there is also a ProperProblem interface, which contains all problems parsed from RFC 7807 messages. Be careful when using ProperProblem in a when clause, however, as it will match everything except UnexpectedContent problems, so unless the condition for is ProperProblem is the last one, your code may not do what you intend it to.

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) {
        when (val problem = it.problem) {
            is Problem.Client.MobileSDK.Unauthorized ->
                Log.d(TAG, "Credentials invalidated: ${problem.message}")

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

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

            is Problem.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 Problem.Server.MobileSDK.BackendConnectionTimeout ->
                Log.d(TAG, "Swedbank Pay timeout: ${problem.message}")

            is Problem.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}")

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

            is UnexpectedContentProblem ->
                Log.d(TAG, "Unexpected response from Merchant Backend: ${problem.body}")
        }
    }
})

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 converts the url to an intent url. 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.