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"
}