Skip to main content

Getting started

This tutorial takes you from an empty Android Studio project to a card payment in the Purse sandbox. At the end you will have a screen that lists the session's payment methods, renders the secure card form, pays, follows 3-D Secure and shows the outcome.

  1. Prerequisites: a sandbox client session and an empty Compose project.
  2. Install the SDK.
  3. Create the checkout in a ViewModel.
  4. List the payment methods.
  5. Render the card form and pay.
  6. Read the outcome, including after 3-D Secure.
  7. Check the payment in the sandbox.
Scope of this tutorial

Card only, no partner artifact. Google Pay, PayPal, Klarna, redirection methods, gift cards and saved methods each have their own guide under Payment methods.

Step 1 - Prerequisites​

A sandbox client session. The SDK never talks to the Purse API on its own behalf: your backend creates a client session and hands the app the base64 widget.data string it gets back. Follow Your first payment with Purse against https://api.purse-sandbox.com with your sandbox credentials, using an entity where a card method vaulted by Purse is activated. Keep the widget.data value: for this tutorial you paste it into the app.

An empty project. In Android Studio, create an Empty Activity project (Compose). Check app/build.gradle.kts:

android {
defaultConfig { minSdk = 26 }
buildFeatures { compose = true }
}

Step 2 - Install​

The SDK is on Maven Central under the eu.purse group. One artifact carries the engine, the payment elements and the card and redirection methods. Google Pay, Klarna and PayPal each bring a partner SDK, so they are separate artifacts you add only if you offer them. This tutorial needs only the first:

// app/build.gradle.kts
dependencies {
implementation("eu.purse:headless-checkout-android:${SDK_VERSION}")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.10.0")
}

SDK_VERSION is the latest version of eu.purse:headless-checkout-android on Maven Central. Everything the method needs ships in the artifact's own manifest: the INTERNET permission and the Activity that hosts 3-D Secure. R8 needs no rules from you, and your IDE shows the SDK's documentation from the published javadoc jar.

Step 3 - Create the checkout​

Hold the checkout in a ViewModel so a rotation never rebuilds it. Paste your widget.data string in place of the placeholder; a real app receives it from its backend.

import androidx.lifecycle.ViewModel
import eu.purse.checkout.PurseHeadlessCheckout
import eu.purse.core.environment.PurseEnvironment

class CheckoutViewModel : ViewModel() {
val checkout: PurseHeadlessCheckout = PurseHeadlessCheckout.init(
widgetData = WIDGET_DATA, // from your backend
lockedEnvironment = PurseEnvironment.SANDBOX, // release builds pass PRODUCTION
)

override fun onCleared() = checkout.teardown()
}

const val WIDGET_DATA = "eyJ...." // the client session's widget.data

init decodes the session and throws PurseSessionException.SessionMalformed if the string is not a client session. lockedEnvironment pins the environment the SDK talks to: a release build must pass PurseEnvironment.PRODUCTION so that a session minted against the wrong environment cannot redirect the app. teardown() releases the SDK's resources when the screen is dismissed for good.

Step 4 - List the payment methods​

Reactive state is exposed as StateFlow. In Compose, collect it with collectAsState():

import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.*
import androidx.lifecycle.viewmodel.compose.viewModel
import eu.purse.checkout.payment.item.PaymentItem
import eu.purse.checkout.payment.item.PrimaryMethod

@Composable
fun CheckoutScreen(viewModel: CheckoutViewModel = viewModel()) {
val checkout = viewModel.checkout
val methods by checkout.paymentMethods.collectAsState()
var selected by remember { mutableStateOf<PaymentItem?>(null) }

Column {
methods.filterIsInstance<PrimaryMethod>().forEach { method ->
MethodRow( // your own row composable
label = method.id, // "purse-creditcard", "paypal-wallet", …
iconUrl = checkout.iconUrl(method), // CDN SVG of the method's logo
enabled = method.disabled == null, // see Method states
selected = method.id == selected?.id,
onClick = { selected = method },
)
}
selected?.let { SelectedMethod(checkout, it) } // step 5
}
}

