Mobile SDK

Other Features

schedule 20 min read

The SDK supports more features than the basic ones needed for integration. This page explores those.

Edit "Other Features" on GitHub
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.

Payment Orders

The SDK works in terms of Payment Orders as used in Checkout and Payment Menu. Therefore, all features of payment orders are available in the SDK by using a suitable custom configuration.

The rest of the page illustrates how to use certain Payment Order features with the SDK-provided Merchant Backend Configuration. Detailed descriptions of the features will not be repeated here; please refer to the Checkout documentation instead.

URLs

A Payment Order created for the SDK must have urls the same way a Payment Order to be used on a web page would. The SDK context places some requirement on these urls.

Field SDK Requirements
hostUrls Should match the value your Configuration returns in the webViewBaseURL of ViewPaymentOrderInfo.
completeUrl No special requirements. However, the SDK will intercept the navigation, so completeUrl will not actually be opened in the SDK Web View.
termsOfServiceUrl No special requirements.
cancelUrl No special requirements. However, the SDK will intercept the navigation, so cancelUrl will not actually be opened in the SDK Web View.
paymentUrl If opened in a browser, must eventually be delivered to the SDK, bringing the containing app to the foreground. See the Android and iOS specific documentation.
callbackUrl No special requirements. This is a server-to-server affair.

Merchant Backend Configuration

The SDK-provided Merchant Backend Configuration allows creating a set of urls that fulfill the above when used with a backend implementing the Merchant Backend API.

Android

1
2
3
4
5
6
7
// backendUrl is the the backendUrl of your MerchantBackendConfiguration
val urls = PaymentOrderUrls(
    context = context,
    backendUrl = backendUrl
    callbackUrl = callbackUrl,
    termsOfServiceUrl = termsOfServiceUrl
)

iOS

1
2
3
4
5
6
7
8
// On iOS, the `paymentUrl` has a nonnegligible chance of actually being shown in Safari,
// so we want to localize it. This is why we need the language parameter here.
let urls = SwedbankPaySDK.PaymentOrderUrls(
    configuration: configuration,
    language: language,
    callbackUrl: callbackUrl,
    termsOfServiceUrl: termsOfServiceUrl
)

Order Items

You may want to populate the orderItems field of the paymentOrder for e.g. printing invoices. The SDK offers facilities for working with orderItems, allowing you to discover the fields of an Order Item in your IDE.

Please refer to the Checkout documentation and/or the class documentation for the meaning of the fields.

On Android, OrderItem is a data class, so its instances are immutable, but you can easily create copies with modified fields.

Android

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
val orderItem = OrderItem(
    reference = "123abc",
    name = "Thing",
    // ItemType is a enum, allowing you to discover the options in your IDE
    type = ItemType.PRODUCT,
    `class` = "Things",
    // Optional Order Item fields are optional in the kotlin OrderItem class as well.
    // The optional fields default to null, so you do not need to specify them if
    // you do not use them.
    itemUrl = null,
    quantity = 1,
    quantityUnit = "pcs",
    unitPrice = 1000,
    vatPercent = 2500 // 25%,
    amount = 1000,
    vatAmount = 200
)

// OrderItem is immutable, but easy to create partially modified copies of
val otherItem = orderItem.copy(
    reference = "456def",
    name = "Other Thing"
)

On iOS SwedbankPaySDK.OrderItem is a struct, allowing you to store and modify them like any other Swift values.

iOS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var orderItem = SwedbankPaySDK.OrderItem(
    reference: "123abc",
    name: "Thing",
    // SwedbankPaySDK.ItemType is a enum, allowing you to discover the options in your IDE
    type: .Product,
    class: "Things",
    quantity: 1,
    quantityUnit: "pcs",
    unitPrice: 1000,
    vatPercent: 2500 // 25%,
    amount: 1000,
    vatAmount: 200
)

// a SwedbankPaySDK.OrderItem var is mutable like any var
orderItem.reference = "456def"
orderItem.name = "Other Thing"

Instrument Mode

In “Instrument Mode” the Payment Menu will display only one specific payment instrument instead of all configured on your merchant account. Please refer to the Payment Menu documentation for more information on when you should use this feature.

To use Instrment Mode with a Merchant Backend, set the instrument field of your payment order to a non-null value.

On Android, instrument is a String, but some common instruments are available int the PaymentInstruments class:

Android

1
2
3
4
5
6
7
8
9
val paymentOrder = PaymentOrder(
    ...,
    instrument = PaymentInstruments.CREDIT_CARD,
    ...
)

val otherPaymentOrder = paymentOrder.copy(
    instrument = "SomeInstrument"
)

On iOS, instrument is an struct that wraps a String, and provides common instruments as static constants:

iOS

1
2
paymentOrder.instrument = .creditCard
paymentOrder.instrument = Instrument(rawValue: "SomeInstrument")

Changing the Instrument

If a payment order is created in instrument mode, the Merchant Backend Configuration will populate the instrument and availableInstruments fields of the ViewPaymentOrderInfo. This is also recommended to do if using instrument mode with a custom Configuration. This way, the containing application can observe the payment process and determine if it is in instrument mode, and display additional UI accordingly.

Android

1
2
3
4
5
6
7
// paymentViewModel is a property of FragmentActivity
// lifecycleOwner should usually be viewLifecycleOwner if we are in a Fragment
paymentViewModel.richState.observe(this) { richState ->
    if (richState?.viewPaymentOrderInfo?.instrument != null) {
        // show instrument mode UI
    }
}

iOS

1
2
3
4
5
6
// paymentOrderDidShow is a part of the protocol SwedbankPaySDKDelegate
func paymentOrderDidShow(info: SwedbankPaySDK.ViewPaymentOrderInfo) {
    if info.instrument != nil {
        // show instrument mode UI
    }
}

To change the instrument of an ongoing payment order, call the updatePaymentOrder method. Its argument is passed to your Configuration, which must know how to interpret it. If using the Merchant Backend Configuration, the argument should be the new instrument to set.

Your Configuration should report the valid instruments for the ongoing payment order in the ViewPaymentOrderInfo.availableInstruments property. Your UI can observe this property to show only valid options to the user. The Merchant Backend Configuration populates this field when.

Android

1
2
// On Android, updatePaymentOrder is part of PaymentViewModel
paymentViewModel.updatePaymentOrder(PaymentInstruments.INVOICE)

iOS

1
2
// On iOS, updatePaymentOrder is part of SwedbankPaySDKController
swedbankPaySDKController.updatePaymentOrder(SwedbankPaySDK.Instrument.invoice)

You should, of course, then observe when the update completes, and update your UI accordingly.

Android

1
2
3
4
5
6
paymentViewModel.state.observe(this) {
    when (it) {
        UPDATING_PAYMENT_ORDER -> showUpdatingUI()
        else -> hideUpdatingUI()
    }
}

iOS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
showUpdatingUI()
swedbankPaySDKController.updatePaymentOrder(SwedbankPaySDK.Instrument.invoice)

// ... elsewhere ...

// paymentOrderDidShow is a part of the protocol SwedbankPaySDKDelegate
func paymentOrderDidShow(info: SwedbankPaySDK.ViewPaymentOrderInfo) {
    // ...code...
    hideUpdatingUI()
}

func updatePaymentOrderFailed(updateInfo: Any, error: Error) {
    hideUpdatingUI()
}

Payer Aware Payment Menu

Using Payer Aware Payment Menu involves managing payment tokens yourself. If you are using a Merchant Backend, you can have a payment order create payment tokens by setting the generatePaymentToken and payerReference fields of PaymentOrder and PaymentOrderPayer, respectively.

Android

1
2
3
4
5
6
7
8
val paymentOrder = PaymentOrder(
    ...,
    generatePaymentToken = true,
    payer = PaymentOrderPayer(
        payerReference = "user1234"
    )
    ...
)

iOS

1
2
3
4
paymentOrder.generatePaymentToken = true
paymentOrder.payer = PaymentOrderPayer(
    payerReference: "user1234"
)

Token Retrieval

The SDK contains a utility method to query a conforming Merchant Backend server for the payment tokens of a particular payerReference. Of course, you should have proper authentication in your implementation if you use this functionality, to prevent unauthorized access to other users’ tokens (the example implementation has the endpoint disabled by default).

The utility method allows you to add extra header to the request; these can be useful for implementing authentication.

Android

1
2
3
4
5
6
7
8
9
10
// N.B! getPayerOwnedPaymentTokens is a suspending function,
// so this call must be done inside suspending code.
// If the backend responds with a Problem, the call will throw
// a RequestProblemException.
val response = MerchantBackend.getPayerOwnedPaymentTokens(
    context,
    configuration,
    payerReference,
    "ExampleHeader", "ExampleValue"
)

iOS

1
2
3
4
5
6
7
8
9
10
11
12
13
let request = SwedbankPaySDK.MerchantBackend.getPayerOwnedPaymentTokens(
    configuration: configuration,
    payerReference: payerReference,
    extraHeaders: [
        "ExampleHeader": "ExampleValue"
    ]
) { result in
    // Use result here.
    // Be prepared for any errors of the type
    // SwedbankPaySDK.MerchantBackendError.
}
// If you need to cancel the request before it is complete, call
// request.cancel()

Token Use

To use a payment token with a Merchant Backend, create a payment order where you set the paymentToken field of PaymentOrder and the payerReference field of PaymentOrderPayer:

Android

1
2
3
4
5
6
7
8
val paymentOrder = PaymentOrder(
    ...,
    payer = PaymentOrderPayer(
        payerReference = "user1234"
    )
    paymentToken = "token"
    ...
)

iOS

1
2
3
4
paymentOrder.payer = PaymentOrderPayer(
    payerReference: "user1234"
)
paymentOrder.paymentToken = "token"

Your backend implementation should have proper authentication to prevent misuse of tokens. The example implementation will reject attempts to use paymentToken by default.

Add Stored Payment Instrument Details

The Merchant Backend allows you to set PaymentOrder.disableStoredPaymentDetails to use this feature as described in the Payment Menu Documentation.

As mentioned there, it is important that you have obtained consent from the user for storing payment details beforehand, if you use this feature.

Android

1
2
3
4
5
val paymentOrder = PaymentOrder(
    ...,
    disableStoredPaymentDetails = true
    ...
)

iOS

1
paymentOrder.disableStoredPaymentDetails = true

Purchase Payments

The Purchase operation is used in all common purchase scenarios. It is the default in the SDK, so you usually do not need to specify it.

Android

1
2
3
4
val paymentOrder = PaymentOrder(
    operation = Operation.PURCHASE
    ...
)

iOS

1
paymentOrder.operation = .Purchase

Verify Payments

Verify payments are also supported by the SDK

Android

1
2
3
4
val paymentOrder = PaymentOrder(
    operation = Operation.VERIFY
    ...
)

iOS

1
paymentOrder.operation = .Verify

Observing the Payment Process

Your application can observe the state of the payment and react accordingly. At a minimum, your application must remove the SDK UI from view when the payment is finished.

Android

On Android, observe the state or richState of PaymentViewModel. The PaymentViewModel of a PaymentFragment is stored in its parent Activity. If you really need to have multiple PaymentFragments in a single Activity, you need to assign each a different key for their PaymentViewModels (use PaymentFragment.ArgumentsBuilder.viewModelProviderKey).

Android

1
2
3
4
5
6
val paymentViewModel = ViewModelProvider(activity)[PaymentViewModel::class.java]
// or use the activity.paymentViewModel convenience property

paymentViewModel.richState.observe(owner) { richState ->
    // handle new state here
}

The process state is available at richState.state, which has the following cases:

  • PaymentViewModel.State.IDLE: Not active
  • PaymentViewModel.State.IN_PROGRESS: Active; waiting for either network response or user interaction
  • PaymentViewModel.State.UPDATING_PAYMENT_ORDER: Updating the payment order (because you called updatePaymentOrder)
  • PaymentViewModel.State.COMPLETE: Complete; you should hide the PaymentFragment and check the payment status from your application servers
  • PaymentViewModel.State.CANCELED: Canceled by the user; you should hide the PaymentFragment.
  • PaymentViewModel.State.RETRYABLE_ERROR: Payment could not proceed, but the error is not fatal. See below for options here.
  • PaymentViewModel.State.FAILURE: Payment has failed. You should hide the PaymentFragment.

In the retryable error and failure states, the error that caused the failure is available at richState.exception. The exception is of any type thrown by your Configuration. If the failure state was instead reached because Swedbank Pay reported a Terminal Failure, that will be available at richState.terminalFailure.

When the PaymentFragment is showing the payment menu of the payment order, the corresponding ViewPaymentOrderInfo is available at richState.viewPaymentOrderInfo. This is the value that your Configuration returned to the SDK; if you are using Instrument Mode with the Merchant Backend Configuration, you may be interested in the instrument and availableInstruments fields to determine whether to show your instrument selection UI.

Retryable Errors

In the retryable error state, the PaymentFragment will, by default, show an error message with instructions to retry. You may disable this by setting the desired default UI flags (PaymentFragment.ArgumentsBuilder.setEnabledDefaultUI). If you do wish to provide your own UI for this state, you can call paymentViewModel.retryPreviousAction() to insruct the PaymentFragment to retry. A message describing the problem is available at richState.retryableErrorMessage.

By default, when the user taps on the Terms of Service link in the Payment Menu, a new Activity will be started, which will display the linked web page. However, you may wish to provide a native Terms of Service UI instead. To do this, set a listener for the event by calling paymentViewModel.setOnTermsOfServiceClickListener.

Android

1
2
3
4
5
paymentViewModel.setOnTermsOfServiceClickListener(lifecycleOwner) { paymentFragment, url ->
    // show ToS UI
    // return true to disable the default behaviour
    // (alternatively, return false to continue with the default behaviour)
}

iOS

On iOS, set a delegate to the SwedbankPaySDKController. The delegate methods will be called on state changes.

iOS

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
37
38
39
swedbankPaySDKController.delegate = self

// ...

extension MyClass : SwedbankPaySDKDelegate {
    func paymentOrderDidShow(info: SwedbankPaySDK.ViewPaymentOrderInfo) {
        // A payment order is now shown;
        // info is the ViewPaymentOrderInfo value from your Configuration
    }

    func paymentOrderDidHide() {
        // No payment order is now shown
    }
    func updatePaymentOrderFailed(
        updateInfo: Any,
        error: Error
    ) {
        // An update failed; error is the error from your Configuration
    }

    func paymentComplete() {
        // Payment is complete; you should hide the SwedbankPaySDKController
    }
    func paymentCanceled() {
        // Payment canceled by user; you should hide the SwedbankPaySDKController
    }

    func paymentFailed(error: Error) {
        // Payment failed. The error is either from your Configuration,
        // or a SwedbankPaySDKController.WebContentError
    }

    func overrideTermsOfServiceTapped(url: URL) -> Bool {
        // Show custom Terms of Service UI, if needed.
        // Return true to disable the default behaviour,
        // false to allow SwedbankPaySDKController to show
        // the default ToS UI.
    }
}

Any errors are ones thrown by your Configuration, or, in the case of paymentFailed, SwedbankPaySDKController.WebContentError. SwedbankPaySDKController.WebContentError has two cases:

  • .ScriptLoadingFailure(scriptUrl: URL?): The script failed to load. No error message is available; this is a limitation of WKWebView
  • .ScriptError(SwedbankPaySDK.TerminalFailure?): The script made an onError callback. The payload is the Terminal Failure reported by Swedbank Pay.

When using the Merchant Backend Configuration, other errors will be of the type SwedbankPaySDK.MerchantBackendError.

Problems

When using the Merchant Backend Configuration, most errors in the payment process will result in Problem responses from the backend. The SDK contains utilities for processing these in your application.

On Android, if any operation fails due to a Problem, the resulting exception will be of the type RequestProblemException, which allows access to the Problem object. Problem is a JVM representation of the Problem JSON response, allowing for easy access to standard fields. You are free to use Problem as-is for modeling your own problem types. The Merchant Backend Configuration further defines a subclass of Problem, MerchantBackendProblem, which is a sealed class containg all the problem types expected to occur with a Merchant Backend server. All problems reported by the Merchant Backenc Configuration are of this type, allowing you to use kotlin’s powerful when expression to handle the different errors:

Android

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
when (problem) {
    is MerchantBackendProblem.Client.MobileSDK.Unauthorized ->
        handleUnauthorized()
    is MerchantBackendProblem.Client ->
        handleOtherClientError()
    is MerchantBackendProblem.Server.MobileSDK ->
        handleMerchantBackendError()
    is MerchantBackendProblem.Server.SwedbankPay.ConfigurationError ->
        handleConfigurationError()
    is MerchantBackendProblem.Server.SwedbankPay ->
        handleSwedbankPayError()
    else ->
        handleUnknownError()

}

On iOS, if any operation fails due to a Problem, the resultig error will be SwedbankPaySDK.MerchantBackendError.problem, and it will have the problem as an associated value, of type SwedbankPaySDK.Problem. SwedbankPaySDK.Problem is a enum with a similar structure to the Android MerchantBackendProblem, allowing for idiomatic error handling.

iOS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if case SwedbankPaySDK.MerchantBackendError.problem(let problem) = error {
    switch problem {
    case .client(.mobileSDK(.unauthorized)):
        handleUnauthorized()
    case .client:
        handleOtherClientError()
    case .server(.mobileSDK):
        handleMerchantBackendError()
    case .server(.swedbankPay):
        handleSwedbankPayError()
    default:
        handleUnknownError()
    }
}

Android: View Model Provider Key

On Android, you communicate with a PaymentFragment through a PaymentViewModel that is stored in the FragmentActivity containing the PaymentFragment. Normally this means that a single Activity can only contain one PaymentFragment. This should not usually be a problem, but for advanced use cases, you can manually set the storage key for the PaymentViewModel of a given PaymentFragment, allowing for multiple instances to coexist in a single Activity. Further use of multiple simultaneous PaymentFragments is beyond the scope of this document.

The key is set as an argument to the PaymentFragment. You can either use the PaymentFragment.ArgumentsBuilder to build the argument bundle (recommended), or you can manually set a value for the key PaymentFragment.ARG_VIEW_MODEL_PROVIDER_KEY.

Android

1
2
3
4
5
6
7
8
9
10
val arguments = PaymentFragment.ArgumentsBuilder()
    .paymentOrder(paymentOrder)
    .viewModelProviderKey("PaymentViewModelOne")
    .build()

val manualArguments = Bundle()
manualArguments.putString(
    PaymentFragment.ARG_VIEW_MODEL_PROVIDER_KEY,
    "PaymentViewModelTwo"
)

Android: Default UI

PaymentFragment is very plain; in normal use it will only contain a progress indicator and/or a WebView. For error conditions that may be resolved by a simple retry, however, it will, by default show a message and a pull-to-refresh control. If desired, this UI can be disabled. In addition, is a completion message and an error message available. Those are unlikely to be useful in production, but can be enabled for development, if desired.

This feature is also controller by an argument. This argument is an integer representing flag bits.

Android

1
2
3
4
5
6
7
8
9
10
val arguments = PaymentFragment.ArgumentsBuilder()
    .paymentOrder(paymentOrder)
    .setEnabledDefaultUI() // enable nothing
    .build()

val manualArguments = Bundle()
manualArguments.putInt(
    PaymentFragment.ARG_ENABLED_DEFAULT_UI,
    PaymentFragment.RETRY_PROMPT or PaymentFragment.ERROR_MESSAGE
)

Debugging Features

To help with investigating problematic scenarios, the SDK has some debugging features.

Android

The SDK normally attempts to keep the payment flow inside a WebView as far as possible. In case some payment instrument fails to work properly, it may be helpful to see if it is related to the WebView. To allow this, you can set a flag to immediately move the payment process to the browser app when it navigates away from the payment menu. In particular, if some card issuer’s flow opens a third-party application, but the process fails to return to your app, you should see if this option makes any difference.

Testing your application with this option enabled can also be useful in ascertaining that your “Android Intent callback” endpoint is working properly.

Android

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

val manualArguments = Bundle()
manualArguments.putBoolean(
    PaymentFragment.ARG_USE_BROWSER,
    true
)

If a payment flow should open a third-party application, but this does not happen as expected, you can set a flag to display error dialogs when web content attempts to start an Activity, but it fails.

Android

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

val manualArguments = Bundle()
manualArguments.putBoolean(
    PaymentFragment.ARG_DEBUG_INTENT_URIS,
    true
)

iOS

As explained in the iOS documentation, the iOS SDK will, in some cases, open navigations out of the payment menu in Safari rather than the WKWebView. The SDK contains a list of redirects that we have tested to be working, but, of course, this list can be neither complete not correct for all time. If you enounter a payment flow that opens in the WKWebView (i.e. does not open in Safari), but that does not work correctly, you can make all navigations go to Safari to check if the flow has become incompatible with WKWebView. On the other hand, if you encounter a payment flow that opens in Safari and wish to investigate if it would work in the web view instead, you can make all navigations open in the web view.

iOS

1
2
3
swedbankPaySDKController.webRedirectBehavior = .AlwaysUseWebView

swedbankPaySDKController.webRedirectBehavior = .AlwaysUseBrowser

If you have set .AlwaysUseBrowser and discovered a site that works with WKWebView, you can set a webNavigationLogger to your SwedbankPaySDKController and make note of the URL. Then, modify your Configuration to allow that URL to be opened in the WKWebView. If using the Merchant Backend Configuration, add a additionalAllowedWebViewRedirects argument to your initializer. If using a custom Configuration, change your decidePolicyForPaymentMenuRedirect implementation accordingly.

iOS

1
2
3
swedbankPaySDKController.webNavigationLogger = { url in
    print(url)
}
1
2
3
4
let configuration = SwedbankPaySDK.MerchantBackendConfiguration(
    backendUrl: backendUrl,
    additionalAllowedWebViewRedirects: [.Domain("example.com")]
)

After you have verified that a domain works with WKWebView, please file an issue to have it added to the SDK’s bundled list.