Wink Identity Mobile SDK (Android)
Wink Android SDK Integration Guide
Requirements
| Requirement | Minimum |
|---|---|
| Android minSdk | 24 (Android 7.0) |
| Android compileSdk | 34 |
| Kotlin | 2.1.0+ |
| JVM Target | 11 |
| Gradle | 7.0+ |
Installation
-
Obtain the Wink SDK Maven repository (the
aar_distribution/directory or a hosted Maven URL). -
Add the Maven repository in your
settings.gradle:
dependencyResolutionManagement {
repositories {
maven { url '/path/to/aar_distribution' } // local path or hosted URL
google()
mavenCentral()
}
}- Add the dependencies in your
app/build.gradle:
dependencies {
releaseImplementation 'com.example.wink_identity:flutter_release:1.0'
implementation 'com.wink:wink-identity-android:2.1.1'
}Permissions
The SDK's Flutter plugins auto-merge the required permissions into your app's manifest. No manual declarations are needed:
| Permission | Usage |
|---|---|
CAMERA | Face and palm biometric enrollment, ID document scanning |
RECORD_AUDIO | Voice biometric enrollment |
INTERNET | API communication |
ACCESS_NETWORK_STATE | Network connectivity checks |
READ_EXTERNAL_STORAGE | Image picker for document scanning |
Configuration
Configure the SDK once at app startup (e.g. in Application.onCreate() or your launch Activity.onCreate()).
Basic Configuration
import com.wink.identity.*
WinkSDKAdapter.configure(applicationContext, WinkConfiguration(
keyId = "your_key_id",
merchantId = "your_merchant_id",
environment = "prod" // "dev", "staging", or "prod"
))Full Configuration
WinkSDKAdapter.configure(applicationContext, WinkConfiguration(
keyId = "your_key_id",
merchantId = "your_merchant_id",
// Biometric / Face
livenessEnabled = true,
faceConfidenceThreshold = 90,
mfaOrder = listOf("face", "voice"),
retriesAllowed = 3,
// Palm
enablePalm = true,
allowPalmFlip = false,
palmCameraDir = 1,
mfaGestureLiveness = false,
enrollGestureLiveness = false,
// OAuth / SSO
auth0Domain = "your-tenant.auth0.com",
auth0ClientId = "your_client_id",
auth0MtmClientId = "your_mtm_client_id",
auth0MtmClientSecret = "your_mtm_client_secret",
oktaClientId = "your_okta_client_id",
allowAppleLogin = true,
allowGoogleLogin = true,
allowPazeLogin = false,
iamFirst = true,
iamFirstProvider = "auth0",
// Features
allowIdScan = true,
enableVoice = true,
enableDocumentVerification = true,
// Environment
environment = "prod",
// Signing key (from Android raw resource)
signingKeyRawResourceId = R.raw.wink_private_key,
// Engine pre-warming (default: true)
autoPreWarm = true
))Configuration Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
keyId | String | required | Your Wink API key ID |
merchantId | String | required | Your merchant identifier |
environment | String | "dev" | "dev", "staging", or "prod" |
livenessEnabled | Boolean | false | Enable face liveness detection |
faceConfidenceThreshold | Int | 90 | Face match confidence threshold (0-100) |
mfaOrder | List<String> | [] | MFA method priority order. Options: "Palm", "Voice", "Okta" |
retriesAllowed | Int | 0 | Number of biometric retries before failure |
presentExtProfile | Boolean | false | Show extended profile fields |
enablePalm | Boolean | false | Enable palm biometric |
allowPalmFlip | Boolean | false | Allow palm flip gesture |
palmCameraDir | Int | 1 | Palm camera direction |
mfaGestureLiveness | Boolean | false | Enable gesture-based liveness during MFA |
enrollGestureLiveness | Boolean | false | Enable gesture-based liveness during enrollment |
auth0Domain | String? | null | Auth0 tenant domain |
auth0ClientId | String? | null | Auth0 client ID |
auth0MtmClientId | String? | null | Auth0 machine-to-machine client ID |
auth0MtmClientSecret | String? | null | Auth0 machine-to-machine client secret |
oktaClientId | String? | null | Okta client ID for native Okta SSO |
allowAppleLogin | Boolean | false | Enable Sign in with Apple |
allowGoogleLogin | Boolean | false | Enable Google Sign-In |
allowPazeLogin | Boolean | false | Enable Paze login |
iamFirst | Boolean | false | Show IAM login before biometrics |
iamFirstProvider | String? | null | IAM provider name ("auth0", "okta", "google", "apple"). Required when iamFirst is true |
allowIdScan | Boolean | false | Enable ID document scanning |
enableVoice | Boolean | false | Enable voice biometric |
enableDocumentVerification | Boolean | false | Enable document verification |
returnAgeStatus | Boolean | false | Return age verification status in responses |
signingKeyRawResourceId | Int? | null | Android raw resource ID for signing PEM (e.g. R.raw.wink_private_key) |
autoPreWarm | Boolean | true | Pre-warm Flutter engine on configure |
Okta SSO Setup
If your integration uses Okta SSO (enabled by setting oktaClientId in configuration), complete these steps.
1. Implement WinkAuthProvider
WinkAuthProviderThe SDK calls your provider mid-flow when it needs Okta authentication or token revocation:
interface WinkAuthProvider {
fun performBrowserSignIn(completion: (WinkAuthResult) -> Unit)
fun revokeCurrentToken(completion: (Boolean) -> Unit)
}Implement it and assign before launching SDK flows:
class MyOktaProvider : WinkAuthProvider {
override fun performBrowserSignIn(completion: (WinkAuthResult) -> Unit) {
// Launch Okta sign-in (e.g. via Okta OIDC SDK), then:
completion(WinkAuthResult(
idToken = idToken,
givenName = user.givenName,
familyName = user.familyName,
email = user.email,
userId = user.sub
))
}
override fun revokeCurrentToken(completion: (Boolean) -> Unit) {
// Revoke stored token. Best-effort.
completion(true)
}
}
// After configure():
WinkSDKAdapter.authProvider = MyOktaProvider()2. Configure Okta Parameters
WinkSDKAdapter.configure(applicationContext, WinkConfiguration(
keyId = "your_key_id",
merchantId = "your_merchant_id",
oktaClientId = "your_okta_client_id", // enables native Okta
mfaOrder = listOf("Palm", "Voice", "Okta"), // include Okta in MFA chain
iamFirst = true, // optional: show Okta login before biometrics
iamFirstProvider = "okta", // required when iamFirst is true
environment = "prod",
signingKeyRawResourceId = R.raw.wink_private_key
))When
oktaClientIdis set, the SDK uses native Okta browser sign-in. When it isnull, the SDK falls back to Auth0-hosted Okta authentication (requiresauth0Domainandauth0ClientId).
Signing Key Setup
The SDK signs API requests using an RSA private key. The key is held in memory (not persisted to Android Keystore). There are two ways to provide the key:
Option A: Auto-import from raw resource (recommended)
Place your wink_private_key.pem file in res/raw/, then set signingKeyRawResourceId in your configuration:
WinkSDKAdapter.configure(applicationContext, WinkConfiguration(
keyId = "your_key_id",
merchantId = "your_merchant_id",
signingKeyRawResourceId = R.raw.wink_private_key // res/raw/wink_private_key.pem
))Option B: Manual import
// Import from a PEM string directly
WinkKeystoreSigner.shared.importPrivateKey(pemString)
// Or import from a raw resource
WinkKeystoreSigner.shared.importPrivateKeyFromResource(context, R.raw.wink_private_key)Signing Key Utilities
// Check if a signing key is present
if (WinkKeystoreSigner.shared.hasSigningKey) {
println("Signing key is ready")
}
// Remove the signing key (e.g. on logout)
WinkKeystoreSigner.shared.clearSigningKey()Supported formats: Both PKCS#8 and PKCS#1 PEM keys are accepted. PKCS#1 keys are automatically wrapped to PKCS#8 internally.
Usage — Activity / Fragment
All SDK methods launch a portrait-locked Flutter activity from the activity you provide. Results are delivered via a callback on the main thread.
Enroll a New User
val request = WinkEnrollRequest(
requiredEnrollment = "face", // "face", "voice", or "palm"
firstName = "Jane",
lastName = "Doe",
email = "[email protected]"
)
WinkSDKAdapter.enroll(activity = this, request = request) { result ->
if (result.isSuccess) {
println("Enrolled! Token: ${result.winkToken}")
} else {
println("Enrollment failed: ${result.message}")
}
}Authenticate an Existing User
WinkSDKAdapter.authenticate(activity = this) { result ->
if (result.isSuccess) {
println("Authenticated: ${result.winkToken}")
println("Access Token: ${result.accessToken}")
}
}Verify Identity
WinkSDKAdapter.verify(
activity = this,
winkTokenComp = "existing_wink_token",
documentCheck = true
) { result ->
if (result.isSuccess) {
println("Verification passed")
}
}Show Profile / Account Management
WinkSDKAdapter.profile(
activity = this,
accessToken = "user_access_token",
displayMode = "combined" // "profile", "wallet", or "combined"
) { result ->
println("Profile closed")
}Launch (Typed Convenience)
A single entry point for post-authentication flows with a typed WinkLaunchMode:
WinkSDKAdapter.launch(
activity = this,
mode = WinkLaunchMode.COMBINED, // ACCOUNT, WALLET, or COMBINED
accessToken = "user_access_token"
) { result ->
println("Launch complete")
}Payment / Wallet
val walletConfig = WinkWalletConfig(
accessToken = "user_access_token",
merchantId = "merchant_xyz",
extra = mapOf("customField" to "value") // additional fields forwarded to Flutter
)
WinkSDKAdapter.start(activity = this, config = walletConfig) { result ->
if (result.isSuccess) {
println("Payment status: ${result.status}")
println("Transaction ID: ${result.transactionId}")
}
}Open Wallet Directly
WinkSDKAdapter.openWallet(
activity = this,
accessToken = "user_access_token",
merchantId = "merchant_xyz"
) { result ->
println("Wallet closed: ${result.status}")
}Usage — Kotlin Coroutines (async/await)
All methods have suspend overloads:
// Authenticate
val result = WinkSDKAdapter.authenticateAsync(activity = this)
// Enroll
val enrollResult = WinkSDKAdapter.enrollAsync(
activity = this,
request = WinkEnrollRequest(requiredEnrollment = "face")
)
// Profile
val profileResult = WinkSDKAdapter.profileAsync(
activity = this,
accessToken = "token"
)
// Launch
val launchResult = WinkSDKAdapter.launchAsync(
activity = this,
mode = WinkLaunchMode.COMBINED,
accessToken = "token"
)
// Open Wallet
val walletResult = WinkSDKAdapter.openWalletAsync(
activity = this,
accessToken = "token"
)Usage — Jetpack Compose
The SDK provides Composable wrappers for all flows. Control presentation with a Boolean state:
Authenticate
import com.wink.identity.compose.*
@Composable
fun LoginScreen() {
var showAuth by remember { mutableStateOf(false) }
var token by remember { mutableStateOf<String?>(null) }
Button(onClick = { showAuth = true }) {
Text("Sign In")
}
WinkAuthenticate(
isPresented = showAuth,
onResult = { result -> token = result.winkToken },
onDismiss = { showAuth = false }
)
}Enroll
var showEnroll by remember { mutableStateOf(false) }
Button(onClick = { showEnroll = true }) {
Text("Enroll")
}
WinkEnroll(
isPresented = showEnroll,
request = WinkEnrollRequest(requiredEnrollment = "face"),
onResult = { result -> println(result.winkToken ?: "cancelled") },
onDismiss = { showEnroll = false }
)Launch (Account / Wallet / Combined)
var showProfile by remember { mutableStateOf(false) }
Button(onClick = { showProfile = true }) {
Text("My Account")
}
WinkLaunch(
isPresented = showProfile,
mode = WinkLaunchMode.COMBINED,
accessToken = accessToken,
onResult = { result -> /* handle result */ },
onDismiss = { showProfile = false }
)Verify
var showVerify by remember { mutableStateOf(false) }
Button(onClick = { showVerify = true }) {
Text("Verify Identity")
}
WinkVerify(
isPresented = showVerify,
winkTokenComp = winkToken,
documentCheck = true,
onResult = { result -> /* handle result */ },
onDismiss = { showVerify = false }
)Payment
var showPayment by remember { mutableStateOf(false) }
Button(onClick = { showPayment = true }) {
Text("Pay")
}
WinkPayment(
isPresented = showPayment,
config = WinkWalletConfig(accessToken = accessToken),
onResult = { result -> println(result.status ?: "") },
onDismiss = { showPayment = false }
)Open Wallet
var showWallet by remember { mutableStateOf(false) }
Button(onClick = { showWallet = true }) {
Text("Wallet")
}
WinkWallet(
isPresented = showWallet,
accessToken = accessToken,
merchantId = "merchant_xyz",
onResult = { result -> /* handle result */ },
onDismiss = { showWallet = false }
)Token Management
Push updated tokens to all active SDK sessions. If no session is live, the update is queued and delivered when the next session starts.
WinkSDKAdapter.updateTokens(
accessToken = newAccessToken,
refreshToken = newRefreshToken
)Token Refresh Callback
The SDK notifies your app when it needs a fresh token:
WinkSDKAdapter.onTokenRefreshRequested = {
// Refresh your token (e.g. via your token manager)
val newToken = TokenManager.refreshAccessToken()
WinkSDKAdapter.updateTokens(accessToken = newToken, refreshToken = newRefresh)
}Engine Pre-Warming
The Flutter engine is pre-warmed automatically when autoPreWarm is true (the default). This makes the first SDK launch near-instant.
If you set autoPreWarm = false, you can control timing manually:
// Pre-warm ahead of time (e.g. on the screen with a "Sign In" button)
WinkSDKAdapter.preWarmEngine(applicationContext)
// Tear down on memory pressure
override fun onTrimMemory(level: Int) {
super.onTrimMemory(level)
if (level >= ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW) {
WinkSDKAdapter.tearDownPreWarmedEngine()
}
}The pre-warmed engine is consumed on the next SDK launch. The engine is automatically re-warmed after each flow completes.
Result Types
WinkLoginResult
Returned by enroll, authenticate, profile, verify, and launch.
| Property | Type | Description |
|---|---|---|
winkToken | String? | The authenticated user's Wink token |
firstName | String? | User's first name |
lastName | String? | User's last name |
contactNo | String? | User's phone number |
email | String? | User's email |
accessToken | String? | OAuth access token |
refreshToken | String? | OAuth refresh token |
externalIAM | String? | External IAM provider identifier |
dateOfBirth | String? | User's date of birth |
isProfileVerified | Boolean? | Whether the user's profile has been verified |
oktaIdToken | String? | Okta ID token (when using Okta SSO) |
message | String? | Error message (prefixed with wink_ on SDK errors) |
isSuccess | Boolean | true if no wink_* error occurred |
WinkPaymentResult
Returned by start and openWallet.
| Property | Type | Description |
|---|---|---|
status | String? | Payment status |
transactionId | String? | Transaction identifier |
message | String? | Error message |
raw | Map<String, Any> | Full raw payload |
isSuccess | Boolean | true if no wink_* error occurred |
WinkAuthResult
Passed to WinkAuthProvider.performBrowserSignIn(completion:). The host app constructs this from its own Okta sign-in result.
| Property | Type | Description |
|---|---|---|
idToken | String? | Okta ID token |
givenName | String? | User's given name |
familyName | String? | User's family name |
email | String? | User's email |
userId | String? | Okta user ID (sub claim) |
error | String? | Error message (non-nil signals failure) |
Error Handling
Configuration Errors
WinkConfiguration.ValidationError is thrown by configure():
| Error | Description |
|---|---|
MissingKeyId | keyId is empty |
MissingMerchantId | merchantId is empty |
EmptyEnvironment | environment is empty |
InvalidIamFirstProvider | iamFirstProvider must be set when iamFirst is true and null when false |
Result-Level Errors
When a flow fails, WinkLoginResult.message or WinkPaymentResult.message will contain a wink_* prefixed string:
| Message | Meaning |
|---|---|
wink_not_configured | SDK was not configured |
wink_engine_failed_to_run | Flutter engine failed to start |
wink_cancelled | User dismissed the flow without completing |
Check result.isSuccess to quickly determine if the flow completed successfully.
ProGuard / R8
The SDK ships with consumer ProGuard rules that are applied automatically. No additional rules are needed in your app:
-keep class com.wink.identity.** { *; }
-keepclassmembers class com.wink.identity.** { *; }
Complete Activity Example
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.wink.identity.*
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 1. Configure the SDK
WinkSDKAdapter.configure(applicationContext, WinkConfiguration(
keyId = "your_key_id",
merchantId = "your_merchant_id",
signingKeyRawResourceId = R.raw.wink_private_key,
environment = "prod"
))
// 2. Set up token refresh callback
WinkSDKAdapter.onTokenRefreshRequested = {
// Your token refresh logic here
}
findViewById<Button>(R.id.signInButton).setOnClickListener {
signIn()
}
}
private fun signIn() {
// 3. Authenticate
WinkSDKAdapter.authenticate(activity = this) { result ->
if (!result.isSuccess) {
println("Auth failed: ${result.message}")
return@authenticate
}
println("Welcome, ${result.firstName}!")
println("Token: ${result.winkToken}")
// 4. Open wallet after login
val accessToken = result.accessToken ?: return@authenticate
WinkSDKAdapter.openWallet(
activity = this,
accessToken = accessToken
) { paymentResult ->
println("Wallet closed: ${paymentResult.status}")
}
}
}
}Complete Jetpack Compose Example
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import com.wink.identity.*
import com.wink.identity.compose.*
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
WinkSDKAdapter.configure(applicationContext, WinkConfiguration(
keyId = "your_key_id",
merchantId = "your_merchant_id",
signingKeyRawResourceId = R.raw.wink_private_key,
environment = "prod"
))
WinkSDKAdapter.onTokenRefreshRequested = {
// Your token refresh logic here
}
setContent {
MaterialTheme {
MainScreen()
}
}
}
}
@Composable
fun MainScreen() {
var showAuth by remember { mutableStateOf(false) }
var showWallet by remember { mutableStateOf(false) }
var accessToken by remember { mutableStateOf<String?>(null) }
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Button(onClick = { showAuth = true }) {
Text("Sign In")
}
WinkAuthenticate(
isPresented = showAuth,
onResult = { result ->
if (result.isSuccess) {
accessToken = result.accessToken
}
},
onDismiss = { showAuth = false }
)
accessToken?.let { token ->
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = { showWallet = true }) {
Text("Open Wallet")
}
WinkWallet(
isPresented = showWallet,
accessToken = token,
onResult = { result ->
println("Wallet: ${result.status}")
},
onDismiss = { showWallet = false }
)
}
}
}Updated 2 days ago