WinkPay POS Integration
WinkPay SDK — Native Android Integration Guide
WinkPay is a biometric (face + palm) payment SDK for Android POS applications. This guide is for partner POS developers integrating the SDK into an Android host app.
The SDK ships as a single Maven artifact:
com.wink:winkpay-sdk:1.2.0-native
The customer-facing capture and confirmation UI is implemented as native Android views .
Integration paths at a glance
| You want… | Use | Section |
|---|---|---|
| One APK that contains your POS UI and the WinkPay biometric engine in-process | Native embedded SDK (this doc) | §1–§11 below |
| Two separate APKs on the same device (your POS + a standalone WinkPay app) talking via Android intents | Companion app (intent-based IPC) | TBA |
| Your POS to drive a WinkPay device over the LAN (mDNS + HTTPS + WebSocket) | Wireless local server | TBA |
Most integrators want the embedded SDK. Use the wireless / companion paths only when you specifically need cross-process or cross-device delivery.
Table of contents
- Quick start
- Prerequisites & build setup
AndroidManifest.xmlcontributions- Public API surface
- Environment file (.env content)
- Lifecycle & threading
- Customer-facing display (dual-display devices)
- Palm capture details
- Error handling
- Build notes & APK size
- Troubleshooting
1. Quick start
The minimum integration is roughly 30 lines of Kotlin: initialize the SDK, register a callback, launch the customer-display Activity.
1.1 settings.gradle.kts
settings.gradle.ktsThe SDK is delivered as a local Maven repository (a directory tree). Unzip the deliverable under vendor/winkpay-sdk/winkpay-sdk-repo in your project and declare the repos:
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS)
repositories {
google()
mavenCentral()
// WinkPay SDK (vendored local Maven repo, shipped in the SDK zip).
maven { url = uri("${rootDir}/vendor/winkpay-sdk/winkpay-sdk-repo") }
}
}1.2 app/build.gradle.kts
app/build.gradle.ktsandroid {
defaultConfig {
minSdk = 24
ndk { abiFilters += "arm64-v8a" } // SDK ships only arm64-v8a
}
packaging {
jniLibs {
useLegacyPackaging = true // bundled native libs must extract to lib/, see §2
}
}
}
dependencies {
implementation("com.wink:winkpay-sdk:1.2.0-native")
}That single line pulls in everything the SDK needs: OkHttp, CameraX, ML Kit Face Detection, AndroidX Fragment + Lifecycle, ViewPager2, RecyclerView, Kotlinx serialization + coroutines, plus the SDK's bundled native runtime.
1.3 Initialize once and start a payment
import android.app.ActivityOptions
import android.content.Context
import android.hardware.display.DisplayManager
import android.os.Bundle
import android.view.Display
import androidx.appcompat.app.AppCompatActivity
import com.wink.winkpay.WinkPaySdk
import com.wink.winkpay.embedded.EmbeddedPaymentCallback
import com.wink.winkpay.embedded.EmbeddedPaymentError
import com.wink.winkpay.embedded.EmbeddedPaymentRequest
import com.wink.winkpay.embedded.EmbeddedPaymentResult
import com.wink.winkpay.embedded.WinkPayEmbedded
import java.util.UUID
class PosMainActivity : AppCompatActivity() {
private lateinit var winkPay: WinkPayEmbedded
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 1. Read the env file you were issued by WinkPay (issue per-merchant).
val envContent = resources.openRawResource(R.raw.winkpay_env)
.bufferedReader().readText()
// 2. Initialize the SDK.
winkPay = WinkPayEmbedded.init(
context = this,
config = WinkPaySdk.Config(
envFileContent = envContent,
sdkEnvironment = "stage", // "stage" or "prod"
useNativeUi = true,
),
)
findViewById<View>(R.id.payBtn).setOnClickListener { startFacePayment() }
}
private fun startFacePayment() {
// 3. Register the result callback for this payment.
val request = EmbeddedPaymentRequest(
requestId = UUID.randomUUID().toString(),
requestType = "PAY_FACE", // PAY | PAY_FACE | PAY_PALM
amount = 1500L, // minor units (cents)
currency = "USD",
orderId = "ORDER-${System.currentTimeMillis()}",
tenderId = "TENDER-001",
cameraIndex = 0, // 0 = back, 1 = front
rotationAngle = 0,
)
winkPay.startPayment(request, object : EmbeddedPaymentCallback {
override fun onSuccess(result: EmbeddedPaymentResult) {
// Payment processed by the SDK (returnExternalTokens=false default):
// result.transactionId, result.amount, result.winkTag
// External-token mode (returnExternalTokens=true):
// result.winkCardToken, result.winkTag, result.amount
}
override fun onCancelled(requestId: String) { /* user cancelled */ }
override fun onFailure(error: EmbeddedPaymentError) {
// error.errorCode is "AUTH_FAILED" or "LIVENESS_FAILED" today.
// See §9 for the full list.
}
})
// 4. Launch the customer display. On single-display devices pass
// Display.DEFAULT_DISPLAY (or omit) and you are done.
val displayId = pickCustomerDisplayId()
val intent = winkPay.createNativeIntent(this, displayId)
val opts = ActivityOptions.makeBasic().setLaunchDisplayId(displayId)
startActivity(intent, opts.toBundle())
}
private fun pickCustomerDisplayId(): Int {
val dm = getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
return dm.displays.firstOrNull {
it.displayId != Display.DEFAULT_DISPLAY && it.isValid
}?.displayId ?: Display.DEFAULT_DISPLAY
}
}That's the whole integration. Everything below is reference material.
2. Prerequisites & build setup
| Requirement | Value | Notes |
|---|---|---|
| Android Gradle Plugin | 8.5+ (tested with 8.5.2) | AAR metadata format |
| Gradle | 8.7+ | required by AGP 8.5 |
| Kotlin | 2.0+ | SDK is compiled with 2.1.0 |
minSdk | 24 | SDK runtime minimum |
compileSdk / targetSdk | 34+ | AndroidX compatibility |
| Java toolchain | 11+ | — |
| Device ABI | arm64-v8a only | The SDK ships native libraries for arm64-v8a only; x86_64 emulators are not supported |
useLegacyPackaging = trueis mandatory. Without it, the SDK's bundled native libraries stay insidebase.apkinstead of being extracted tolib/arm64-v8a/, and the SDK fails to initialize at runtime. AGP 8 makes legacy packaging opt-in.
3. AndroidManifest.xml contributions
AndroidManifest.xml contributionsThe SDK manifest declares the permissions and Activities the customer display needs; manifest merger pulls them into your app automatically. You do not need to copy these into your own manifest.
Inherited from the SDK:
| Item | Why |
|---|---|
android.permission.CAMERA | Face + palm capture |
android.permission.INTERNET | Auth + payment APIs |
android.permission.RECORD_AUDIO, MODIFY_AUDIO_SETTINGS | Required by the camera plugin's Manifest.permission set; not actually recorded by the native path |
android.permission.FOREGROUND_SERVICE | Reserved for the wireless server flow |
<uses-feature android:name="android.hardware.camera"/> | Play Store filter |
| The SDK's customer-display Activity | Launched by createNativeIntent |
You only add what you need for your own POS UI — typically just your launcher Activity and any platform-specific intent-filter (e.g. a Clover custom-tender filter, if applicable).
CAMERA is required at runtime, but the SDK does not request it for you. You must request
android.permission.CAMERAfrom the user yourself before launching a payment — typically in your app's first-launch flow or right before callingstartPayment. If the permission is not granted when the customer-display Activity starts, the camera preview shows black and the biometric session eventually fails (there is no "permission denied" callback — request the permission proactively).
4. Public API surface
These are the only types third-party integrators are expected to touch. Everything else under com.wink.winkpay.* is internal.
4.1 WinkPaySdk and WinkPaySdk.Config
WinkPaySdk and WinkPaySdk.Configcom.wink.winkpay.WinkPaySdk is a singleton. You don't normally call it directly — WinkPayEmbedded.init(...) calls it for you. Configuration is the data class WinkPaySdk.Config:
WinkPaySdk.Config(
envFileContent: String? = null,
sdkEnvironment: String = "stage",
useNativeUi: Boolean = false,
enableHttpsServer: Boolean = false,
serverPort: Int = 8443,
)| Flag | Default | Effect |
|---|---|---|
envFileContent | null | Raw .env-formatted content (see §5). Always pass content explicitly; the null branch only exists for internal builds and is not a supported integrator path. |
sdkEnvironment | "stage" | "stage" or "prod". Selects baked-in URLs for the auth, payment, and palm APIs. Any other value throws. |
useNativeUi | false | Always set to true. Retained as a constructor parameter for backward compatibility; only true is supported for new integrations. Pass it explicitly in every Config(...) call. |
enableHttpsServer | false | Starts the Ktor HTTPS server on serverPort for wireless POS integration (POS_INTEGRATION.md). Off by default — enabling it costs ~30–50 MB of RAM (Netty thread pools, direct ByteBuffers, TLS). |
serverPort | 8443 | TCP port for the wireless server. |
4.2 WinkPayEmbedded
WinkPayEmbeddedcom.wink.winkpay.embedded.WinkPayEmbedded is the integration facade.
object Companion {
fun init(
context: Context,
config: WinkPaySdk.Config = WinkPaySdk.Config(),
envFileName: String = ".env.clover979", // legacy fallback if config.envFileContent is null
): WinkPayEmbedded
fun getInstance(): WinkPayEmbedded // throws if init() not called
}
fun startPayment(request: EmbeddedPaymentRequest, callback: EmbeddedPaymentCallback)
fun startCheckin(request: EmbeddedCheckinRequest, callback: EmbeddedPaymentCallback)
fun createNativeIntent(context: Context, displayId: Int = -1): Intent
fun cancelPayment()
fun shutdown()| Method | When to call | Effect | |
|---|---|---|---|
init(context, config) | Once, early — typically Activity.onCreate or Application.onCreate. Subsequent calls are no-ops. | Initializes the native API layer (parses env, picks stage/prod URLs), wires up the optional wireless server, prepares for startPayment. | |
startPayment(request, callback) | Once per payment, before launching the Activity. | Stores order data and registers your callback. Both must be called from the same process that will host the customer-display Activity. | |
startCheckin(request, callback) | Identity-only flow (no payment). | Same as startPayment but skips card selection — returns user identity + saved cards. See §4.6. | |
createNativeIntent(ctx, displayId) | Right after startPayment. | Returns an Intent for the SDK's customer-display Activity. Pass displayId >= 0 for a secondary display; the method adds `FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_MULTIPLE_TASKautomatically. You still need to passActivityOptions.makeBasic().setLaunchDisplayId(displayId)tostartActivity` — see §7. |
cancelPayment() | If you need to abort programmatically (e.g. POS-side timeout). | Triggers onCancelled on the active callback. | |
shutdown() | When you're done with WinkPay (e.g. POS app exiting). | Unregisters callbacks, resets state, stops the wireless server, releases internal singletons. |
4.3 EmbeddedPaymentRequest
EmbeddedPaymentRequestdata class EmbeddedPaymentRequest(
val requestId: String, // your unique id; UUIDv4 recommended
val requestType: String, // "PAY" | "PAY_FACE" | "PAY_PALM"
val amount: Long, // minor units (cents). Must be > 0
val currency: String = "USD", // ISO 4217
val orderId: String, // your POS order id; non-blank
val tenderId: String, // your tender id (free-form, non-blank)
val ageRestrictedPurchase: Boolean = false, // gates capture behind merchant override
val rotationAngle: Int = 0, // 0/90/180/270 — set per device orientation
val cameraIndex: Int = 0, // 0 = back, 1 = front
val returnExternalTokens: Boolean = false, // see §4.7
val returnOnFailure: Boolean = false, // see §4.8
)PAY shows the face/palm chooser; PAY_FACE and PAY_PALM jump straight into the corresponding capture screen.
4.4 EmbeddedPaymentResult
EmbeddedPaymentResultdata class EmbeddedPaymentResult(
val requestId: String,
val transactionId: String?, // null in external-token mode
val amount: Long,
val winkTag: String? = null, // biometric-resolved customer id
val winkToken: String? = null, // payment token, when available
val winkCardToken: String? = null, // selected card token, populated in external-token mode
val mfaUsed: Boolean = false, // true if MFA was performed during this auth
)4.5 EmbeddedPaymentError and EmbeddedPaymentCallback
EmbeddedPaymentError and EmbeddedPaymentCallbackdata class EmbeddedPaymentError(
val requestId: String,
val errorCode: String,
val errorMessage: String,
)
interface EmbeddedPaymentCallback {
fun onSuccess(result: EmbeddedPaymentResult)
fun onCancelled(requestId: String)
fun onFailure(error: EmbeddedPaymentError)
fun onCheckinSuccess(result: EmbeddedCheckinResult) { /* default delegates to onSuccess */ }
}All callback methods are dispatched on the main thread.
4.6 Check-in mode (identity-only, no payment)
Use this for kiosk check-in and similar flows where you want to identify a returning user but not charge a card. The SDK runs the biometric capture and returns the full login response.
data class EmbeddedCheckinRequest(
val requestId: String,
val currency: String = "USD",
val rotationAngle: Int = 0,
val cameraIndex: Int = 0,
val biometricType: String = "face", // "face" or "palm"
)
data class EmbeddedCheckinResult(
val requestId: String,
val winkTag: String?,
val accessToken: String?,
val loginResponseJson: String, // full server JSON: user, cards, addresses, faceResponse
)
winkPay.startCheckin(EmbeddedCheckinRequest(requestId = UUID.randomUUID().toString()),
object : EmbeddedPaymentCallback {
override fun onCheckinSuccess(result: EmbeddedCheckinResult) {
// Parse result.loginResponseJson — userPaymentDetails.user, .cards, .shippingAddresses
}
override fun onSuccess(r: EmbeddedPaymentResult) {}
override fun onCancelled(requestId: String) {}
override fun onFailure(error: EmbeddedPaymentError) {}
})4.7 returnExternalTokens — let the host process the charge
returnExternalTokens — let the host process the chargeIf your processor already handles the actual card charge and you only need WinkPay to identify the customer and pick a card:
returnExternalTokens | UI label | SDK behaviour | Result fields |
|---|---|---|---|
false (default) | Pay Now | SDK charges the selected card via WinkPay's payment rails. | transactionId, amount, winkTag |
true | Confirm | SDK does not charge the card. Returns the selected card's winkCardToken + winkTag. | winkCardToken, winkTag, amount |
4.8 returnOnFailure — surface auth failures to the host
returnOnFailure — surface auth failures to the hostBy default, when biometric authentication fails (face not recognized, liveness rejected, etc.) the native UI shows a Retry / See Options error screen. If you want control to return to your POS app instead:
EmbeddedPaymentRequest(..., returnOnFailure = true)The native Activity finishes with onFailure(EmbeddedPaymentError(...)) on the registered callback (see §9 for the error codes).
5. Environment file (.env content)
WinkPay is configured per merchant via a small .env-style file you receive during onboarding. Pass its raw text as WinkPaySdk.Config.envFileContent. Format is KEY=VALUE per line; # comments and blank lines are ignored; surrounding ' or " on values are stripped.
| Key | Required | Used by | Notes |
|---|---|---|---|
CLIENT_ID | ✓ | Auth API | OAuth client id issued by WinkPay |
MERCHANT_CLIENT_SECRET | ✓ | Auth API | OAuth client secret |
BASE_LOGIN_URL | optional | Auth API | Override the baked-in stage/prod URL — only set for non-standard deployments |
BASE_PAYMENT_URL | optional | Payment API | Override of baked-in URL |
BASE_API_URL_PALM | optional | Palm API | Override of baked-in URL |
PMS_PROJECT_ID | optional | Palm API | Override of baked-in project id |
Stage vs prod URLs are baked into the SDK and selected via Config.sdkEnvironment. The override keys above are honored if present, but for the vast majority of integrations you only need CLIENT_ID and MERCHANT_CLIENT_SECRET.
Where to keep it. A common pattern is
app/src/main/res/raw/winkpay_env(note: no extension — Android forbids dots inres/rawfilenames). Read withresources.openRawResource(R.raw.winkpay_env).bufferedReader().readText()as in §1.3. Treat the file as a secret in your build pipeline.
6. Lifecycle & threading
What happens when you call startPayment and launch the Activity
startPayment and launch the ActivitystartPaymentstores the order data in process-scoped state and registers your callback.createNativeIntentreturns an Intent for the SDK's customer-display Activity.- The Activity reads the order data in
onCreate. IfrequestTypewasPAY_FACEorPAY_PALM, it routes straight to the corresponding capture screen; otherwise it shows the face/palm chooser. - Capture → API auth → consent / MFA dialogs as required → card selection → either pay or return external tokens.
- The Activity finishes. Your callback fires (on the main thread):
onSuccess— payment processed or external tokens returnedonCancelled— user tapped cancel, or you calledcancelPayment()onFailure— auth/payment failed andreturnOnFailure=true, or a terminal error occurred
Threading
- All callback methods are dispatched on the main thread.
- API calls (auth, payment, palm) run on a single-threaded executor inside the Activity; the Activity tears it down in
onDestroy. - Camera analysis runs on CameraX's analyzer executor; results are posted back to the main thread before any UI / API call.
What gets torn down on Activity destroy
The SDK's customer-display Activity releases all biometric, network, and camera resources automatically when it finishes. You don't need to do anything in your own lifecycle to "clean up" the SDK between payments — just register a fresh callback before the next startPayment.
Calling startPayment twice
startPayment twicestartPayment overwrites the previously-registered callback. It does not serialize concurrent calls — only one biometric session can be in flight at a time. If you need to start a second payment after the first completes, wait for the callback to fire, then call startPayment again. There is no explicit "session id" — the requestId you supply is what threads back to your callback.
Process death / re-entry
The order data and callback registration are process-scoped and do not survive process death. If the OS kills your app while the Activity is running, restart the flow from your POS — there is no resumable session.
7. Customer-facing display (dual-display devices)
On single-display devices you can ignore this section: pass Display.DEFAULT_DISPLAY (or omit displayId) and you're done.
On dual-display POS hardware (separate merchant + customer screens), the capture UI should land on the customer display. Two things must be true:
- The Intent must include
FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_MULTIPLE_TASK—createNativeIntent(ctx, displayId)adds these for you whendisplayId >= 0. - You must pass an
ActivityOptionsbundle. Flags alone do not target a display.
val displays = (getSystemService(Context.DISPLAY_SERVICE) as DisplayManager).displays
val customerDisplayId = displays.firstOrNull {
it.displayId != Display.DEFAULT_DISPLAY && it.isValid
}?.displayId ?: Display.DEFAULT_DISPLAY
val intent = winkPay.createNativeIntent(this, customerDisplayId)
val opts = ActivityOptions.makeBasic().setLaunchDisplayId(customerDisplayId)
startActivity(intent, opts.toBundle())Without the options bundle you'll see Failed to put TaskRecord on display N in logcat and the Activity will land on the merchant display.
Relaunching your own POS Activity on the customer display
If your POS UI also belongs on the customer display:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (relaunchOnCustomerDisplayIfNeeded()) return
// ... normal onCreate
}
private fun relaunchOnCustomerDisplayIfNeeded(): Boolean {
if (intent.getBooleanExtra("__relaunched", false)) return false
val dm = getSystemService(DisplayManager::class.java)
val customer = dm.displays.firstOrNull {
it.displayId != Display.DEFAULT_DISPLAY && it.isValid
} ?: return false
val current = if (Build.VERSION.SDK_INT >= 30) display?.displayId
else windowManager.defaultDisplay.displayId
if (current == customer.displayId) return false
val relaunch = Intent(this, javaClass).apply {
putExtra("__relaunched", true)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
}
startActivity(relaunch, ActivityOptions.makeBasic()
.setLaunchDisplayId(customer.displayId).toBundle())
finish()
return true
}Add android:taskAffinity=".your_customer_task" to your Activity in the manifest so Android grants a fresh task on the customer display.
The SDK's customer-display Activity is already configured to land on a fresh task — you do not need to configure it.
8. Palm capture details
The SDK runs palm capture on-device, including liveness verification. The capture screen positions the user's hand, prompts a randomized gesture, and verifies the gesture before delivering a palm model to the WinkPay backend for identification. The full flow — including model build and identify call — is internal to the SDK; your host code only sees the eventual onSuccess or onFailure callback.
If liveness is rejected the errorMessage starts with "Liveness check…" (e.g. "Liveness check timed out", "Liveness check failed"). The SDK turns this into errorCode = "LIVENESS_FAILED" if returnOnFailure is set; otherwise it routes to the SDK's retry screen.
When debugging false-rejects, capture logcat with tag PalmCaptureSession — every reject logs a one-line reason.
9. Error handling
9.1 errorCode values delivered via EmbeddedPaymentError
errorCode values delivered via EmbeddedPaymentErrorThe native flow currently produces two error codes:
errorCode | Cause |
|---|---|
LIVENESS_FAILED | The biometric session failed because liveness was rejected (face or palm). The user-visible message starts with "Liveness check…". |
AUTH_FAILED | Any other terminal authentication / payment failure: face/palm not recognized, server error, network error, payment declined, etc. The errorMessage carries the human-readable reason (server-supplied where available). |
onFailure is only called when:
- You set
EmbeddedPaymentRequest.returnOnFailure = true, or - A terminal error occurs that the SDK does not have a retry path for (e.g. network failure during a payment that already debited).
By default (returnOnFailure = false) the native UI shows a Retry / See Options screen and the user can re-attempt without your code being notified.
9.2 User-visible strings you may see in logcat / errorMessage
errorMessageNot all of these surface to your callback (most route to the native retry screen unless returnOnFailure = true), but you may see them while debugging:
| Message prefix | Meaning |
|---|---|
Liveness check failed. | Palm or face liveness rejected the gesture/blink |
Liveness check timed out | Gesture window expired before a PASS verdict |
Liveness check incomplete | Palm left the frame mid-gesture |
Face not recognized / User not found | The captured face did not match any enrolled user for this merchant |
Payment declined | Card-level decline from the processor |
Network error | OkHttp transport error reaching the auth or payment API |
9.3 Initialization-time exceptions
These come out as standard Kotlin exceptions, not via the callback:
| Exception | Cause |
|---|---|
IllegalStateException("WinkPaySdk not initialized") | You accessed an API that requires init before calling WinkPayEmbedded.init |
IllegalStateException("$key is not configured in .env") | A required env key (CLIENT_ID, MERCHANT_CLIENT_SECRET, …) was missing or blank when the API layer tried to use it |
IllegalArgumentException("environment must be \"stage\" or \"prod\"") | Config.sdkEnvironment was something else |
9.4 Companion-app error codes
When integrating via the companion app (intent-based IPC) instead of the embedded SDK, you'll see an additional set of error codes — INVALID_REQUEST, UNAUTHORIZED_CALLER, IDEMPOTENT_REJECT, PAYMENT_FAILED, NOT_IMPLEMENTED. Those are documented in POS_INTEGRATION.md, which is the authoritative reference for that integration path.
10. Build notes & APK size
10.1 ProGuard / R8 — consumer rules are auto-propagated
The SDK ships consumer-rules.pro and declares it via consumerProguardFiles, so it is automatically merged into your app's R8 rules at build time. You do not need to copy any WinkPay-specific rules into your app — the bundled set covers the public API surface (WinkPaySdk, WinkPaySdk.Config, WinkPayEmbedded, the EmbeddedPayment* data classes) and the kotlinx.serialization companion serializers used by the API model classes.
If you minify your app and see R8 warnings about other transitive dependencies you pull in (OkHttp, ML Kit, your POS vendor SDK, etc.), add the relevant -keep / -dontwarn rules in your own app/proguard-rules.pro — those live outside the WinkPay SDK and are your app's responsibility.
10.2 APK size impact
The SDK adds roughly 10 MB compressed (~25 MB uncompressed, arm64-v8a only) to your APK. The ML Kit face-detection model is delivered at runtime by Google Play Services — zero APK cost.
To keep the build lean, set ndk.abiFilters += "arm64-v8a" (the SDK only ships arm64-v8a anyway — including other ABIs just inflates your APK with native libs that won't run).
10.3 Versioning
Maven coordinates are com.wink:winkpay-sdk:<version>. The current recommended version is 1.2.0-native. The SDK versioning convention is:
1.x.0— feature releases1.x.y— bug-fix releases
When you upgrade the SDK, swap the version in app/build.gradle.kts and copy the new local Maven repo over vendor/winkpay-sdk/winkpay-sdk-repo. No code changes are required for patch-level upgrades within the same major.
11. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
IllegalStateException: WinkPaySdk not initialized | API used before WinkPayEmbedded.init() returned | Move init earlier (Application.onCreate or top of Activity.onCreate). |
IllegalStateException: CLIENT_ID is not configured in .env | envFileContent was null / empty / missing the key | Verify the file you read into envContent actually contains the keys; check that Config(envFileContent = …) was passed (and not silently dropped). |
| SDK init crashes with a native-library load error | Bundled native libs are being packaged compressed inside base.apk | Set packaging.jniLibs.useLegacyPackaging = true in your app's build.gradle.kts. |
Failed to put TaskRecord on display N (then Activity lands on the merchant display) | startActivity called without ActivityOptions.setLaunchDisplayId(...) | Pass opts.toBundle() as the second arg. Flags alone are insufficient — see §7. |
Customer display shows a black preview; capture eventually times out or fails with LIVENESS_FAILED | Host app didn't request android.permission.CAMERA at runtime before launching the payment. There is no "permission denied" callback — the camera just opens to black and the biometric session never sees a usable frame. | Call ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.CAMERA), REQ_CODE) before winkPay.startPayment(...). Don't proceed to createNativeIntent until the user grants it. |
Payment succeeds but winkCardToken / transactionId is null in the result | Mode confusion: returnExternalTokens=true populates winkCardToken and leaves transactionId null; returnExternalTokens=false does the opposite | Read §4.7 — pick the mode that matches who is processing the charge. |
| Palm liveness consistently fails for a real user on a real device | One of the SDK's local liveness guards is rejecting | Capture logcat with tag PalmCaptureSession — each reject logs the precise reason. If you see false rejects in normal use, file a bug with the log excerpt. |
INSTALL_FAILED_INVALID_APK: Failed to extract native libraries, res=-2 on adb install | Same root cause as the native-library load error above | useLegacyPackaging = true. |
Updated 8 days ago
