Logo
Getting started

Quickstart Guide

Android Embedded Payment SDK Quickstart

🧑🏽‍💻 Full API reference is available in this documentation site.

1. Add the Neurogine registry

To get the Neurogine package, setup the credential and registry in your project:

# neurogine client registry
NEUROGINE_REGISTRY_LOGIN=neurogine-product-support
NEUROGINE_REGISTRY_TOKEN={token-value}

Please request to our customer support for the token.

Then in your project's settings gradle, setup the Neurogine registry as the following:

dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()

        // Neurogine maven registry
        maven {
            val NEUROGINE_REGISTRY_LOGIN: String? by settings
            val NEUROGINE_REGISTRY_TOKEN: String? by settings

            requireNotNull(NEUROGINE_REGISTRY_LOGIN) {
                """
                    Please set your Neurogine Github credential in `gradle.properties`.
                    On local machine,
                    ** DO NOT **
                    ** DO NOT **
                    ** DO NOT **
                    Do not put it in the project's file. (and accidentally commit and push)
                    ** DO **
                    Do set it in your machine's global (~/.gradle/gradle.properties)
                """.trimIndent()
            }
            requireNotNull(NEUROGINE_REGISTRY_TOKEN)
            println("Neurogine GPR: $NEUROGINE_REGISTRY_LOGIN")

            name = "NeurogineMavenClientRegistry"
            url = uri("https://neurogine.jfrog.io/artifactory/neurogine-softpos-release-n2tap-mobile/")
            credentials {
                username = NEUROGINE_REGISTRY_LOGIN
                password = NEUROGINE_REGISTRY_TOKEN
            }
        }
    }
}

2. Add dependency in app gradle

In your app's build.gradle.kts, add the following dependency of the Embedded Payment SDK:

dependencies {
    // ... other deps

    // ++
    releaseImplementation("com.neurogine.sdk.softpos:headless:1.0.0")
    // ++ for debug SDK, you can declare as debugImplementation
    debugImplementation("com.neurogine.sdk.softpos:headless-stage:1.0.0")
}

The public API of the Neurogine SDK uses NG* classes (e.g. NGHeadlessSetup, NGHeadlessActivity, NGPoiRequest , NGWrappedResult).

Note that there are 2 versions:

  • headless: it is connected to prod environment, with a more strict checking, like anti-debugging, anti-hooking etc.
  • headless-stage: it is for developing and debugging, note some checking might be loosen with this variant.

Exclude some resource file

In the app's build.gradle.kts, also make sure the following packaging option exist (this is by default included in a new android project):

android {
    // ...
    packagingOptions {
        resources {
            excludes += "/META-INF/{AL2.0,LGPL2.1}"
            excludes += "/META-INF/DEPENDENCIES"
            excludes += "/META-INF/LICENSE"
            excludes += "/META-INF/LICENSE.txt"
            excludes += "/META-INF/license.txt"
            excludes += "/META-INF/NOTICE"
            excludes += "/META-INF/NOTICE.txt"
            excludes += "/META-INF/notice.txt"
            excludes += "/META-INF/ASL2.0"
            excludes += "/META-INF/*.kotlin_module"
        }
    }
}

3. Put the .license file to your project

Place the your-license-file.license in the app module's ./src/main/assets respectively

Please request to our customer support for the license if you haven't got one.

app
└── src
    └── main
        └── assets
            └── your-license-file.license

4. Create your headless activity

First create a new activity and inherit for the NGHeadlessActivity:

import com.neurogine.softpos.sdk.headless.NGHeadlessActivity

class ClientHeadlessImpl: NGHeadlessActivity()

Remember to register the new activity in the app's AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.MyApplication"
        tools:targetApi="31">
        ...

        <activity
            android:name=".ClientHeadlessImpl"
            android:launchMode="singleTask"
            />
    </application>
</manifest>

5. Register and init the SDK

Before performing any card reads, the SDK must be registered and initialized. Underneath the init, the Embedded Payment SDK would wire up the CPoC/ MPoC, check the register status, also it would perform an attestation.

Note the init process is required for every application start ups. It means if the app is killed, the next time app launches it'll also need to init the SDK.

The android's application is an ideal place to invoke the init, if you wish to init the SDK elsewhere, maybe in some activity you could definitely do so, but keep in mind this init process might take some time for processing because of the network call and key generation.

