DEV Community

Cover image for Accept payments using Tap to Pay for Android with Stripe
Charlie Gerard for Stripe

Posted on

Accept payments using Tap to Pay for Android with Stripe

Stripe recently announced Tap to Pay for Android. Let’s look into how to implement this so you can start collecting in-person payments directly using a mobile phone! In this blog post, let’s pretend that you work for a (fake) food truck company called CoolBeans and you need to create an Android app so you can collect payments on the go.

Here’s what we’ll be building:

GIF of the demo Android application with 3 screens, one to connect to a Terminal, then one to indicate an amount, and finally, the Tap to Pay screen.

This tutorial uses Kotlin; however, this feature can also be implemented using Java. If you need more information, please refer to our docs or check out the Stripe Terminal Android SDK repository that contains an example in both Kotlin and Java.

If you learn better with videos, you can check out the video tutorial on YouTube!

Prerequisites

To follow along with this tutorial, you will need:

If you are using a different version of the Android SDK or Kotlin, the code samples in this tutorial may still work but this is not guaranteed.

This post will only cover the code samples needed to get Tap to Pay working in your Android application. I will purposely not cover how to build and structure an Android app or go into details on the specific flow I implemented as this is more relative to what you want to build.

If you want to get started quickly, you can clone my demo application on GitHub, follow the instructions in the README to make sure you update the URL to the back-end server, and follow along as I explain the different parts.

After cloning and running the demo application, you should see the following screens. The first part of the application is responsible for connecting your mobile phone as a reader. Then, the user can input an amount, and finally collect the payment using Tap to Pay.

GIF of the demo Android application with 3 screens, one to connect to a Terminal, then one to indicate an amount, and finally, the Tap to Pay screen.

Install the Stripe Terminal Android SDK

Configuration

If you’ve already cloned the Tap to Pay demo application, you can skip this section. If you have already integrated with the Stripe Terminal Android SDK before and would like to update it to also use Tap to Pay, keep reading to understand the few changes you will need to make.

In your module’s build.gradle file, replace the Stripe Terminal dependencies with the latest version and add the stripeterminal-localmobile package:

implementation "com.stripe:stripeterminal-localmobile:2.20.0"
implementation "com.stripe:stripeterminal-core:2.20.0"
Enter fullscreen mode Exit fullscreen mode

Permissions

The Stripe Terminal Android SDK requires different permissions depending on the type of terminal device you’re using. For Tap to Pay, Bluetooth and Location are required.

To do this, make sure you’re adding the following lines to the AndroidManifest file:

<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
Enter fullscreen mode Exit fullscreen mode

And in your application logic, check that the permission is granted before continuing. If the user has granted Bluetooth permission, you can start initializing a terminal instance.

Initializing a Terminal instance

One of the first things you need to do is initialize a terminal instance with the following code:

Terminal.initTerminal(
  applicationContext, LogLevel.VERBOSE, TokenProvider(),
  TerminalEventListener()
)
Enter fullscreen mode Exit fullscreen mode

This will initialize a terminal for the given application context that you need to pass as the first parameter. Then you can pass the level of logging verbosity, an instance of the ConnectTokenProvider interface to use when a new token is needed and finally, a listener to inform of events in the Terminal lifecycle.

If you have used the Stripe Terminal Android SDK before, you should already have created the interface for the token provider and the listener. If not, feel free to check the TokenProvider and TerminalEventListener files in the demo application.

Loading locations

To use Stripe Terminal, you need to register one or more locations to manage readers and their activity by associating them with a physical location. Even though food trucks can be located in various places, there are regulations and parking permits needed to sell food in the street so CoolBeans will likely have a set list of locations where the business can operate. You can create these locations via the Stripe Dashboard or using the API.

Stripe dashboard showing 2 locations for the fake customer's food trucks

This way, the activity of a reader associated with a location will be reflected in the dashboard and you will be able to collect data about how the different locations are performing in terms of sales.

In your Android app, you can then list your locations, passing a limit for the number of locations you would like to fetch, and a callback. If you’re following along with the demo application, this code can be found in the MainActivity.kt file.

Terminal.getInstance().listLocations(
  ListLocationsParameters.Builder().apply {
      limit = 100
  }.build(),
  locationCallback
)
Enter fullscreen mode Exit fullscreen mode

The callback will save the location objects in a mutable list.

