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:
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:
- Android SDK version 33
- AndroidStudio
- Kotlin 1.7.10
- Stripe account
- Stripe Terminal Android SDK
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.
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"
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" />
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()
)
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.
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
)
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,
)
}
}
}
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
)
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()
}
})
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!!
)
}
}
}
}
)
}
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()
}
}
)
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()
}
}
}
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()
}
}
}
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()
}
}
}
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 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)
Hello,
Great example, thank you. Could you direct me to example for tap-to-pay with Android's jetpack compose please.