Getting started
Add the Android Secure Fields SDK to your project and render a working card form.
Prerequisites
- An Android project targeting SDK 26 or higher
- A Purse account with a tenant ID (available in your Purse dashboard)
Step 1 — Add the dependency
The SDK is published to Maven Central — no repository configuration required. Add the dependency to your app module build.gradle.kts:
dependencies {
implementation("eu.purse:securefields-android:<latest-version>")
}
mavenCentral() is included in Android projects by default. No credentials or additional repository declarations are needed.
Gradle verifies checksums automatically, but GPG signature verification confirms the artifact was signed by Purse and guards against supply chain tampering. For a payment integration this is strongly recommended. Run once in your project:
./gradlew --write-verification-metadata pgp,sha256 help
Commit the generated gradle/verification-metadata.xml. Subsequent builds will reject any artifact whose signature does not match.
Step 2 — Add the Internet permission
Add the permission to your app's AndroidManifest.xml:
<uses-permission android:name="android.permission.INTERNET" />
The SDK makes two network calls: a BIN lookup during card entry and a tokenization request on submit. Both require this permission.
Step 3 — Initialize the SDK
Choose the approach that matches your UI framework:
- Jetpack Compose
- View system
Call rememberSecureFields() inside your composable. It creates the SDK instance and automatically calls destroy() when the composable leaves the composition.
import eu.purse.securefields.config.SecureFieldsConfig
import eu.purse.securefields.config.SecureFieldsFieldsConfig
import eu.purse.securefields.config.VaultEnvironment
import eu.purse.securefields.config.VaultFieldConfig
import eu.purse.securefields.model.Brand
import eu.purse.securefields.rememberSecureFields
@Composable
fun CheckoutScreen() {
val secureFields = rememberSecureFields(
config = SecureFieldsConfig(
brands = listOf(Brand.VISA, Brand.MASTERCARD),
fields = SecureFieldsFieldsConfig(
cardNumber = VaultFieldConfig(placeholder = "Card number"),
expDate = VaultFieldConfig(placeholder = "MM / YY"),
cvv = VaultFieldConfig(placeholder = "CVV"),
)
),
environment = VaultEnvironment.SANDBOX,
tenantId = "${TENANT_ID}",
)
// Continue below — render fields and add a submit button
}
Call SecureFieldsAndroid.init() in onViewCreated(). Pass viewLifecycleOwner so the SDK cleans up when the view is destroyed. Call destroy() in onDestroyView().
import eu.purse.securefields.SecureFieldsAndroid
import eu.purse.securefields.config.SecureFieldsConfig
import eu.purse.securefields.config.SecureFieldsFieldsConfig
import eu.purse.securefields.config.VaultEnvironment
import eu.purse.securefields.config.VaultFieldConfig
import eu.purse.securefields.model.Brand
class CheckoutFragment : Fragment(R.layout.fragment_checkout) {
private lateinit var secureFields: SecureFieldsAndroid
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
secureFields = SecureFieldsAndroid.init(
context = requireContext(),
lifecycleOwner = viewLifecycleOwner,
config = SecureFieldsConfig(
brands = listOf(Brand.VISA, Brand.MASTERCARD),
fields = SecureFieldsFieldsConfig(
cardNumber = VaultFieldConfig(
view = binding.vaultCardNumber,
placeholder = "Card number"
),
expDate = VaultFieldConfig(
view = binding.vaultExpDate,
placeholder = "MM / YY"
),
cvv = VaultFieldConfig(
view = binding.vaultCvv,
placeholder = "CVV"
),
)
),
environment = VaultEnvironment.SANDBOX,
tenantId = "${TENANT_ID}",
)
}
override fun onDestroyView() {
super.onDestroyView()
secureFields.destroy()
}
}
Use VaultEnvironment.SANDBOX during development. Switch to VaultEnvironment.PRODUCTION for live traffic. Drive this from your build configuration:
environment = if (BuildConfig.DEBUG) VaultEnvironment.SANDBOX else VaultEnvironment.PRODUCTION
| Value | Endpoint |
|---|---|
VaultEnvironment.SANDBOX | https://api.vault.purse-sandbox.com |
VaultEnvironment.PRODUCTION | https://api.vault.purse-secure.com |
Before going live, set FLAG_SECURE on your checkout Activity to prevent the card form from being captured by the Android task switcher or screen recording apps. The SDK cannot set this for you — it only warns via onSecurityWarning() if you forget. See Security and compliance — Enable FLAG_SECURE.
Step 4 — Render the card form
- Jetpack Compose
- View system
Use VaultForm to render all configured fields in a pre-built layout. Give it an explicit height that fits your active fields (e.g. 3 × 48dp + 2 × 12dp gaps ≈ 168dp).
import eu.purse.securefields.VaultForm
@Composable
fun CheckoutScreen() {
val secureFields = rememberSecureFields(config = ...)
LaunchedEffect(secureFields) {
secureFields.render()
}
VaultForm(
sdk = secureFields,
modifier = Modifier.fillMaxWidth()
)
}
VaultForm renders the whole card form as a single component. If you need to lay out individual fields in your own arrangement, render each one with the VaultField composable instead — pass it a field controller from the SDK instance (secureFields.cardNumber, .expDate, .cvv, .holderName):
import eu.purse.securefields.VaultField
@Composable
fun CheckoutScreen() {
val secureFields = rememberSecureFields(config = ...)
LaunchedEffect(secureFields) {
secureFields.render()
}
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
VaultField(secureFields.cardNumber, Modifier.fillMaxWidth().height(48.dp))
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
VaultField(secureFields.expDate, Modifier.weight(1f).height(48.dp))
VaultField(secureFields.cvv, Modifier.weight(1f).height(48.dp))
}
}
}
The controllers are populated by render(), so call it before the fields display anything. Set an explicit height on each VaultField — it does not auto-size. Don't render the same field with both VaultForm and VaultField; a native field view can only be attached to one parent.
Declare VaultFieldView elements in your XML layout. The SDK fills them with secure inputs when you call render().
<!-- res/layout/fragment_checkout.xml -->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<eu.purse.securefields.VaultFieldView
android:id="@+id/vaultCardNumber"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_marginBottom="12dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="12dp">
<eu.purse.securefields.VaultFieldView
android:id="@+id/vaultExpDate"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="48dp"
android:layout_marginEnd="8dp" />
<eu.purse.securefields.VaultFieldView
android:id="@+id/vaultCvv"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="48dp" />
</LinearLayout>
<Button
android:id="@+id/payButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="Pay" />
</LinearLayout>
Call render() after init():
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
// ... init() call from Step 3 ...
secureFields.render()
}
Step 5 — Submit and handle the result
Call submit() when the user taps Pay. It validates the form and tokenizes the card data. submit() is a suspend function — call it from a coroutine.
- Jetpack Compose
- View system
@Composable
fun CheckoutScreen() {
val scope = rememberCoroutineScope()
val secureFields = rememberSecureFields(config = ...)
var formValid by remember { mutableStateOf(false) }
LaunchedEffect(secureFields) {
secureFields.render()
secureFields.addListener(object : SecureFieldsListener {
override fun onFormValid(payload: FormValidPayload) {
formValid = !payload.hasErrors
}
})
}
Button(
enabled = formValid,
onClick = {
scope.launch {
when (val result = secureFields.submit()) {
is SubmitResult.Success -> onTokenReceived(result.vaultFormToken)
is SubmitResult.Error -> showError(result.message)
}
}
}
) { Text("Pay") }
VaultForm(sdk = secureFields, modifier = Modifier.fillMaxWidth())
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
// ... init() and render() ...
secureFields.addListener(object : SecureFieldsListener {
override fun onFormValid(payload: FormValidPayload) {
binding.payButton.isEnabled = !payload.hasErrors
}
})
binding.payButton.setOnClickListener {
lifecycleScope.launch {
binding.payButton.isEnabled = false
when (val result = secureFields.submit()) {
is SubmitResult.Success -> navigateToConfirmation(result.vaultFormToken)
is SubmitResult.Error -> {
showSnackbar(result.message)
binding.payButton.isEnabled = true
}
}
}
}
}
vaultFormToken is an opaque server-side token. Pass it to your backend and use it to create a payment via the Payment API. Do not attempt to decode or store the raw token client-side.
Next steps
- Events and tokenization — full event reference, brand detection, error handling
- Customize — apply styles and update field config at runtime
- Security and compliance — required hardening for production