private val mutableListState = MutableStateFlow(LocationListState())

private val locationCallback = object : LocationListCallback {
  override fun onFailure(e: TerminalException) {
      e.printStackTrace()
  }

  override fun onSuccess(locations: List<Location>, hasMore: Boolean) {
      mutableListState.value = mutableListState.value.let {
          it.copy(
              locations = it.locations + locations,
              hasMore = hasMore,
              isLoading = false,
          )
      }
  }
}
Enter fullscreen mode Exit fullscreen mode

Once you have fetched your locations, you need to write the logic that will identify the reader you will use.

Discovering readers

To find available readers and select the one you want to use, you need to call the discoverReaders method.

When implementing Tap to Pay, the configuration object needs to have its discoveryMethod argument set to DiscoveryMethod.LOCAL_MOBILE so it considers the mobile phone running the application as a terminal device.

val config = DiscoveryConfiguration(
           timeout = 0,
           discoveryMethod = DiscoveryMethod.LOCAL_MOBILE,
           isSimulated = false,
           location = mutableListState.value.locations[0].id
       )
Enter fullscreen mode Exit fullscreen mode

Call the discoverReaders method passing this configuration object as well as a DiscoveryListener instance that will provide the list of discovered readers.

Terminal.getInstance().discoverReaders(config, discoveryListener = object :
DiscoveryListener {
override fun onUpdateDiscoveredReaders(readers: List<Reader>) {
   // Filtering the list of readers discovered to only store the ones currently online
   readers.filter { it.networkStatus != Reader.NetworkStatus.OFFLINE }
  // For simplicity, I’m only using the first reader retrieved but this code would need to be updated if you wanted to show the list in the UI and let the user select a specific location.
   var reader = readers[0]
   // Handle the connection to the reader in a separate function
   connectToReader(reader)


}, object : Callback {
  override fun onSuccess() {
     println("Finished discovering readers")
  }
  override fun onFailure(e: TerminalException) {
     e.printStackTrace()
  }
})
Enter fullscreen mode Exit fullscreen mode

Connecting a reader to a location

Now that we listed our locations and discovered the reader we want to use, let’s connect the location to the reader using the connectLocalMobileReader method in the connectToReader function created in the code sample above.

private fun connectToReader(reader: Reader){
    // Pass the location chosen to the LocalMobileConnectionConfiguration method.
       val config = ConnectionConfiguration.LocalMobileConnectionConfiguration("${mutableListState.value.locations[0].id}")
    // Call the connectLocalMobileReader method passing the reader selected, the config object and a callback function.
       Terminal.getInstance().connectLocalMobileReader(
           reader,
           config,
           object: ReaderCallback {
               override fun onFailure(e: TerminalException) {
                   e.printStackTrace()
               }


               override fun onSuccess(reader: Reader) {
                   // [Optional] Update the UI with the location name and terminal ID to indicate to the user that the reader is successfully connected.
                   runOnUiThread {
                       val manager: FragmentManager = supportFragmentManager
                       val fragment: Fragment? = manager.findFragmentByTag(ConnectReaderFragment.TAG)


                       if(reader.id !== null && mutableListState.value.locations[0].displayName !== null){
                           (fragment as ConnectReaderFragment).updateReaderId(
                               mutableListState.value.locations[0].displayName!!, reader.id!!
                           )
                       }
                   }
               }
           }
       )
   }
Enter fullscreen mode Exit fullscreen mode

Collecting payment

When the reader is successfully connected, you should be ready to accept payments the same way as you would when using other Terminal devices. You’ll need a back-end server with endpoints to create a connection token and handle the different events that happen when capturing and processing payments with Stripe. If you don’t already have one, you can clone our Stripe Terminal backend example repo and follow the instructions in the README to set it up.

Creating a payment intent

First, you need to create a PaymentIntent. To do this, your Android application will need to make a POST request to your server implementing the Stripe API. In the demo application, the ApiClient singleton object handles the calls to the back-end and provides a createPaymentIntent method you can call and pass payment details including the amount, currency, authorization details and a callback.
For simplicity, the code sample below uses hardcoded values but you would need to adapt your application to your particular use case.

ApiClient.createPaymentIntent(
   amount = 100.toLong() ,
   currency = usd,
   extendedAuth = false,
   incrementalAuth = false,
   callback = object : retrofit2.Callback<PaymentIntentCreationResponse> {
       override fun onResponse(
           call: Call<PaymentIntentCreationResponse>,
           response: Response<PaymentIntentCreationResponse>
       ) {
           if (response.isSuccessful && response.body() != null) {
        // Retrieve the payment intent once it is created successfully
               Terminal.getInstance().retrievePaymentIntent(
                   response.body()?.secret!!,
                   createPaymentIntentCallback
               )
           } else {
               println("Request not successful: ${response.body()}")
           }
       }
       override fun onFailure(
           call: Call<PaymentIntentCreationResponse>,
           t: Throwable
       ) {
           t.printStackTrace()
       }
   }
)
Enter fullscreen mode Exit fullscreen mode

When the payment intent is successfully created, the code sample above calls the retrievePaymentIntent method with the secret present in the response body to retrieve the Payment Intent.
The code sample below shows how the callback function is then implemented.

private val createPaymentIntentCallback by lazy {
   object : PaymentIntentCallback {
       override fun onSuccess(paymentIntent: PaymentIntent) {
        collectPaymentMethod(paymentIntent)
       }
       override fun onFailure(e: TerminalException) {
           e.printStackTrace()
       }
   }
}
Enter fullscreen mode Exit fullscreen mode

Collecting a payment method

After the Payment Intent is created and retrieved, we can pass it into the collectPaymentMethod method with a callback and the tipping configuration.

private fun collectPaymentMethod(paymentIntent: PaymentIntent){
  // Hardcoded for the purpose of this tutorial
   val skipTipping = true
   val collectConfig = CollectConfiguration.Builder()
       .skipTipping(skipTipping)
       .build()


   Terminal.getInstance().collectPaymentMethod(
       paymentIntent, collectPaymentMethodCallback, collectConfig
   )
}

private val collectPaymentMethodCallback by lazy {
   object : PaymentIntentCallback {
       override fun onSuccess(paymentIntent: PaymentIntent) {
        processPayment(paymentIntent)
       }


       override fun onFailure(e: TerminalException) {
           e.printStackTrace()
       }
   }
}
Enter fullscreen mode Exit fullscreen mode

When the payment method is successfully collected, we can start processing the payment.

Processing and capturing the payment

To process the payment, you need to call the processPayment method passing the Payment Intent and a callback function that will capture the payment.

private fun processPayment(paymentIntent: PaymentIntent){
   Terminal.getInstance().processPayment(paymentIntent, processPaymentCallback)
}

private val processPaymentCallback by lazy {
   object : PaymentIntentCallback {
       override fun onSuccess(paymentIntent: PaymentIntent) {
           ApiClient.capturePaymentIntent(paymentIntent.id)
        // Return to previous screen
           navigateTo(PaymentDetails.TAG, PaymentDetails(), true)
       }
       override fun onFailure(e: TerminalException) {
           e.printStackTrace()
       }
   }
}
Enter fullscreen mode Exit fullscreen mode

At this point, if all the previous steps were successful, you should be able to run your application, connect your mobile phone as a reader, see the Tap to Pay screen after calling the collectPaymentMethod, and tap a credit card behind your device to process the payment. CoolBeans is now ready to accept payment on the go!

Conclusion

If you have used the Stripe Terminal Android SDK before, you only need to make minimal code changes to update your application to implement the Tap to Pay feature. If you want to get started quickly, feel free to clone the repository of the demo application and customize it to adapt to your use case. You can also check out our documentation and the Stripe Terminal Android SDK repository if you want to learn more.

We hope you’ll share with us how you’re planning to use Tap to Pay with Stripe!

You can also stay up to date with Stripe developer updates on the following platforms:
📣 Follow @StripeDev and our team on Twitter
📺 Subscribe to our YouTube channel
💬 Join the official Discord server
📧 Sign up for the Dev Digest

About the author

Charlie Gerard's profile picture. She is a caucasian woman with long brown hair, wearing glasses. She is standing in front of a white wall with purple lights.

Charlie Gerard is a Developer Advocate at Stripe, a published author and a creative technologist. She loves researching and experimenting with technologies. When she’s not coding, she enjoys spending time outdoors, reading and setting herself random challenges.

Top comments (1)

Collapse
 
batz profile image
batz

Hello,
Great example, thank you. Could you direct me to example for tap-to-pay with Android's jetpack compose please.