Initialization
To use the SDK, you must have a valid Configuration
for it. The API for this
is a bit different in Android and iOS, but generally you will only need one
Configuration
for your app. On Android set it to
PaymentFragment.defaultConfiguration
; on iOS store it in a convenient
variable.
If you are using a backend implementing the Merchant Backend API, there is a Configuration ready for you in the SDK. If you are designing your own backend API, you will need to create the Configuration yourself.
sequenceDiagram
alt Using Merchant Backend
alt Android
App ->> SDK: MerchantBackendConfiguration.Builder(backendUrl)...build()
SDK -->> App: configuration
App ->> SDK: PaymentFragment.defaultConfiguration = configuration
else iOS
App ->> SDK: SwedbankPaySDK.MerchantBackendConfiguration.init(backendUrl: backendUrl)
SDK -->> App: configuration
end
else Using Custom Backend
alt Android
App ->> App: class MyConfiguration : Configuration()
App ->> SDK: PaymentFragment.defaultConfiguration = configuration
else iOS
App ->> App: struct/class MyConfiguration : SwedbankPaySDK.Configuration
end
end
When you want to make a payment, create a PaymentFrament
or
SwedbankPaySDKController
. SwedbankPaySDKController
’s designated initializer
takes all the required arguments; for PaymentFrament
you should use
PaymentFragment.ArgumentsBuilder
to create the arguments bundle.
The meaning of the arguments depends on your Configuration. If you are using
MerchantBackendConfiguration
, you will always need a PaymentOrder
argument.
A very important part of the PaymentOrder
is the urls
field. The SDK has
convenience methods for creating one; unless your use-case is advanced, you
should use these. On Android use the PaymentOrderUrls(context, backendUrl,
[callbackUrl], [termsOfServiceUrl])
constructor; on iOS use the
PaymentOrderUrls.init(configuration:language:[callbackUrl:][termsOfServiceUrl:])
initializer. In both cases the convenience method depends on your
MerchantBackendConfiguration
(backendUrl
is part of the
MerchantBackendConfiguration
), so be careful if you have multiple
MerchantBackendConfigurations in your app.
Additionally, you may construct a Consumer
if you wish to identify the payer.
This enables saving payment details for further payments.
If you are using a custom Configuration, you may use the PaymentOrder
or
Consumer
arguments if you wish. Additionally you can use a generic data
argument (of type Any
, though on Android it must implement either Parcelable
or Serializable
). By default, the existence of the Consumer
argument
controls whether the consumer identfication happens, but you can also specify it
explicitly.
sequenceDiagram
participant User
participant App
participant SDK
User ->> App : Begin payment
rect rgba(138, 205, 195, 0.1)
note right of App: Build Payment Order
alt Android
App ->> SDK: PaymentOrder(...) or PaymentOrder.Builder()...build()
SDK -->> App: paymentOrder
else iOS
App ->> SDK: SwedbankPaySDK.PaymentOrder.init(...)
SDK -->> App: paymentOrder
end
end
opt Build Consumer
alt Android
App ->> SDK: Consumer(...)
SDK -->> App: consumer
else iOS
App ->> SDK: Consumer.init(...)
SDK -->> App: consumer
end
end
rect rgba(138, 205, 195, 0.1)
note right of App: Create and configure payment UI component
alt Android
App ->> SDK: PaymentFragment.ArgumentsBuilder()...build()
SDK -->> App: arguments
App ->> SDK: PaymentFragment()
SDK -->> App: paymentFragment
App ->> SDK: paymentFragment.arguments = arguments
App ->> SDK: activity.paymentViewModel.[rich]state.observe(...)
else iOS
App ->> SDK: SwedbankPaySDKController.init(...)
SDK -->> App: swedbankPaySDKController
App ->> SDK: swedbankPaySDKController.delegate = ...
end
end
App ->> App : Show payment UI component
Merchant Backend: Discover Endpoints
This is an implementation detail of the Merchant Backend configuration; it is not necessary to replicate this step in a your own systems.
The Merchant Backend is specified with a single static entry point; other
interfaces are accessed by following links from previous responses. A request to
the static entry point currently returns links to the consumers
and
paymentorders
endpoints. In most cases the response to this request can be
cached, and thus only needs to be made once per App session.
sequenceDiagram
participant SDK
participant Backend
SDK ->> Backend: GET /
Backend -->> SDK: { "consumers": "[consumers]", "paymentorders": "[paymentorders]" }
Optional Checkin
Optionally, the payment beings with a “checkin” flow, where the payer is identified. This allows for saving payment details for later payments.
The checkin flow is simple: first a request is made to begin a checkin session,
then an html page is constructed using the script link received from that
request, and when that page successfully identifies the payer, a javascript
callback is received. The consumerProfileRef
received from that callback is
then used when creating the payment order in the next step.
sequenceDiagram
participant Conf as Configuration
participant SDK
participant Backend
participant SwedbankPay as Swedbank Pay
participant WebView
participant User
SDK ->> Conf: postConsumers
alt Merchant Backend
Conf ->> Backend: POST [consumers] { "operation": "initiate-consumer-session", ... }
else Custom Backend
Conf ->> Backend: Custom protocol
end
Backend ->> SwedbankPay: POST /psp/consumers/ { "operation": "initiate-consumer-session", ... }
SwedbankPay -->> Backend: { "operations": [{ "rel": "view-consumer-identification", "href": "[consumer-script]" }] }
alt Merchant Backend
Backend -->> Conf: { "operations": [{ "rel": "view-consumer-identification", "href": "[consumer-script]" }] }
else Custom Backend
Backend -->> Conf: Custom protocol
end
Conf --> SDK: ViewConsumerIdentificationInfo
SDK ->> WebView: <html>...<script src="[consumer-script]">...payex.hostedView.consumer(...)...</html>
WebView ->> User: Show checkin UI
User ->> WebView: Enter personal details
WebView ->> SDK: onConsumerIdentified({ "consumerProfileRef" : "..." })
SDK ->> SDK: store consumerProfileRef for checkout
Begin Checkout
With the Payment Order ready, the SDK begins the “checkout” flow, where the
actual payment is made. The checkout flow begins similarly to the checkin flow:
a request is made to create a Payment Order, then an html page is constructed
and displayed to the user. In the case of the “create Payment Order” request,
however, it is expected that the Merchant Backend processes the request and
response: Setting of payeeId
and paymentReference
in particular seems better
left to the backend; similarly the backend is probably interested in storing the
id
of the created Payment Order for capture and other possible operations.
At this point the user is interacting with the payment menu; the next step depends on the exact payment instrument chosen.
sequenceDiagram
participant Conf as Configuration
participant SDK
participant Backend
participant SwedbankPay as Swedbank Pay
participant WebView
participant User
SDK ->> Conf: postPaymentorders
alt Merchant Backend
Conf ->> Backend: POST [paymentorders] { paymentorder: {...} }
else Custom Backend
Conf ->> Backend: Custom protocol
end
Backend ->> Backend: Preprocess payment order (e.g. create payeeReference)
Backend ->> SwedbankPay: POST /psp/paymentorders/ { paymentorder: {...} }
SwedbankPay -->> Backend: { "id": "...", "operations": [{ "rel": "view-paymentorder", "href": "[paymentorder-script]" }], ... }
Backend ->> Backend: Postprocess payment order (e.g. store id)
alt Merchant Backend
Backend -->> Conf: { "id": "...", "operations": [{ "rel": "view-paymentorder", "href": "[paymentorder-script]" }], ... }
else Custom Backend
Backend ->> Conf: Custom protocol
end
Conf -->> SDK: ViewPaymentOrderInfo
SDK ->> WebView: <html>...<script src="[paymentorder-script]">...payex.hostedView.paymentMenu(...)...</html>
WebView ->> User: Show checkout UI
User ->> WebView: Choose payment method and enter details
External Content
While some payments may be completed inside the payment menu in their entirety,
others will require interaction with some external web page, and/or application.
In most cases external web pages can be opened in the same web view used to show
the payment menu, and when they want to return to the payment menu, they signal
this by attempting to navigate to the url set as paymentUrl
in the Payment
Order. We intercept this navigation, and reload the payment menu, as
appropriate.
When an external application is launched, the flow signals the return to the
payment menu by again opening paymentUrl
. This time, however, we cannot
intercept it. The system then handles opening that url the usual way. For
maximum compatibility, paymentUrl
is a regular https url. On iOS, paymentUrl
is designed to be in format that is registered as a Universal
Link to the app, which causes the system to open
paymentUrl
in the app. The example backend serves a
/.well-known/apple-app-site-association
file that assigns the paths under
/sdk-callback/
to be Universal Links to the application set in the
configuration. The SDK defaults to building paymentUrl
under this path.
Combined with the proper configuration in the app and backend, this makes
paymentUrl
s be Universal Links. On Android 6.0 and later it is possible to do
a similar thing, but it is much more difficult to set up on the server side, and
we need a solution for earlier versions anyway. Therefore, on Android,
paymentUrl
will be opened in the browser.
Finally, in our testing, we have seen that certain external pages used with
certain payment instruments do not work correctly inside a web view, and must be
shown in the browser instead. If we determine that the external page is one of
these pages, it is opened in the browser. Again, return to the payment menu is
signaled by a navigation to paymentUrl
, which will, in this case be opened in
the browser on both platforms (but see below for iOS details).
sequenceDiagram
participant App
participant SDK
participant WebView
participant System
participant Browser
participant Ext as External App
participant User
WebView ->> SDK: Navigate to another page
alt Navigation is to a regular http(s) URL
SDK ->> SDK: Check web view compatibility
alt Compatible with Web View
SDK ->> WebView: Proceed with navigation normally ①
WebView ->> SDK: Navigate to paymentUrl ②
SDK ->> SDK: Recognize paymentUrl
SDK ->> WebView: Cancel navigation
else Not Compatible with Web View
SDK ->> WebView: Cancel navigation
SDK ->> System: Open URL
System ->> Browser: Open URL in Browser
User ->> Browser: Handle external flow
Browser ->> User: Handle external flow
Browser -> Browser: Open paymentUrl in Browser
end
else Navigation is to an app-specific URL (custom scheme, Android Deep Link/App Link, iOS Universal Link)
SDK ->> WebView: Cancel navigation
SDK ->> System: Open URL
System ->> Ext: Launch URL-appropriate app
User ->> Ext: Handle external flow
Ext ->> User: Handle external flow
Ext ->> System: Open paymentUrl
alt Android
System ->> Browser: Open paymentUrl in Browser ③
else iOS
System ->> App: Launch app with paymentUrl ④
end
end
- ① The same check is repeated for any further navigation inside the WebView
- ② All properly configured authentication flows must end up here
- ③ On Android, paymentUrl is an https URL that redirects to an Android Intent URL.
- ④ On iOS, paymentUrl is a Universal Link. When an app open a Universal Link to another app, it should be routed to that app instead of the Browser. However, Universal Links are finicky things, and it is not impossible that it gets opened in the Browser instead. In that case, the flow continues with “paymentUrl opened in Browser” below instead.
Return From Browser
If the external flow ended with paymentUrl
opened in the browser, we need a
way to get back to the app. On Android, this is simple to accomplish by
redirecting to an Android Intent Uri; the SDK and
backend work together to construct the Intent Uri to point to the correct app.
This Intent will cause the app to be brought back into focus, and the
PaymentFragment will recognize the paymentUrl
and reload the payment menu. We
still need to have an actual html page served at paymentUrl
, though, as the
redirect may be blocked in some scenarios. If that happens, the page will also
contain a link the user can tap on, which opens the same Intent Uri.
On iOS, the situation is more complicated. As mentioned above, paymentUrl
is a
Universal Link, and navigation to it should be routed to the app. However,
Universal Links are a bit unreliable, in that they require certain conditions to
be fulfilled; otherwise, they are opened in the browser like regular links.
Unfortunately, one of the conditions, namely that the navigation originated from
the user pressing on a link, is often not fulfilled in the external pages used
by payment methods. Therefore, we must have a system that works correctly, even
if paymentUrl
is opened in the browser.
On iOS, we use the following system:
-
paymentUrl
redirects (301) to a trampoline page hosted at a different domain - the trampoline page has a button
- pressing that button navigates to
paymentUrl
but with an extra parameter -
paymentUrl
with the extra parameter redirects to a custom-scheme url
The trampoline page is on a different domain due to another requirement of
Universal Links: they are only routed to the app if opened from a different
domain. Now, both paymentUrl
and paymentUrl
with the extra parameter are
Universal Links, and as the second navigation is “forced” to originate from User
Interaction, it should be routed to the app. However, if something still goes
sideways, and experience says it can, and even this “augmented” paymentUrl
is
opened in the browser, then we finally redirect to a custom-scheme url, which
has no choice but to be routed to the app. The reason we do not do this
immediately is because using custom schemes triggers a confirmation dialog the
developer has no control over, and we want to avoid that.
When the app is then launched with paymentUrl
, the augmented paymentUrl
, or
the custom-scheme url constructed from paymentUrl
, the Application Delegate
must then forward the url to the SDK using the SwedbankPaySDK.open(url:) or
SwedbankPaySDK.continue(userActivity:) method, as the case may be. The SDK will
then proceed to reload the payment menu as appropriate.
This system does have the drawback of requiring a custom url scheme, which will almost always be left unused. As we gather more data, we may be able to remove the requirement in the future.
Please see this diagram for an illustration of the different steps in the process:
sequenceDiagram
participant User
participant Browser
participant Backend
participant Trampoline as Universal Link Trampoline
participant System
participant SDK
participant App
alt Android
Browser ->> Merchant: Load paymentUrl
Merchant -->> Browser: Html document that redirects to an Intent URL ⑤
Browser ->> Browser: Parse Android Intent URL
Browser ->> System: Start activity with the parsed Intent, where the Intent Uri is paymentUrl
System ->> SDK: Start callback activity
SDK ->> SDK: Recognize paymentUrl
else iOS
alt Happiest Path
Browser ->> Browser: Recognize that paymentUrl is a Universal Link for App
Browser ->> System: Launch app with paymentUrl ⑥
else Less Happy Path
Browser ->> Merchant: Load paymentUrl
Merchant -->> Browser: 301 Moved Permanently ⑦
Browser ->> Trampoline: Load trampoline page
Browser ->> User: Show trampoline page ⑧
User ->> Browser: Press "Return to App" button
Browser ->> Browser: Navigate to paymentUrl&fallback=true
alt Less Happy Path (contd.)
Browser ->> Browser : Recognize that paymentUrl&fallback=true is a Universal Link for App
Browser ->> System : Launch app with paymentUrl&fallback=true
else Sad Path ⑨
Browser ->> Merchant: Load paymentUrl&fallback=true
Merchant -->> Browser: 301 Moved Permanently ⑩
Browser ->> User: Show confirmation dialog
User ->> Browser: Accept app launch
Browser ->> System: Launch app with customscheme://[paymentUrl-without-scheme]&fallback=true
end
end
System ->> App: Call URL handler ⑪
App ->> SDK: SwedbankPaySDK.open(url:) or SwedbankPaySDK.continue(userActivity:)
SDK ->> SDK: Recognize paymentUrl or modified paymentUrl
end
- ⑤
intent://[paymentUrl-without-scheme]/#Intent;scheme=[paymentUrl-scheme];action=com.swedbankpay.mobilesdk.VIEW_PAYMENTORDER;package=[app-package];end;
- ⑥ Universal Links have certain conditions for them to be activated. One of these is that the navigation must have started from a user interaction. As many 3D-Secure pages have an automatic redirect, this can cause the link to be opened in the Browser instead. Therefore the chance for this path to be taken is low. (N.B. It does seem than iOS 13.4 has made some change to the logic, causing this happiest path to be hit more often.)
- ⑦ Location:
https://ecom.stage.payex.com/externalresourcehost/trampoline?target=paymentUrl%26fallback=true
- ⑧ The “Trampoline Page” has a button, which links back to paymentUrl, but with an additional query parameter (actually this extra parameter is added by the backend when generating the redirect to the trampoline page). Importantly, the Trampoline is on a different domain than paymentUrl, as Universal Links are only forwarded to the app if they are opened from a different domain than the link’s domain.
- ⑨ All cases should be caught by one of these two flows. However, Universal Links remain finicky, and therefore it is good to provide one final fallback.
- ⑩ Location:
customscheme://[paymentUrl-without-scheme]&fallback=true
.customscheme
is a URL scheme unique to the App. - ⑪ Universal links result in a call to
UIApplicationDelegate.application(_:continue:restorationHandler:)
, while custom-scheme links result in a call toUIApplicationDelegate.application(_:open:options:)
.
Payment Completion
When the payment is completed, possibly after reloading the payment menu after a
navigation to paymentUrl
, the payment menu will report success by attempting
to navigate to completeUrl
. The SDK intercepts this and invokes a callback to
your app. It is your app’s responsibility to then remove the payment UI from
view and notify the user. Similarly, if the payment is cancelled, the SDK
intercepts the navigation to cancelUrl
and reports the cancellation status to
your app.
sequenceDiagram
participant User
participant App
participant SDK
participant WebView
SDK ->> WebView: Reload <html>...<script src="[paymentorder-script]">...payex.hostedView.paymentMenu(...)...</html>
WebView ->> SDK: Navigate to completeUrl
SDK ->> WebView: Cancel navigation
alt Android
SDK ->> SDK: PaymentViewModel.state <- SUCCESS
SDK ->> App: Observer<PaymentViewModel.State>.onChanged(SUCCESS)
else iOS
SDK ->> App: SwedbankPaySDKDelegate.paymentComplete()
end
App ->> App: Remove payment UI component
App ->> User: Report payment result