Headless Universal Checkout


Overview

Headless Universal Checkout is currently in beta for iOS and Android.

Where there is a need for more customization and control over the checkout experience, a headless version of Primer’s Universal Checkout is available.

Universal Checkout can be used with your own UI, giving you more flexibility and allowing you to move quicker when making design changes, while still having Universal Checkout capture sensitive PCI card data.

As with all versions of Universal Checkout, any payment method can be added and configured through Dashboard, meaning Primer handles the logic of when to display each method.

Headless Universal Checkout is currently available in beta on Android version 1.10.0.

Step 1. Install

Add the following to your app/build.gradle file

1234567
repositories {  mavenCentral()} dependencies {  implementation 'io.primer:android:latest.version'}
kotlin
copy

For more details about SDK versions, please see our changelog.

Step 2. Prepare the Checkout Listener

12345678910111213141516171819202122232425
class CheckoutActivity : AppCompatActivity() {  private val listener = object : PrimerHeadlessUniversalCheckoutListener {         override fun onClientSessionSetupSuccessfully(paymentMethods: List<PrimerHeadlessUniversalCheckoutPaymentMethod>) {            /* TODO */        }         override fun onTokenizationSuccess(            paymentMethodToken: PaymentMethodToken,            resumeHandler: ResumeHandler        ) {            /* TODO */        }         override fun onResumeSuccess(resumeToken: String, resumeHandler: ResumeHandler) {            /* TODO */        }         override fun onError(error: APIError) {            /* TODO */        }            // handle other events    }}
kotlin
copy

Step 3. Generate a Client Token

Check our guide on how to set up the client session here

❗️
Remember that based on your client token different payment methods will show up.

Make an API call to your backend to fetch a Client Token. Here is a simple example of how it can be done from your activity:

12345678910111213141516171819202122232425262728
class CheckoutActivity : AppCompatActivity() {   // other code goes here  private lateinit var viewModel: CheckoutViewModel   override fun onCreate(savedInstanceState: Bundle?) {    super.onCreate(savedInstanceState)    setContentView(R.layout.activity_main)     setupViewModel()    setupObservers()    fetchClientToken()  }    private fun setupViewModel() {    viewModel = ViewModelProvider(this).get(CheckoutViewModel::class.java)  }   // prepare the client  private fun fetchClientToken() = viewModel.fetchClientToken()   private fun setupObservers() {    viewModel.clientToken.observe(this) { clientToken ->      // show checkout    }  }}
kotlin
copy

Your view model code may look something like this:

12345678910
class CheckoutViewModel : ViewModel() {     private val _clientToken= MutableLiveData<String>()    val clientToken: LiveData<String> = _clientToken     fun fetchClientToken() {    // fetch your token here (ask your backend to provide token)       _clientToken.postValue("retrieved_token")    }}
kotlin
copy

Step 4. Start Headless Universal Checkout

Once you have a client token, you can initialize Primer’s Headless Universal Checkout like this:

12345678910111213141516171819202122
class CheckoutActivity : AppCompatActivity() {   // other code goes here    private val listener = object : PrimerHeadlessUniversalCheckoutListener {        // ...    }   private fun setupObservers() {    viewModel.clientToken.observe(this) { clientToken ->      startHeadlessUniversalCheckout(clientToken)    }  }   private fun startHeadlessUniversalCheckout(clientToken: String) {    PrimerHeadlessUniversalCheckout.current.start(            this,            clientToken,            PrimerConfig(),            listener        )  }}
kotlin
copy

Once Primer’s Headless Universal Checkout is configured, its listener functions will notify you about Headless Universal Checkout events.

Additionally, you can pass the listener as an argument in the start function, or set it with PrimerHeadlessUniversalCheckout.current.setListener(listener)

Step 5: Show (Alternative) Payment Methods

When the checkout is done initializing, the onClientSessionSetupSuccessfully method in the listener will be invoked. Use this event to show the list of payment methods to the user. You can add your buttons for them, or use Primer SDK’s buttons.

12345678910111213141516171819202122232425262728293031
private val listener = object : PrimerHeadlessUniversalCheckoutListener {   override fun onClientSessionSetupSuccessfully(        paymentMethods: List<PrimerHeadlessUniversalCheckoutPaymentMethod>    ) {        paymentMethods.forEach { setupView(it) }  }} // your custom method to render list of payment method buttons UI.private fun setupView(paymentMethod: PrimerHeadlessUniversalCheckoutPaymentMethod) {  val headlessUniversalCheckout = PrimerHeadlessUniversalCheckout.current    val containerView = findViewById<ViewGroup>(R.id.container)    val view = PrimerHeadlessUniversalCheckout.current.makeView(paymentMethod.paymentMethodType)     view?.apply {      containerView.addView(this)        setOnClickListener {          // for card, render your own UI, see more details below.          if (paymentMethod.paymentMethodType == PrimerPaymentMethodType.PAYMENT_CARD) {            createForm()          }           // for all payment methods          headlessUniversalCheckout.showPaymentMethod(              this.context,              paymentMethod.paymentMethodType              )        }    }}
kotlin
copy
Payment methods are added and configured through Dashboard. Only payment methods whose conditions match the current client session will be returned.

Handle onPaymentMethodShowed Callback (Optional)

This function will notify you that the payment method you requested to show (e.g. Google Pay) has been showed.

12345
private val listener = object : PrimerHeadlessUniversalCheckoutListener {     override fun onPaymentMethodPresented() {    }}
kotlin
copy

Step 6. Show Card Form

Headless Universal Checkout securely captures payment method data while fully embedded in your app. By communicating directly with Primer's PCI-L1 tokenization service, Headless Universal Checkout transforms sensitive customer data into a secure uniform string called a payment method token.

If the user wants to pay with card you should avail a card form using the following three steps.

Render Input Elements

First, use the PrimerCardManager object and render the card form input elements like this:

1234567891011121314151617181920
class CheckoutActivity : AppCompatActivity() {   private val cardManager by lazy { PrimerCardManager.newInstance() }     private fun createForm() {        val elementTypes = cardManager.getRequiredInputElementTypes().orEmpty()     val inputElements = elementTypes.map { type ->        PrimerEditTextFactory.createFromType(this, type).apply {            setPrimerInputElementListener(inputElementListener)        }    }     val cardFormView = findViewById<ViewGroup>(R.id.cardForm)     inputElements.forEach { cardFormView.addView(it) }     cardManager.setInputElements(inputElements)  }}
kotlin
copy

The returned elements are of EditText type so you can do any type of customization on them. Additionally you can include them in the xml files if you need it.

Subscribe to Validation Listener

Then, listen to the card form validation state and enable your form’s submit button accordingly:

1234567891011
// val submitButton = findViewById<Button>(R.id.pay_with_card) private val cardManagerListener = object : PrimerCardManagerListener {  override fun onCardValidationChanged(isCardFormValid: Boolean) {    submitButton.isEnabled = isCardFormValid  }} private fun setupListeners() {    cardManager.setCardManagerListener(cardManagerListener)}
kotlin
copy

Handle Input Updates (Optional)

When displaying a card form, you may want to trigger UI changes based on the updates and/or validation status of an active input field. To achieve this please implement the following listener interface and pass it to the input field:

12345678910111213141516
val inputListener = object : PrimerInputElementListener {     override fun inputElementValueChanged(inputElement: PrimerInputElement) {            /* TODO */    }     override fun inputElementValueIsValid(inputElement: PrimerInputElement, isValid: Boolean) {            /* TODO */    }    override fun inputElementDidDetectCardType(type: CardType.Type) {            /* TODO */    }} cardNumberField.setListener(inputLister)
kotlin
copy

Configure Submit Button

Finally, configure the on click listener of the submit button:

1
submitButton.setOnClickListener { cardManager.tokenize() }
kotlin
copy

Handle onPreparationStarted Callback (Optional)

This function will notify you that the tokenization preparation has started. At this point the SDK has not yet started to communicate with Primer's backend. It may be useful in case you want to show a loader or notify user about progress in any other way.

12345
private val listener = object : PrimerHeadlessUniversalCheckoutListener {     override fun onPreparationStarted() {    }}
kotlin
copy

Handle onTokenizationStarted Callback (Optional)

This function will notify you that the tokenization API call has been fired. It may be useful in case you want to show a loader or notify user about progress in any other way.

12345
private val listener = object : PrimerHeadlessUniversalCheckoutListener {     override fun onTokenizationStarted() {    }}
kotlin
copy

Enable 3DS (Optional)

The HUC supports 3DS out of the box, but some configuration is needed to be able to use it.

Installation Guide

3D Secure on Android requires the addition of the io.primer:3ds-android library to your project. This library is currently held on a Primer’s artifactory.

❗️
If the library is not setup, 3DS will fail. In the case of a 3DS triggered by the workflow, the resumeError event will be fired.
  1. 1
    Add the URL to our artifactory to your gradle.properties
1
PRIMER_ANDROID_ARTIFACTORY_URL=https://primer.jfrog.io/artifactory/primer-android/
gradle
copy
  1. 3
    Amend the repositories section of your app's build.gradle to include our artifactory
1234567
repositories {  /* Other repositories... */   maven {    url "${PRIMER_ANDROID_ARTIFACTORY_URL}"  }}
gradle
copy
  1. 4
    Finally, amend the dependencies section of your app's build.gradle to include the 3ds-android library
12345
dependencies {  /* Other dependencies... */   implementation "io.primer:3ds-android:1.1.0"}
gradle
copy
  1. 5
    Use the following card number to trigger a 3DS challenge on the Android SDK with the workflow: 9120 0000 0000 0006

Step 7. Create Payment

Once the payment method data has been securely captured, Primer will return a uniform paymentMethodToken via onTokenizationSuccess that can be safely passed to your server to create a payment with the Payments API.

123456789
private val listener = object : PrimerHeadlessUniversalCheckoutListener {     override fun onTokenizationSuccess(        paymentMethodToken: PaymentMethodToken,        resumeHandler: ResumeHandler    ) {        viewModel.sendPaymentMethodToken(paymentMethodToken, resumeHandler)    }}
kotlin
copy
1234567891011121314151617
class CheckoutViewModel : ViewModel() {  //..  fun sendPaymentMethodToken(token: PaymentMethodToken, handler: ResumeHandler)  {    // Send the payment method token to your server to create a payment (https://primer.io/docs/payments/#one-create-a-payment-request) with `paymentMethod.token`    val paymentResponse =  //...    if (paymentResponse.isSuccessful()) {      // If the payment is successful, call resumeHandler.handleSuccess().      handler.handleSuccess()    } else if (paymentResponse.isPending()) {      // Payments API may return a new `clientToken` for additional steps. In this case, call `resumeHandler.handleNewClientToken(clientToken)`      handler.handleNewClientToken(paymentResponse.requiredAction.clientToken)    } else {      // If the payment is unsuccessful, call `resumeHandler.handleError(error: Error)` to show an error / failed message.      handler.handleError(Error())    }  }}
kotlin
copy

If the above callback responds with a required action, the SDK flow will need to resume (e.g. to complete a 3DS authentication for a card payment). You will need to implement the onResumeSuccess callback to once again call the payment API once this required action has completed.

123456
private val listener = object : PrimerHeadlessUniversalCheckoutListener {     override fun onResumeSuccess(resumeToken: String, resumeHandler: ResumeHandler) {        viewModel.sendResumeToken(e.resumeToken, e.resumeHandler)    }}
kotlin
copy
1234567891011121314151617
class CheckoutViewModel : ViewModel() {  //...  fun sendResumeToken(token: String, handler: ResumeHandler)  {    // Send a resume payment request with resumeToken (https://primer.io/docs/payments/#two-handle-resuming-payment-flows)    val paymentResponse =  //..    if (paymentResponse.isSuccessful()) {      // If the payment is successful, call resumeHandler.handleSuccess().      handler.handleSuccess()    } else if (paymentResponse.isPending()) {      // Payments API may return a new `clientToken` for additional steps. In this case, call `resumeHandler.handleNewClientToken(clientToken)`      handler.handleNewClientToken(paymentResponse.requiredAction.clientToken)    } else {      // If the payment is unsuccessful, call `resumeHandler.handleError(error: Error)` to show an error / failed message.      handler.handleError(Error())    }  }}
kotlin
copy

Handle Errors

Use the onError method to handle any errors emitted by the SDK during the checkout flow.

1234567
private val listener = object : PrimerHeadlessUniversalCheckoutListener {     override fun onError(error: APIError) {            // your custom method to show an error view            showErrorView(error)    }}
kotlin
copy