paymentMethods already excludes methods the session suspended or that cannot reach the amount. The disabled state of the remaining ones carries a code and a displayable message, and moves as the split moves.

Step 5 - Render the card form and pay​

One composable renders any method: rememberPaymentElement wires the native SDK, its launchers and its /init for you, PaymentElementUi places its UI, and submit() pays.

import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import eu.purse.checkout.PurseHeadlessCheckout
import eu.purse.checkout.element.PaymentElementUi
import eu.purse.checkout.element.rememberPaymentElement
import eu.purse.checkout.payment.item.PaymentItem
import eu.purse.checkout.payment.item.PrimaryMethod
import eu.purse.partnerui.PaymentElementListener
import eu.purse.partnerui.PaymentOutcome

@Composable
fun SelectedMethod(checkout: PurseHeadlessCheckout, item: PaymentItem) {
val element = rememberPaymentElement(checkout, item, listener = object : PaymentElementListener {
override fun onOutcome(outcome: PaymentOutcome) { /* step 6 */ }
})
LaunchedEffect(item.id) {
if (item is PrimaryMethod) checkout.setPrimary(item) // the method pays the whole amount
}

PaymentElementUi(element, Modifier.fillMaxWidth()) // the secure card form
if (!element.selfSubmitting) {
Button(onClick = { element.submit() }, enabled = element.ready) { Text("Pay") }
}
}

The card form is the Android Secure Fields form: card number, expiry, CVV and holder. Card data never reaches your code. element.ready turns true only once every field validates, so the Pay button cannot submit an incomplete form. submit() tokenizes the card, runs /init and /validate, then follows 3-D Secure in an Activity the SDK owns.

Run the app, select the card method and pay with a test card from your PSP's sandbox documentation.

Other methods

The same two calls render every method. Google Pay and PayPal are selfSubmitting: their branded button is the Pay button, and they select themselves as primary. Redirection methods and saved cards without CVV have no UI (hasUi is false): only your Pay button shows. Each method's guide says what differs.

Step 6 - Read the outcome​

onOutcome receives a sealed PaymentOutcome, one per attempt. Use it to update the shopper's screen. In SelectedMethod from step 5:

val element = rememberPaymentElement(checkout, item, listener = object : PaymentElementListener {
override fun onOutcome(outcome: PaymentOutcome) {
when (outcome) {
is PaymentOutcome.Completed -> // /validate answered directly
if (outcome.isSuccess) showSuccess() else showDeclined(outcome.status)
PaymentOutcome.ReturnedFromRedirect -> showProcessing() // below
PaymentOutcome.Canceled -> Unit // the shopper backed out, nothing changed
is PaymentOutcome.Failed -> showError(outcome.error.code) // a PurseException with a stable code
}
}
})

showSuccess, showDeclined, showProcessing, showError and the other show… calls on this page stand for your own UI code, like MethodRow in step 4.

  • Completed: /validate answered directly. status is the API's verdict. PaymentStatus.SUCCESS is a payment, anything else a declined payment.
  • ReturnedFromRedirect: the shopper came back from 3-D Secure. That is not an outcome: show a processing state and get the final status from your backend (step 7).

A Failed before any submit is not a failed payment: keep the shopper on the screen with a message. The Error handling guide covers each code.

Step 7 - Check the payment​

The app's outcome is for the shopper's screen. Confirm the payment where your business logic lives:

One session pays once. To pay again, create a new client session and either build a new checkout or hand the new widget.data to the existing one with checkout.setSession(widgetData).

Your basic checkout is operational

Your backend remains the source of truth for fulfilment: confirm the payment with webhooks or with Get a client session, server-side, never from the app alone.

Where to go from here​

  • Key concepts: the model behind what you just wired.
  • Payment methods: one guide per method, with the partner setup each needs.
  • Sessions: refresh a session, survive process death, tear down.
  • Run the sample app: every method, gift cards and saved cards in a complete app.