Android—Clover Go SDK quick start guide
This quick start guide explains how to take a payment using the Android Clover Payment software development kit (SDK) and your Clover Go reader.
Prerequisites
- See Overview of the Clover platform.
- Create a global developer account with a default test merchant account.
- Create additional test merchants, if needed.
- Order a Clover Go reader Developer Kit (Dev Kit) and set it up.
- Install Android Studio Chipmunk | 2021.2.1 Patch 2 or higher.
- Use an Android-powered device (Android Oreo/8.0+).
- Charge Clover Go reader—Device battery charging requirement. Several operations on your Clover Go reader require at least 30% battery. Charge your device before you configure your iOS project using the instructions in this guide.
OAuth authentication
Clover uses OAuth to authenticate your app's users to the Clover servers. Use the steps in this topic to create a Clover app and install it on your test merchant. The Clover app has an associated App ID and App Secret that the Android app uses to obtain the permissions needed to perform OAuth. See Authenticate with OAuth for more information.
Steps
1. Create your test app in sandbox
-
Log in to your Clover global developer account. The Global Developer Dashboard appears.
-
Click Create App and follow the instructions.
-
Use the following settings as a baseline for your test app.
Field Description App Type In the REST Clients section, click Web. Requested Permissions Select checkboxes based on required permissions. See Set app permissions. REST Configuration - Site URL Enter the domain and URL (link) that you control for OAuth redirection upon completion. - Default OAuth Response Select Code. Code and Token responses are both supported; however, only Code responses are used in production because they provide an extra level of security. Ecommerce Integrations Enabled: API -
Note the App ID and App Secret to use later when you configure the Android app.
2. Create your Android app and import the SDK
-
In Android Studio, create a new Android Project.
Field Description Activity Leave blank. Name Go SDK Intro Language Kotlin Minimum SDK API 26: Android 8.0 (Oreo) Package name and Save location As per your preference. -
Add the dependency directly to the module's dependencies in the
build.gradle
file:
implementation ("com.clover.sdk:go-sdk:latest.release")
3. Configure the SDK
-
Create a file with the name
GoSDKCreator
in your Android project. -
Copy the following code and paste it into the
GoSDKCreator
file.
object GoSDKCreator {
// App ApiKey and Secret
private const val API_KEY = YOUR_API_KEY
private const val API_SECRET = YOUR_API_SECRET
private fun buildConfig(context: Context) = GoSdkConfiguration.Builder(
context = context,
appId = BuildConfig.APPLICATION_ID,
appVersion = "1.0.0",
apiKey = API_KEY,
apiSecret = API_SECRET,
oAuthFlowAppSecret = YOUR_OAUTH_FLOW_APP_SECRET,
oAuthFlowRedirectURI = YOUR_OAUTH_FLOW_REDIRECT_URI,
oAuthFlowAppID = YOUR_OAUTH_FLOW_APP_ID,
environment = GoSdkConfiguration.Environment.SANDBOX,//Available environments are PROD, DEV, STAGING and SANDBOX
reconnectLastConnectedReader = true
)
.build()
private lateinit var goSdkInstance: GoSdk
@JvmStatic
fun get(context: Context): GoSdk {
if (!GoSDKCreator::goSdkInstance.isInitialized) {
goSdkInstance = GoSdkCreator.create(buildConfig(context))
}
return goSdkInstance
}
}
public class GoSdkCreatorJava {
// Companion App ApiKey and Secret
private static final String API_KEY = YOUR_API_KEY;
private static final String API_SECRET = YOUR_API_SECRET;
private GoSdk goSdkInstance;
private GoSdkConfiguration buildConfig(Context context) {
return new GoSdkConfiguration.Builder(
context,
BuildConfig.APPLICATION_ID,
YOUR_REDIRECT_URI,
YOUR_APP_ID,
YOUR_APP_SECRET,
"1.0.0", //app version
API_KEY,
API_SECRET,
null, //api access token
GoSdkConfiguration.Environment.SANDBOX,//Available environments are PROD, DEV, STAGING and SANDBOX
false, //Deprecated field
false, //Deprecated field
true //reconnectLastConnectedReader
).build();
}
public GoSdk get(Context context) {
if (goSdkInstance == null) {
goSdkInstance = GoSdkCreator.Companion.create(buildConfig(context));
}
return goSdkInstance;
}
}
-
Enter the following configuration in your file:
Property Description Environment Environment you want to connect to on Clover servers. Start with Sandbox to build and test your app. OAuthFlowRedirectURI Link http://<yourdomain>/OAuthResponse
where your domain is located.
When the OAuth flow starts, this URI is passed to Clover login servers. After the user authenticates, this is the URI that Clover servers call to link back to your app.
This value is validated against the site URL you configured on Clover servers before Clover servers call it.OAuthFlowAppID App ID of the Clover app you created in sandbox. See step 1. OAuthFlowAppSecret App Secret of the Clover app you created in sandbox. See step 1. APIKey Ask your Developer Relations Representative for this value. APISecret Ask your Developer Relations Representative for this value.
4. Initialize the SDK
-
In your activity or fragment, add this line:
private val goSdk: GoSdk by lazy { GoSDKCreator.get(requireContext()) }
private GoSdk goSdk = GoSDKCreator.get(this); //in the activity scenario, "this" is the context, in the fragment you would need to grab the context
-
Run your app on an Android-powered device to go to the Clover login window.
-
Enter your credentials in the login window. On successful login, you are redirected to your app.
5. Scan for devices and connect
-
Open a Bluetooth scan, receive a list of devices found, and connect to the first found device.
- If the device is running Android S (Snow Cone, Android 12) or higher, request BLUETOOTH_CONNECT and BLUETOOTH_SCAN permissions.
- If the device is not running Android S or higher, determine what Android version the device is running and request ACCESS_FINE_LOCATION permissions using runtime permissions. See the Android documentation for more information.
-
Inside a coroutine scope, add the lines:
goSdk.scanForReaders().catch { //handle error }.collectLatest { //adapter.addDevice(it) //In case you have a scan list // OR in case you want to connect to a particular device if (it.bluetoothName.contains("XXXXXX")) { //XXXXXX in this case, is the last 6 digits of your device's serial number goSdk.connect(it) } }
goSdk.scanForReaders(activity, new GoSdkCallback<ReaderInfo>() { @Override public void onNext(ReaderInfo readerInfo) { //adapter.addDevice(readerInfo) //In case you have a scan list // OR in case you want to connect to a particular device if(readerInfo.getBluetoothName().contains("XXXXXX")) { //XXXXXX in this case, is the last 6 digits of your device's serial number goSdk.connect(readerInfo); } } @Override public void onError(@NonNull Throwable throwable) { //handle error } } );
6. Take a payment
Clover recommends that you wait to initiate a charge request until the CardReaderStatus.READY
event appears. The card status appears regardless of the last action.
- To observe the
CardReaderStatus
:
goSdk.observeCardReaderStatus(viewLifecycleOwner, cardReaderStatusCallback)
val cardReaderStatusCallback = object : GoSdkCallback<CardReaderStatus> {
override fun onError(e: Throwable) {
//handle error scenario
}
override fun onNext(result: CardReaderStatus) {
// publish it or save in ViewModel?.. anything you want
when (result) {
is CardReaderStatus.BatteryPercentChanged -> {}
is CardReaderStatus.BleFirmwareUpdateInProgress -> {}
is CardReaderStatus.CheckingForUpdate -> {}
is CardReaderStatus.Connected -> {}
is CardReaderStatus.Connecting -> {}
is CardReaderStatus.Deleted -> {
//this is where someone will update UI to disable payment taking UI
}
is CardReaderStatus.Disconnected -> {
//this is where someone will update UI to disable payment taking UI
}
is CardReaderStatus.Ready -> {
//this is where someone will update UI to take a payment
}
}
is CardReaderStatus.ResetInProgress -> {}
is CardReaderStatus.RkiInProgress -> {}
is CardReaderStatus.SpFirmwareUpdateInProgress -> {}
is CardReaderStatus.UpdateComplete -> {}
}
}
goSdk.observeCardReaderStatus(this, new GoSdkCallback<CardReaderStatus>() {
@Override
public void onNext(CardReaderStatus cardReaderStatus) {
// publish it or save in ViewModel?.. anything you want
switch (cardReaderStatus.toString()) {
case "BatteryPercentChanged":
case "BleFirmwareUpdateInProgress":
case "CheckingForUpdate":
case "Connected":
case "Connecting":
case "Deleted": //this is where someone will update UI to disable payment taking UI
case "Disconnected": //this is where someone will update UI to disable payment taking UI
case "Ready: ${result.readerInfo.serialNo}": //this is where someone will update UI to take a payment
case "ResetInProgress":
case "RkiInProgress":
case "SpFirmwareUpdateInProgress":
case "UpdateComplete":
}
}
@Override
public void onError(@NonNull Throwable throwable) {
//handle error scenario
}
});
- Inside a coroutine scope, call the charge function in the GoSDK interface.
val request = PayRequest(
final = true, //true for Sales, false for Auth or PreAuth Transactions
capture = true, //true for Sales, true for Auth, false for PreAuth Transactions
amount = 100L,
taxAmount = 0L,
tipAmount = 0L,
externalPaymentId = "pay-id", //Can be null
externalReferenceId = "invoice-num", //can be null
keyedInCard = null //Card value can be filled in, if you want to take payment w/o cardReader
)
// Clover recommends that you wait to initiate a charge request until the CardReaderStatus.READY event is emitted
goSdk.chargeCardReader(
request
).collectLatest {
//We suggest logging or updating the UI with the transaction status so you follow the transaction steps
//and to know if the device is waiting for a user action (i.e. if the device is waiting for a card to be inserted or tapped)
updateUIWithCardReaderState(it)
}
private fun updateUIWithCardReaderState(it: ChargeCardReaderState) {
updateTransactionLog(
when (it) {
is ChargeCardReaderState.OnPaymentComplete -> "OnPaymentComplete: ${it.response}"
is ChargeCardReaderState.OnPaymentError -> "OnPaymentError: $it"
is ChargeCardReaderState.OnReaderPaymentProgress -> "OnReaderPaymentProgress: ${it.event}"
}.toString()
)
}
PayRequest request = new PayRequest(
10000L,
true,
true,
"pay-id",
"invoice num",
10L,
1000L,
keyedInCard);
// Clover recommends that you wait to initiate a charge request until the CardReaderStatus.READY event is emitted
goSdk.chargeCardReader(
this, //In this context, "this" means the activity. If in a fragment, the viewLifeCycleOwner is required
request,
new GoSdkCallback<ChargeCardReaderState>() {
@Override
public void onNext(ChargeCardReaderState result) {
Log.i("TAG", result.toString());
if (result instanceof PaymentState.OnPaymentComplete) {
handlePaymentComplete((PaymentState.OnPaymentComplete) result);
}
}
@Override
public void onError(@NonNull Throwable e) {
Log.i("TAG", e.getMessage());
}
}
);
7. Install and run the app
When the app first launches, the Clover login window opens in Chrome.
- In the Chrome browser window, enter your login details.
- In the app, approve the required permissions.
The app scans for devices. The app connects to the first found device, and starts a charge.
package com.example.gosdkintro
import android.Manifest
import android.content.DialogInterface
import android.content.pm.PackageManager
import android.os.Build
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.core.app.ActivityCompat
import androidx.lifecycle.lifecycleScope
import com.clover.sdk.gosdk.GoSdk
import com.clover.sdk.gosdk.model.PayRequest
import com.clover.sdk.gosdk.payment.domain.model.CardReaderStatus
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
class MainActivity : AppCompatActivity() {
private val goSdk: GoSdk by lazy { GoSDKCreator.get(this) }
private val requiredPermissions = when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> arrayOf(
Manifest.permission.BLUETOOTH_CONNECT,
Manifest.permission.BLUETOOTH_SCAN,
)
Build.VERSION.SDK_INT <= Build.VERSION_CODES.P -> arrayOf(
Manifest.permission.ACCESS_COARSE_LOCATION
)
else -> arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION
)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
ActivityCompat.requestPermissions(
this,
requiredPermissions,
REQUEST_PERMISSION_CODE
)
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
val permissionsList = ArrayList<String>()
if (requestCode == REQUEST_PERMISSION_CODE && grantResults.isNotEmpty()) {
for (i in permissions.indices) {
if (grantResults[i] == PackageManager.PERMISSION_DENIED) {
permissionsList.add(permissions[i])
}
}
if (permissionsList.isEmpty()) {
permissionsGranted()
} else {
var showRationale = false
for (permission in permissionsList) {
if (ActivityCompat.shouldShowRequestPermissionRationale(this, permission)) {
showRationale = true
break
}
}
if (showRationale) {
showAlertDialog(
{ _, _ ->
ActivityCompat.requestPermissions(
this,
permissionsList.toTypedArray(),
REQUEST_PERMISSION_CODE
)
},
{ _, _ ->
permissionsDenied()
}
)
}
}
}
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
private fun showAlertDialog(
okListener: DialogInterface.OnClickListener,
cancelListener: DialogInterface.OnClickListener
) {
AlertDialog.Builder(this)
.setMessage("Some permissions are not granted. Application may not work as expected. Do you want to grant them?")
.setPositiveButton("OK", okListener)
.setNegativeButton("Cancel", cancelListener)
.create()
.show()
}
private fun permissionsDenied() {
Toast.makeText(this, "Permissions Denied!", Toast.LENGTH_SHORT).show()
}
private fun permissionsGranted() {
Toast.makeText(this, "Permissions granted!", Toast.LENGTH_SHORT).show()
lifecycleScope.launch {
goSdk.scanForReaders().collectLatest { reader ->
//This callback will hit for every device found
//Once you found the device you want to connect to, call the line below
goSdk.connect(reader)
}
}
lifecycleScope.launch {
goSdk.observeCardReaderStatus().collectLatest {
println(it)
if (it is CardReaderStatus.Ready) {
val request = PayRequest(
final = true, //true for Sales, false for Auth or PreAuth Transactions
capture = true, //true for Sales, true for Auth, false for PreAuth Transactions
amount = 100L,
taxAmount = 0L,
tipAmount = 0L,
externalPaymentId = null
)
goSdk.chargeCardReader(request).collectLatest { chargeState ->
println(chargeState)
}
}
}
}
}
companion object {
const val REQUEST_PERMISSION_CODE = 100
}
}
package com.example.gosdkintro;
import android.Manifest;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import com.clover.sdk.gosdk.GoSdk;
import com.clover.sdk.gosdk.core.domain.model.ReaderInfo;
import com.clover.sdk.gosdk.model.PayRequest;
import com.clover.sdk.gosdk.models.ChargeCardReaderState;
import com.clover.sdk.gosdk.models.GoSdkCallback;
import com.clover.sdk.gosdk.models.PaymentState;
import com.clover.sdk.gosdk.payment.domain.model.CardReaderStatus;
import java.util.ArrayList;
import java.util.Arrays;
public class MainActivity extends AppCompatActivity {
private GoSdk goSdk;
private final MainActivity activity = this;
private final int REQUEST_PERMISSION_CODE = 100;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
goSdk = GoSdkCreatorJava.get(activity);
String[] requiredPermissions = new String[]{Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION};
// Get Bluetooth permissions to connect to the reader
ActivityCompat.requestPermissions(
this,
requiredPermissions,
REQUEST_PERMISSION_CODE
);
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
ArrayList<String> permissionsList = new ArrayList<>();
if (requestCode == REQUEST_PERMISSION_CODE && grantResults.length != 0) {
for (int i = 0; i < permissions.length; i++) {
if (grantResults[i] == PackageManager.PERMISSION_DENIED) {
permissionsList.add(permissions[i]);
}
}
if (permissionsList.size() == 0) {
permissionsGranted();
} else {
boolean showRationale = false;
for (int i = 0; i < permissionsList.size(); i++) {
String permission = permissionsList.get(i);
if (ActivityCompat.shouldShowRequestPermissionRationale(activity, permission)) {
showRationale = true;
break;
}
}
if (showRationale) {
DialogInterface.OnClickListener okListener = (dialogInterface, i) -> {
String[] permissionsArray = Arrays.copyOf(permissionsList.toArray(), permissionsList.size(), String[].class);
ActivityCompat.requestPermissions(
activity,
permissionsArray,
REQUEST_PERMISSION_CODE
);
};
DialogInterface.OnClickListener cancelListener = (dialogInterface, i) -> permissionsDenied();
showAlertDialog(okListener, cancelListener);
}
}
}
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
private void permissionsGranted() {
Toast.makeText(this, "Permissions granted!", Toast.LENGTH_SHORT).show();
goSdk.scanForReaders(activity, new GoSdkCallback<ReaderInfo>() {
@Override
public void onNext(ReaderInfo readerInfo) {
//This callback will hit for every device found
//Once you found the device you want to connect to, call the line below
goSdk.connect(readerInfo);
}
@Override
public void onError(@NonNull Throwable throwable) {
//handle error scenario
}
});
goSdk.observeCardReaderStatus(
activity,
new GoSdkCallback<CardReaderStatus>() {
@Override
public void onNext(CardReaderStatus cardReaderStatus) {
// Ready status means the updates are done, if any and we are ready to take the payment
if (cardReaderStatus instanceof CardReaderStatus.Ready) {
PayRequest request = new PayRequest(10000L, true, true, "pay-id", "invoice num", 10L, 1000L, null);
goSdk.chargeCardReader(
activity,
request,
new GoSdkCallback<ChargeCardReaderState>() {
@Override
public void onNext(ChargeCardReaderState result) {
// Let's do some actual work
if (result instanceof PaymentState.OnPaymentComplete) {
handlePaymentComplete((PaymentState.OnPaymentComplete) result);
}
}
@Override
public void onError(@NonNull Throwable e) {
//handle error scenario
}
}
);
}
}
@Override
public void onError(@NonNull Throwable throwable) {
//handle error scenario
}
}
);
}
private void handlePaymentComplete(@NonNull PaymentState.OnPaymentComplete event) {
//save it to our ViewModel.... Will help with other operations like printing receipts
}
private void permissionsDenied() {
Toast.makeText(this, "Permissions Denied!", Toast.LENGTH_SHORT).show();
}
private void showAlertDialog(
DialogInterface.OnClickListener okListener,
DialogInterface.OnClickListener cancelListener
) {
new AlertDialog.Builder(this)
.setMessage("Some permissions are not granted. Application may not work as expected. Do you want to grant them?")
.setPositiveButton("OK", okListener)
.setNegativeButton("Cancel", cancelListener)
.create()
.show();
}
}
Updated 5 months ago