Android—Clover Go SDK quick start guide

United States

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

📘

Card Reader battery level

Several operations on your Clover Go reader require at least 30% battery to complete. Charge your device before you configure your Android project using the instructions in this guide.

OAuth authentication

Clover uses OAuth to authenticate your app's users to the Clover servers. To enable OAuth, create a Clover app and install it to your test merchant. Your 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 on Clover Developer Dashboard

  1. Click Create App and follow the instructions provided. Use the following settings as a baseline for your test app.
    • App Type: REST Clients
    • Requested Permissions: All On
    • REST Configuration:
      • Site URL: A domain and URL (link) that you control for OAuth redirection upon completion.
      • Default OAuth Response: Code
        Code and Token responses are both supported; however, use only Code responses in production because they provide an extra level of security.
    • Ecommerce Settings: Integrations Enabled: API
  2. 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

  1. In Android Studio, create a new Android Project.

    • Activity: Leave blank
    • Name: Go SDK Intro
    • Language: Kotlin
    • Minimum SDK: API 26: Android 8.0 (Oreo)
    • Package name and Save location are up to your preference
  2. 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

  1. Create a file called GoSDKCreator in your Android project.

  2. 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;
    }
}
  1. Enter the following configuration in your file:
PropertyDescription
EnvironmentEnvironment you want to connect to on Clover servers. Start with Sandbox for development.
OAuthFlowRedirectURILink 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.
OAuthFlowAppIDApp ID of your Clover App on Developer Home that you created in step 1.
OAuthFlowAppSecretApp Secret of your Clover App on Developer Home that you created in step 1.
APIKeyAsk your Developer Relations Representative for this value.
APISecretAsk your Developer Relations Representative for this value.

4. Initialize the SDK

  1. 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
    
  2. Run your app on an Android device to go to the Clover login window.

  3. Enter your credentials in the login window. On successful login you are redirected your app.

5. Scan for devices and connect

  1. 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.
  2. 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.

  1. 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
        }
    });
  1. 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.

  1. In the Chrome browser window, enter your login details.
  2. In the app, approve the required permissions.

The app scans for devices, 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();
    }
}