Headless SDK Quickstart
You can find the example project here via github (opens in a new tab)
Full API reference 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:
# 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:
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:
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):
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
:
import com.theminesec.sdk.headless.HeadlessActivity
class ClientHeadlessImpl: HeadlessActivity()
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>
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.
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 thelicense
(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">
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.
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:
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:
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 thesetup
call to load the config & keys.- In practical scenario, you can call the
setup
just once per application install. In contrast, theinit
is required everytime the app is launched. Init success
is required before calling thesetup
.
- In practical scenario, you can call the
- 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(
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: