In this chapter we explore how to integrate the mobile SDK with a fully custom backend server. It is recommended that you also read through the chapters covering the example Merchant Backend API and gain an understanding of how the SDK works with that as backend.
Basic Backend Requirements
To support the SDK, your backend must be capable of at least
creating a payment order. If you wish to use consumer
identification, it must also be able to
start an identification session. In addition to
these, your backend should serve the appropriate html documents at urls used for
the paymentUrl; the content of these html documents will be
discussed below, but it is noteworthy that they are different for payments from
Android applications and those from iOS applications. Further, the urls used for
as paymentUrl on iOS should be configured as universal links for your iOS
application.
Android Configuration
To bind the SDK to your custom backend, you must create a subclass of
com.swedbankpay.mobilesdk.Configuration. This must be a Kotlin class. If you
cannot use Kotlin, you can use the compatibility class
com.swedbankpay.mobilesdk.ConfigurationCompat.
Your subclass must provide implementations of postPaymentorders and
postConsumers. These methods are named after the corresponding Swedbank
Pay APIs they are intended to be forwarded to. If you do not intend to use
consumer identification, you can have your postConsumers implementation throw
an exception.
The methods will be called with the arguments you give to the PaymentFragment.
Therefore, the meaning of consumer, paymentOrder, and userData is up to
you. If the consumer was identified before creating the paymentOrder, the
consumer reference will be passed in the consumerProfileRef argument of
postPaymentorders. The exact implementation of these methods is outside the
scope of this document.
You must return a ViewPaymentOrderInfo and optionally also a
ViewConsumerIdentificationInfo object respectively; please refer to their
class documentation on how to populate them from your backend responses. Any
exception you throw from these methods will in turn be reported from the
PaymentViewModel. Whether a given exception is treated as a retryable
condition is controlled by the shouldRetryAfter<Operation>Exception methods;
by default they only consider IllegalStateException as fatal. Please refer to
the Configuration documentation on all the features.
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
class MyConfiguration : Configuration() {
override suspend fun postPaymentorders(
context: Context,
paymentOrder: PaymentOrder?,
userData: Any?,
consumerProfileRef: String?
): ViewPaymentOrderInfo {
val viewPaymentOrder = post("https://example.com/pay/android")
return ViewPaymentOrderInfo(
viewPaymentLink = "https://example.com/",
viewPaymentOrder = viewPaymentOrder,
completeUrl = "https://example.com/complete",
cancelUrl = "https://example.com/cancel",
paymentUrl = "https://example.com/payment/android",
termsOfServiceUrl = "https://example.com/tos",
isV3 = true
)
}
override suspend fun postConsumers(
context: Context,
consumer: Consumer?,
userData: Any?
): ViewConsumerIdentificationInfo {
val viewConsumerIdentification = post("https://example.com/identify")
return ViewConsumerIdentificationInfo(
webViewBaseUrl = "https://example.com/",
viewConsumerIdentification = viewConsumerIdentification
)
// Or throw Exception() if not using consumer identification
}
}
iOS Configuration
On iOS you must conform to the SwedbankPaySDKConfiguration protocol. Just like
on Android, you must provide implementations for the postPaymentorders and
postConsumers methods. The consumer, paymentOrder, and userData
arguments to those methods will be the values you initialize your
SwedbankPaySDKController with, and their meaning is up to you. The
postPaymentorders method will optionally receive a consumerProfileRef
argument, if the consumer was identified before creating the payment order.
The methods are asynchronous, and the result is reported by calling the
completion callback with the result. Successful results have payloads of
SwedbankPaySDK.ViewPaymentOrderInfo and
SwedbankPaySDK.ViewConsumerIdentificationInfo, respectively; please refer to
the type documentation on how to populate those types. If you do not intend to
use consumer identification, your postConsumers should callback should be
called with a failing result of SwedbankPayConfigurationError.notImplemented.
The other errors of any failure result you report will be propagated back to
your app in the paymentFailed(error:) delegate method. You must call the
completion callback exactly once, multiple calls are a programming error.
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
struct MyConfiguration : SwedbankPaySDKConfiguration {
func postPaymentorders(paymentOrder: SwedbankPaySDK.PaymentOrder?,
userData: Any?,
consumerProfileRef: String?,
options: SwedbankPaySDK.VersionOptions,
completion: @escaping (Result<SwedbankPaySDK.ViewPaymentOrderInfo, Error>) -> Void) {
post(URL(string: "https://example.com/pay/ios")!) { result in
do {
let viewPaymentorder = try result.get()
let info = SwedbankPaySDK.ViewPaymentOrderInfo(isV3: true,
webViewBaseURL: URL(string: "https://example.com/"),
viewPaymentLink: viewPaymentorder,
completeUrl: URL(string: "https://example.com/complete")!,
cancelUrl: URL(string: "https://example.com/cancel"),
paymentUrl: URL(string: "https://example.com/payment/ios"),
termsOfServiceUrl: URL(string: "https://example.com/tos"))
completion(.success(info))
} catch {
completion(.failure(error))
}
}
}
func postConsumers(consumer: SwedbankPaySDK.Consumer?,
userData: Any?,
completion: @escaping (Result<SwedbankPaySDK.ViewConsumerIdentificationInfo, Error>) -> Void) {
post(URL(string: "https://example.com/identify")!) { result in
do {
let viewConsumerIdentification = try result.get()
let info = SwedbankPaySDK.ViewConsumerIdentificationInfo(webViewBaseURL: URL(string: "https://example.com/"),
viewConsumerIdentification: viewConsumerIdentification)
completion(.success(info))
} catch let error {
completion(.failure(error))
}
}
// Or completion(.failure(SwedbankPayConfigurationError.notImplemented)) if not using consumer identification
}
}
Backend
The code examples allude to a run-of-the-mill https API, but you can of course handle the communication in any way you see fit. The important part is that your backend must then communicate with the Swedbank Pay API using your secret token, and perform the requested operation.
POST Consumers
The “POST consumers” operation is simple, you must make a request to POST
/psp/consumers with a payload of your choosing, and you must get the
view-consumer-identification link back to the SDK.
Consumer SDK Request
SDK Request
1
2
POST /identify HTTP/1.1
Host: example.com
Consumer SDK Response
SDK Response
1
2
3
4
HTTP/1.1 200 OK
Content-Type: text/plain
https://ecom.externalintegration.payex.com/consumers/core/scripts/client/px.consumer.client.js?token=5a17c24e-d459-4567-bbad-aa0f17a76119
Consumer Swedbank Pay Request
Swedbank Pay Request
1
2
3
4
5
6
7
8
9
10
POST /psp/consumers HTTP/1.1
Host: api.externalintegration.payex.com
Authorization: Bearer <AccessToken>
Content-Type: application/json
{
"operation": "initiate-consumer-session",
"language": "sv-SE",
"shippingAddressRestrictedToCountryCodes" : ["NO", "SE", "DK"]
}
Consumer Swedbank Pay Response
Swedbank Pay Response
1
2
3
4
5
6
7
8
9
10
11
12
13
HTTP/1.1 200 OK
Content-Type: application/json
{
"operations": [
{
"method": "GET",
"rel": "view-consumer-identification",
"href": "https://ecom.externalintegration.payex.com/consumers/core/scripts/client/px.consumer.client.js?token=5a17c24e-d459-4567-bbad-aa0f17a76119",
"contentType": "application/javascript"
}
]
}
This is, of course, an over-simplified protocol for documentation purposes.
POST Payment Orders
The “POST paymentorders” is a bit more complicated, as it needs to tie in with
paymentUrl handling. Also, the full set of payment order urls must be made
available to the app. In this simple example we use static urls for all of
those, but in a real application you will want to create at least some of them
dynamically, and will therefore need to incorporate them to your protocol.
Payment Order SDK Request
SDK Request
1
2
POST /pay/android HTTP/1.1
Host: example.com
Payment Order SDK Response
SDK Response
1
2
3
4
HTTP/1.1 200 OK
Content-Type: text/plain
https://ecom.externalintegration.payex.com/paymentmenu/core/scripts/client/px.paymentmenu.client.js?token=5a17c24e-d459-4567-bbad-aa0f17a76119&culture=sv-SE
Payment Order Swedbank Pay Request
Swedbank Pay Request
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /psp/paymentorders HTTP/1.1
Host: api.externalintegration.payex.com
Authorization: Bearer <AccessToken>
Content-Type: application/json
{
"paymentorder": {
"urls": {
"hostUrls": ["https://example.com/"],
"completeUrl": "https://example.com/complete",
"cancelUrl": "https://example.com/cancel",
"paymentUrl": "https://example.com/payment/android"
}
}
}
Payment Order Swedbank Pay Response
Swedbank Pay Response
1
2
3
4
5
6
7
8
9
10
11
12
13
HTTP/1.1 201 Created
Content-Type: application/json
{
"operations": [
{
"href": "https://ecom.externalintegration.payex.com/paymentmenu/core/scripts/client/px.paymentmenu.client.js?token=5a17c24e-d459-4567-bbad-aa0f17a76119&culture=sv-SE&_tc_tid=30f2168171e142d38bcd4af2c3721959",
"rel": "view-paymentorder",
"method": "GET",
"contentType": "application/javascript"
}
]
}
Payment URL
As discussed in previous chapters, in some situations the paymentUrl of a
payment will be opened in the browser. When this happens, we need a way of
returning the flow to the mobile application. We need to take a slightly
different approach depending on the client platform.
Android
The SDK has an Intent Filter for the
com.swedbankpay.mobilesdk.VIEW_PAYMENTORDER action. When it receives this
action, if the Intent uri is equal to the paymentUrl of an ongoing payment (as
reported by ViewPaymentOrderInfo), it will reload the payment menu of that
payment. Therefore, if the paymentUrl is opened in the browser, that page must
start an activity with such an Intent. This can be done by navigating to an
intent scheme url. Note that the
rules for following intent-scheme navigations can sometimes cause redirects to
those url not to work. To work around this, the paymentUrl must serve a proper
html page, which attempts to immediately redirect to the intent-scheme url, but
also has a link the user can tap on.
Refer to the intent scheme url documentation on how to form one. You should always include the package name so that your intent is not mistakenly routed to the wrong app.
Request
1
2
GET /payment/android HTTP/1.1
Host: example.com
Response
1
2
3
4
5
6
7
8
9
10
11
12
HTTP/1.1 200 OK
Content-Type: text/html
<html>
<head>
<title>Swedbank Pay Payment</title>
<meta http-equiv="refresh" content="0;url=intent://example.com/payment/android#Intent;scheme=https;action=com.swedbankpay.mobilesdk.VIEW_PAYMENTORDER;package=com.example.app;end;">
</head>
<body>
<a href="intent://example.com/payment/android#Intent;scheme=https;action=com.swedbankpay.mobilesdk.VIEW_PAYMENTORDER;package=com.example.app;end;">Back to app</a>
</body>
</html>
iOS
Switching apps on iOS is always done by opening a URL. urls. It is preferred to
use a Universal Link URL. Your app and backend must be configured such that the
paymentUrl used on iOS payments is registered as a universal link to your app.
Then, on iOS 13.4 or later, in most cases when the paymentUrl is navigated to,
it will be immediately given to your app to handle. However, Universal Links are
not entirely reliable, in particular if you wish to support iOS earlier than
13.4, and we must still not get stuck if the paymentUrl is opened in the
browser instead.
Now, the most straightforward way of escaping this situation is to define a
custom url scheme for your app, and do something similar to the Android
solution, involving that scheme. If you plan to support only iOS 13.4 and up,
where the situation is rather unlikely to occur, this can be sufficient. Doing
this on earlier versions is likely to end up suboptimal, though, as doing this
will cause an unsightly confirmation dialog to be shown before the app is
actually launched. As the situation where paymentUrl is opened in the browser
is actually quite likely to occur on iOS earlier than 13.4, this means you are
more or less subjecting all users on those systems to sub-par user experience.
To provide a workaround to the confirmation popup, we devise a system that
allows the user to retrigger the navigation to paymentUrl in such a way as to
maximize the likelihood that the system will let the app handle it. As one of
the criteria is that the navigation must be to a domain different to the current
one, the paymentUrl itsef will always redirect to a page on a different
domain. That page is then able to link back to the paymentUrl and have that
navigation be routed to the app. You could host this “trampoline” page yourself,
but Swedbank Pay has a generic one available for use. The trampoline page takes
three arguments, target, which should be set to your paymentUrl, language,
which supports all the Checkout languages, and app, you app name that will be
shown on the page.
On iOS any URL the app is opened with is delivered to the
UIApplicationDelegate by either the
application(_:continue:restorationHandler:) method (for universal links) or
application(_:open:options:). To let the SDK respond appropriately, you need
to call SwedbankPaySDK.continue(userActivity:) or SwedbankPaySDK.open(url:)
from those methods, respectively.
Request
1
2
GET /payment/ios HTTP/1.1
Host: example.com
Response
1
2
HTTP/1.1 301 Moved Permanently
Location: https://ecom.stage.payex.com/externalresourcehost/trampoline?target=https%3A%2F%2Fexample.com%2Fpayment%2Fios%3Ffallback%3Dtrue&language=en-US&app=Example%20App
The trampoline url will, in turn, serve an html page:
Request
1
2
GET /externalresourcehost/trampoline?target=https%3A%2F%2Fexample.com%2Fpayment%2Fios%3Ffallback%3Dtrue&language=en-US&app=Example%20App HTTP/1.1
Host: ecom.stage.payex.com
Response
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
HTTP/1.1 200 OK
Content-Type: text/html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Swedbank Pay Redirect</title>
<link rel="icon" type="image/png" href="/externalresourcehost/content/images/favicon.png">
<link rel="stylesheet" href="/externalresourcehost/content/css/style.css">
</head>
<body>
<div class="trampoline-container" onclick="redirect()">
<img alt="Swedbank Pay Logo" src="/externalresourcehost/content/images/swedbank-pay-logo-vertical.png" />
<span class="trampoline-text">
<a>Back to Example App</a>
</span>
</div>
<script>
function redirect() { window.location.href = decodeURLComponent("https%3A%2F%2Fexample.com%2Fpayment%2Fios%3Ffallback%3Dtrue"); };
</script>
</body>
</html>
The page links back to https://example.com/payment/ios?fallback=true. Notice
the additional parameter. This is, indeed, part of the target parameter, and
under the control of your backend. The purpose of this is to allow for one final
escape hatch, in case the universal link mechanism fails to work. If this url is
yet again opened in the browser, the backend responds with a redirect to to a
custom-scheme url. (This should only happen if your universal links
configuration is broken, or if iOS has somehow failed to load the
Apple App-Site Association file.)
Request
1
2
GET /payment/ios?fallback=true HTTP/1.1
Host: example.com
Response
1
2
HTTP/1.1 301 Moved Permanently
Location: com.example.app://example.com/payment/ios?fallback=true
From the app perspective, in our example, the url the app is opened with will be
one these three: https://example.com/payment/ios,
https://example.com/payment/ios?fallback=true, or
com.example.app://example.com/payment/ios?fallback=true. When any of these is
passed to the SDK from your UIApplicationDelegate, the SDK will then call into
your Configuration to check if it matches the paymentUrl
(https://example.com/payment/ios in this example). This can be customized, but
by default it will allow the scheme to change and for additional query
parameters to be added to the url, so this example would work with the default
configuration.
Apple App-Site Association
As the iOS paymentUrl needs to be a universal link, the backend will also need
an Apple App-Site Association file. This must be
served at /.well-known/apple-app-site-association, and it must associate any
url used as a paymentUrl with the app.
Request
1
2
GET /.well-known/apple-app-site-association HTTP/1.1
Host: example.com
Response
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
HTTP/1.1 200 OK
Content-Type: application/json
{
"applinks": {
"apps": [],
"details": [
{
"appID": "ABCDE12345.com.example.app",
"paths": [ "/payment/ios" ],
"appIDs": [ "ABCDE12345.com.example.app" ],
"components": [
{ "/": "/payment/ios" }
]
}
]
}
}
Note that the AASA file must be served over https, otherwise iOS will not load
it. This example AASA file contains both old-style and new-style values for
maximum compatibility. You may not need the old-style values in your
implementation, depending on your situation.
Updating The Payment Order
The SDK includes a facility for updating a payment order after is has been created. The Merchant Backend Configuration uses this to allow setting the method of an instrument mode payment, but your custom Configuration can use it for whatever purpose you need.
Android
First, implement updatePaymentOrder in your Configuration subclass. This
method returns the same data type as postPaymentorders, and when it does, the
PaymentFragment reloads the payment menu according to the new data. The
paymentOrder and userData arguments are what you set for the
PaymentFragment, the viewPaymentOrderInfo argument is the current
ViewPaymentOrderInfo (as returned from a previous call to this method, or, if
this is the first update, the original postPaymentorders call). The
updateInfo argument will be the value you call
PaymentViewModel.updatePaymentOrder with, its meaning is therefore defined by
you.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class MyConfiguration : Configuration() {
override suspend fun updatePaymentOrder(
context: Context,
paymentOrder: PaymentOrder?,
userData: Any?,
viewPaymentOrderInfo: ViewPaymentOrderInfo,
updateInfo: Any?
): ViewPaymentOrderInfo {
val viewPaymentOrder = post("https://example.com/payment/android/frobnicate")
return ViewPaymentOrderInfo(
viewPaymentLink = "https://example.com/",
viewPaymentOrder = viewPaymentOrder,
completeUrl = "https://example.com/complete",
cancelUrl = "https://example.com/cancel",
paymentUrl = "https://example.com/payment/android",
termsOfServiceUrl = "https://example.com/tos",
isV3 = true
)
}
}
To trigger an update, call updatePaymentOrder on the PaymentViewModel of the
active payment. The argument of that call will be passed to your
Configuration.updatePaymentOrder as the updateInfo argument.
1
activity.paymentViewModel.updatePaymentOrder("frob")
iOS
Implement updatePaymentOrder in your configuration. Rather like the Android
method, this method takes a callback of the same type as postPaymentorders,
and when that callback is invoked with a Success result, the
SwedbankPaySDKController reloads the payment menu according to the new data.
Unlike postPaymentorders, this method must also return a request handle, which
can be used to cancel the request if needed. If the request is cancelled, the
completion callback should not be called.
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
func updatePaymentOrder(paymentOrder: SwedbankPaySDK.PaymentOrder?,
options: SwedbankPaySDK.VersionOptions,
userData: Any?,
viewPaymentOrderInfo: SwedbankPaySDK.ViewPaymentOrderInfo,
updateInfo: Any,
completion: @escaping (Result<SwedbankPaySDK.ViewPaymentOrderInfo, Error>) -> Void) -> SwedbankPaySDKRequest? {
var request = post(URL(string: "https://example.com/payment/ios/frobnicate")!) { result in
do {
let viewPaymentorder = try result.get()
let info = SwedbankPaySDK.ViewPaymentOrderInfo(isV3: true,
webViewBaseURL: URL(string: "https://example.com/"),
viewPaymentLink: viewPaymentorder,
completeUrl: URL(string: "https://example.com/complete")!,
cancelUrl: URL(string: "https://example.com/cancel"),
paymentUrl: URL(string: "https://example.com/payment/ios"),
termsOfServiceUrl: URL(string: "https://example.com/tos"))
completion(.success(info))
} catch NetworkError.cancelled {
// no callback
} catch {
completion(.failure(error))
}
}
return request
}
To trigger an update, call updatePaymentOrder on the
SwedbankPaySDKController. The argument will be passed to your configuration in
the updateInfo argument.
1
2
3
swedbankPayController.updatePaymentOrder(
updateInfo: "frob"
)
Backend
The backend implementation makes any needed calls to Swedbank Pay, and returns
whatever your implementation expects. It is recommended to always use the
view-paymentorder link from the update response, in case the update has caused
a change to that url.
Request
1
2
POST /payment/android/frobnicate HTTP/1.1
Host: example.com
Response
1
2
3
4
HTTP/1.1 200 OK
Content-Type: text/plain
https://ecom.externalintegration.payex.com/paymentmenu/core/scripts/client/px.paymentmenu.client.js?token=5a17c24e-d459-4567-bbad-aa0f17a76119&culture=sv-SE
Errors
Any exception you throw from your Configuration will be made available in
PaymentViewModel.exception or SwedbankPaySDKDelegate.paymentFailed(error:).
You are therefore fully in control of the model you wish to use to report
errors. We recommend adopting the
Problem Details for HTTP APIs convention for
reporting errors from your backend. At the moment of writing, the Android SDK
also contains a utility for parsing RFC 7807
messages to help with this.
iOS Payment Menu Redirect Handling
In many cases the payment menu will need to navigate to a different web page as
part of the payment process. Unfortunately, testing has shown that not all such
pages are happy about being opened in a WKWebView. To mitigate this, the SDK
contains a list of pages we know to work, and any others will be opened in
Safari (or whatever browser the user has set as default in recent iOS). If you
wish, you can customize this behavior by overriding
decidePolicyForPaymentMenuRedirect in your configuration. Note that you can
also modify this behavior by the webRedirectBehavior property of
SwedbankPaySDKController.
1
2
3
4
5
6
7
struct MyConfiguration : SwedbankPaySDKConfiguration {
func decidePolicyForPaymentMenuRedirect(navigationAction: WKNavigationAction,
completion: @escaping (SwedbankPaySDK.PaymentMenuRedirectPolicy) -> Void) {
// we like to live dangerously, allow everything
completion(.openInWebView)
}
}
iOS Payment URL Matching
The iOS paymentUrl universal-link/custom-scheme contraption makes it so that
your app must be able to accept some variations in the urls. The default
behavior is to allow for a different scheme and additional query parameters. If
these are not good for your app, you can override the
url(_:matchesPaymentUrl:) method in your configuration. If you wish to simply
specify the allowed custom scheme, you can conform to
SwedbankPaySDKConfigurationWithCallbackScheme instead.
1
2
3
4
5
6
7
struct MyConfiguration : SwedbankPaySDKConfiguration {
func url(_ url: URL, matchesPaymentUrl paymentUrl: URL) -> Bool {
// We trust universal links enough
// so we do not need the custom-scheme fallback
return url == paymentUrl
}
}
1
2
3
struct MyConfiguration : SwedbankPaySDKConfigurationWithCallbackScheme {
let callbackScheme = "com.example.app"
}