class ClientApp : Application() {
    private val appScope = CoroutineScope(Dispatchers.Main)
    private val _sdkInitStatus = MutableSharedFlow<NGWrappedResult<SdkInitResp>>(replay = 1)
    val sdkInitStatus: SharedFlow<NGWrappedResult<SdkInitResp>> = _sdkInitStatus

    override fun onCreate() {
        super.onCreate()
        appScope.launch {
            val clientAppInitRes = NGHeadlessSetup.initSoftPos(this@ClientApp, "your.license")
            Log.d(TAG, "Application init: $clientAppInitRes")
            _sdkInitStatus.emit(clientAppInitRes)
        }
    }
}

A few things to note:

  • You'll need to pass the application and the license (filename) in this method
  • During the init process, the SDK would register and perform attestation via network call, that's why the initSoftPos is a suspend function.

Remember to check for the app's manifest to make sure the ClientApp is there as well:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.MyApplication"
        android:name=".ClientApp"
        tools:targetApi="31">
        <!-- other activities -->
    </application>
</manifest>

Now if you run the app, wait for a second or two you should be able to see in logcat:

HL/ ClientApp: Success(value=SdkInitResp(sdkVersion=1.10.105.12.17, sdkId=4411be126c0c91b7))

6. Load the default config and download keys

For a newly installed application, it'll need to download and load the emv, terminal parameters, as well as the keys for later transaction processing. This setup can be done once per application.

Note that the initialSetup is also a suspending function - which it'll download the key from Neurogine backend.

fun setup() = lifecycleScope.launch {
    val setupResp = NGHeadlessSetup.initialSetup(this@MainActivity)
    Log.d(TAG, "setup: $setupResp")
}

7. Request a transaction

To make a new sale request is fairly straight forward:

From your other activity wish to launch the request, create a activity launcher for the result:

private val launcher = registerForActivityResult(
    NGHeadlessActivity.contract(ClientHeadlessImpl::class.java)
) {
    Log.d(TAG, "MainActivity launcher result back for NGWrappedResult<Transaction>: $it}")
}

The above launcher would print out whatever the result got back from the NGHeadlessActivity.

Then let's create another function to launch the Sale request:

fun launchSale() = launcher.launch(
    NGPoiRequest.ActionNew(
        tranType = TranType.SALE,
        amount = Amount(
            BigDecimal("1.0"),
            Currency.getInstance("HKD"),
        ),
        profileId = "prof_01HYYPGVE7VB901M40SVPHTQ0V",
    )
)

Note the in the data model NGPoiRequest.ActionNew, the tranType, amount and profileId are mandatory parameters. The transaction type and amount are as the name suggest while the profileId is used for the backend to lookup the corresponding MID/ TID and routing.

8. Wire it up

Now to wire it all up, let's consider this:

  • When the app starts, in the application it'll do the init SDK, attestation will be performed in the background.
  • Once the init is done, we can invoke the setup call to load the config & keys.
    • In practical scenario, you can call the setup just once per application install. In contrast, the init is required everytime the app is launched.
    • Init success is required before calling the setup.
  • Then, launch a sale from the main activity, this request will be passed to the ClientHeadlessImpl
import kotlinx.coroutines.flow.first

class MainActivity : AppCompatActivity() {
    private val launcher = registerForActivityResult(
        NGHeadlessActivity.contract(ClientHeadlessImpl::class.java)
    ) {
        Log.d(TAG, "onCreate: NGWrappedResult<Transaction>: $it}")
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        lifecycleScope.launch {
            // wait for the first sdk init signal
            when (val initStatus = (application as ClientApp).sdkInitStatus.first()) {
                // if this is success
                is NGWrappedResult.Success -> {
                    Log.d(TAG, "MainActivity awaited init success: $initStatus")

                    // then do a setup call for downloading the param and keys
                    val setupStatus = NGHeadlessSetup.initialSetup(this@MainActivity)
                    Log.d(TAG, "MainActivity setup status: $setupStatus")

                    // lastly we can launch the sale
                    launchSale()
                }

                else -> {
                    Log.d(TAG, "MainActivity awaited init failed! $initStatus")
                }
            }
        }
    }

    fun launchSale() = launcher.launch(
        NGPoiRequest.ActionNew(
            tranType = TranType.SALE,
            amount = Amount(
                BigDecimal("1.0"),
                Currency.getInstance("HKD"),
            ),
            profileId = "prof_01HYYPGVE7VB901M40SVPHTQ0V",
        )
    )
}

Run the app and you should be able to see: Headless Demo

On this page