White-label App SDK
Getting Started
Quickstart Guide

Headless SDK Quickstart

🧑🏽‍💻

You can find the example project here via github (opens in a new tab)

🧑🏽‍💻

Add the MineSec's registry

To get the MineSec's package, setup the credential and registry in your project:

~/.gradle/gradle.properties
# minesec client registry
MINESEC_REGISTRY_LOGIN=minesec-product-support
MINESEC_REGISTRY_TOKEN={token-value}

Please request to our customer support for the token.

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

settings.gradle.kts
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
 
        // MineSec's maven registry
        maven {
            val MINESEC_REGISTRY_LOGIN: String? by settings
            val MINESEC_REGISTRY_TOKEN: String? by settings
 
            requireNotNull(MINESEC_REGISTRY_LOGIN) {
                """
                    Please set your MineSec 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(MINESEC_REGISTRY_TOKEN)
            println("MS GPR: $MINESEC_REGISTRY_LOGIN")
 
            name = "MineSecMavenClientRegistry"
            url = uri("https://maven.pkg.github.com/theminesec/ms-registry-client")
            credentials {
                username = MINESEC_REGISTRY_LOGIN
                password = MINESEC_REGISTRY_TOKEN
            }
        }
    }
}

Add dependency in app gradle

In your app's build.gradle.kts, add the following dependency of the headless sdk:

app/build.gradle.kts
dependencies {
    // ... other deps
 
    // ++
    releaseImplementation("com.theminesec.sdk:headless:1.0.17")
    // ++ for debug SDK, you can declare as debugImplementation
    debugImplementation("com.theminesec.sdk:headless-stage:1.0.17")
}

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):

app/build.gradle.kts
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"
        }
    }
}

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.

      • your-license-file.license
  • Create your headless activity

    First create a new activity and inherit for the HeadlessActivity:

    ClientHeadlessImpl.kt
    import com.theminesec.sdk.headless.HeadlessActivity
     
    class ClientHeadlessImpl: HeadlessActivity()

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

    ./app/src/main/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>

    Register and init the SDK

    Before performing any card reads, the SDK must be registered and initialized. Underneath the init, the Headless 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.

    ClientApp.kt
    class ClientApp : Application() {
        private val appScope = CoroutineScope(Dispatchers.Main)
        private val _sdkInitStatus = MutableSharedFlow<WrappedResult<SdkInitResp>>(replay = 1)
        val sdkInitStatus: SharedFlow<WrappedResult<SdkInitResp>> = _sdkInitStatus
     
        override fun onCreate() {
            super.onCreate()
            appScope.launch {
                val clientAppInitRes = HeadlessSetup.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:

    ./app/src/main/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"
            android:name=".ClientApp"
            tools:targetApi="31">

    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))

    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 MineSec's backend.

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

    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:

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

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

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

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

    Note the in the data model PoiRequest.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.

    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
    MainActivity.kt
    import kotlinx.coroutines.flow.first
     
    class MainActivity : AppCompatActivity() {
        private val launcher = registerForActivityResult(
            HeadlessActivity.contract(ClientHeadlessImpl::class.java)
        ) {
            Log.d(TAG, "onCreate: WrappedResult<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 WrappedResult.Success -> {
                        Log.d(TAG, "MainActivity awaited init success: $initStatus")
     
                        // then do a setup call for downloading the param and keys
                        val setupStatus = HeadlessSetup.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(
            PoiRequest.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:

    Hello