Wallet Setup (Android)
A wallet is the interface your users interact with to authenticate and complete transactions - typically hosted on a dedicated domain like wallet.example.com. It handles passkey authentication and transaction approval.
The Wallet SDK supports two key user scenarios involving MFA and passkeys:
- Creating a passkey as part of your existing banking sign-up/sign-in flow
- Using that passkey to authenticate payment actions
Prerequisites
- Create an application to obtain a base URL. The SDK uses this base URL to interact with the LoginID authentication service.
- Create an API key with at least the external:verify scope. You’ll need this to request authorization tokens from your backend.
SDK Setup
Requirements
- Android 9+ (API level 28+)
- Android Studio
- Kotlin
Prerequisites
- Create an application to obtain a base URL. The SDK uses this base URL to interact with the LoginID authentication service.
- Create an API key with at least the external:verify scope. You’ll need this to request authorization tokens from your backend.
Before installing the SDK, configure the association between your Android application and your website using a Digital Asset Links file.
Configure Digital Asset Links
Android uses a Digital Asset Links file named assetlinks.json to verify the association between your application and your website.
Find Your Package Name
You can find your application's package name in the app module's build.gradle.kts file under defaultConfig:
android {
defaultConfig {
applicationId = "<PACKAGE_NAME>"
}
}
Generate the SHA-256 Certificate Fingerprint
Use the keytool utility included with the Java Development Kit to retrieve the SHA-256 fingerprint of your application's signing certificate:
keytool -list -v \
-keystore KEYSTORE_PATH \
-alias KEYSTORE_ALIAS \
-storepass KEYSTORE_PASSWORD \
-keypass KEY_PASSWORD
Replace the following values:
KEYSTORE_PATHwith the path to your keystoreKEYSTORE_ALIASwith the signing-key aliasKEYSTORE_PASSWORDwith the keystore passwordKEY_PASSWORDwith the signing-key password
The command output includes the certificate's SHA-256 fingerprint.
For local development, you can use the default Android debug keystore with the following values:
KEYSTORE_ALIAS:androiddebugkeyKEYSTORE_PASSWORD:androidKEY_PASSWORD:android
Use a dedicated signing key for production applications. For applications distributed through Google Play, you can find the app-signing certificate fingerprint in the Google Play Console.
Register the Fingerprint
Register your application's SHA-256 certificate fingerprint in your LoginID application's FIDO2 section before continuing. For instructions, see Adding Android Fingerprint.
Host the assetlinks.json File
Create an assetlinks.json file containing your application's package name and SHA-256 certificate fingerprint:
[
{
"relation": [
"delegate_permission/common.get_login_creds"
],
"target": {
"namespace": "android_app",
"package_name": "<PACKAGE_NAME>",
"sha256_cert_fingerprints": [
"<SHA256_FINGERPRINT>"
]
}
}
]
Replace <PACKAGE_NAME> and <SHA256_FINGERPRINT> with your application's values.
Host the file at the following location:
https://<WEBSITE_DOMAIN>/.well-known/assetlinks.json
The file must be publicly accessible over HTTPS.
Declare the Association in Your Android Application
Add an asset_statements resource to res/values/strings.xml:
<resources>
<string name="asset_statements" translatable="false">
[{
\"include\": \"https://<WEBSITE_DOMAIN>/.well-known/assetlinks.json\"
}]
</string>
</resources>
Then add the corresponding <meta-data> element inside the <application> element of your AndroidManifest.xml file:
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<meta-data
android:name="asset_statements"
android:resource="@string/asset_statements" />
</application>
</manifest>
Set Up the SDK
Download the SDK
After creating your LoginID application, open the Android section of the Get Started guide and download the latest version of the SDK.
The SDK is distributed as Android Archive (.aar) files.
Add the SDK to Your Project
Copy the LoginID SDK artifacts into your app module's libs directory:
app/
└── libs/
├── api.jar
├── core-release.aar
└── mfa-release.aar
Add the SDK artifacts and required dependencies to your app module's build.gradle.kts:
dependencies {
// LoginID SDK
implementation(files("libs/api.jar"))
implementation(files("libs/core-release.aar"))
implementation(files("libs/mfa-release.aar"))
// Required dependencies
implementation("com.squareup.okhttp3:okhttp:5.1.0")
implementation("com.squareup.moshi:moshi:1.15.2")
implementation("com.squareup.moshi:moshi-adapters:1.15.2")
ksp("com.squareup.moshi:moshi-kotlin-codegen:1.15.2")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
implementation("androidx.credentials:credentials-play-services-auth:1.6.0")
implementation("androidx.security:security-crypto:1.1.0")
}
Initialize the SDK
- Kotlin
import com.loginid.core.models.LoginIDConfig
import com.loginid.mfa.LoginIDCheckoutMFA
class MyApplication : Application() {
val lid = LoginIDCheckoutMFA(<LOGINID_BASE_URL>)
override fun onCreate() {
super.onCreate()
// Additional setup...
}
}
Wallet Payment Page Overview
This overview outlines how the Wallet SDK determines the next user action once the checkout flow begins. Using an
identifier called trustId, the SDK selects the correct path and guides the user — either confirming the payment
with a passkey in a single step (serving as both sign-in and checkout approval), or presenting a dedicated sign-in step,
where the user can authenticate with a passkey or a fallback method such as bank login into their wallet.
The overview diagram is numbered to match key steps throughout the rest of this section. You can follow along each action (e.g., [2], [3], [4a], etc.) in the flowchart as you read through the related code and explanations that follow.
[1] Initiate the Flow With beginFlow — SDK Determines the Next Required Action
This is the starting point. The user arrives at the screen where the wallet payment process begins.
The code below demonstrates a basic wallet payment page. The Wallet SDK determines the next required action. Based on the response, you decide what to display to the user.
The SDK determines the next step by calling beginFlow with txPayload.
Call beginFlow as soon as your screen loads (e.g., in onCreate / onStart / a ViewModel init).
txPayload?txPayload is a string-based representation of the transaction being authorized. Some examples may include:
- A JSON string containing the transaction details
- An encoded or encrypted representation of those details
- A transaction identifier or reference of your choosing that your backend can resolve into the transaction details
The value is included within a challenge and signed by the user's passkey as proof that the user explicitly approved the transaction.
Depending on your implementation, txPayload can be generated on the wallet domain or passed in from the merchant
domain (e.g., via iframe postMessage or a redirect method).
merchantTrustId is explained in more detail here.
The result includes nextAction, which guides the UI toward either a passkey transaction or a fallback login.
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
import com.loginid.mfa.LoginIDCheckoutMFA
import com.loginid.mfa.models.BeginFlowOptions
import com.loginid.mfa.enums.ActionName
class WalletActivity : AppCompatActivity() {
private lateinit var lid: LoginIDCheckoutMFA
private var txPayload: String = "<TX_PAYLOAD>"
private var next: String? = null
private var error: String = ""
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lid = LoginIDCheckoutMFA(this, "<LOGINID_BASE_URL>")
lifecycleScope.launch {
checkFlowHandler()
}
}
private suspend fun checkFlowHandler() {
try {
// [1] Call beginFlow to start the wallet transaction flow
val result = lid.beginFlow(txPayload = txPayload)
// [1] The SDK decides what action to take next
next = if (result.nextAction == ActionName.PASSKEY_TX) {
"transaction"
} else {
"signin"
}
} catch (e: LoginIDError) {
error = e.message ?: "Failed to start wallet flow."
} catch (e: Exception) {
error = e.localizedMessage ?: "Failed to start wallet flow."
}
}
}
The SDK determines the appropriate action using trustId from the wallet.
The following sections provide example implementations for each possible next step and how they can integrate with your existing setup.
Checkout Flows
[2a] Confirming Transaction With Passkey
When an existing user has a registered passkey, beginFlow will return nextAction: "passkey:tx". In this step, the user
is not only signing in with their passkey, but also explicitly approving the payment for the selected transaction payload.
You can override the txPayload during confirmation by passing a new payload to performAction:
val result = lid.performAction(
action = ActionName.PASSKEY_TX,
activity = this@MainActivity,
options = PerformActionOptions(txPayload = "<TX_PAYLOAD>")
)
This lets you update transaction details right before user approval.
import com.loginid.mfa.LoginIDCheckoutMFA
import com.loginid.mfa.enums.ActionName
class TxAuthFragment : Fragment() {
private lateinit var lid: LoginIDCheckoutMFA
private var error: String = ""
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lid = LoginIDCheckoutMFA(this, "<LOGINID_BASE_URL>")
}
private fun onConfirmClick() {
viewLifecycleOwner.lifecycleScope.launch {
try {
// [2a] Confirm transaction using passkey
val result = lid.performAction(ActionName.PASSKEY_TX, this@MainActivity)
if (result.payloadSignature != null) {
// Finalize transaction by verifying payloadSignature and returning to merchant
finalizeTransaction(payloadSignature = result.payloadSignature!!)
// Navigate back to merchant here
} else {
error = "Transaction failed."
}
} catch (e: LoginIDError) {
error = e.message ?: ""
} catch (e: Exception) {
error = e.localizedMessage ?: "Passkey transaction failed."
}
}
}
}
[2b] Wallet Passkey Signin
If the result from beginFlow is anything other than nextAction: "PASSKEY_TX", we show a login view that lets the user sign in with either a passkey or a fallback method (in this case, a bank login).
This is a common first-time experience on a new device.
import com.loginid.mfa.LoginIDCheckoutMFA
import com.loginid.mfa.enums.ActionName
import com.loginid.mfa.models.PerformActionOptions
class WalletSignInFragment : Fragment() {
private lateinit var binding: ActivityMainBinding
private lateinit var lid: LoginIDCheckoutMFA
private var error: String = ""
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lid = LoginIDCheckoutMFA(this, "<LOGINID_BASE_URL>")
// Attempt autofill on appear
viewLifecycleOwner.lifecycleScope.launch { autofill() }
}
private suspend fun autofill() {
try {
// Passkey autofill authentication
val options = PerformActionOptions(
activity = this@MainActivity,
usernameAnchorView = binding.usernameInput
)
val result = lid.performAction(
action = ActionName.PASSKEY_AUTH,
options = options
)
if (result.accessToken != null) {
// [3a] Autofill successful, finalize payment
finalizeTransaction(accessToken = result.accessToken!!)
}
} catch (e: LoginIDError) {
error = e.message ?: ""
} catch (e: Exception) {
error = e.localizedMessage ?: ""
}
}
fun onPasskeyLoginClick() {
viewLifecycleOwner.lifecycleScope.launch {
try {
// Initiates passkey usernameless authentication.
val result = lid.performAction(ActionName.PASSKEY_AUTH, this@MainActivity)
if (result.accessToken != null) {
// [3a] Authentication successful, finalize payment
finalizeTransaction(accessToken = result.accessToken!!)
}
} catch (e: LoginIDError) {
error = e.message ?: ""
} catch (e: Exception) {
error = e.localizedMessage ?: ""
}
}
}
fun onBankLoginClick() {
// Redirect to external bank login or another identity method
// startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://bank.example.com/login?...")))
}
}
If the user signs in with a passkey successfully, they're signed into their wallet and can proceed to payment confirmation. Since a passkey was used, the next session will skip passkey login and go straight to transaction confirmation.
If the user signs in via a fallback login (like bank login), you can guide them to create a passkey for future frictionless access.
After passkey login, complete the flow by following the Finalizing Result to Merchant step.
[3b-1] Fallback Login or New User Onboarding After Bank Redirect
If the user doesn't have a passkey or is on a new device, the wallet may fall back to a traditional login method — such as bank login — as shown in [2b].
After completing the fallback login, the user is redirected back to the wallet app to continue the checkout flow. This section picks up from that point — when the user returns from an external identity provider (e.g., a bank login page).
To keep things simple, the following example just builds on the previous section of onBankLoginClick. In practice, you’d want to use a more secure and production-ready approach.
fun onBankLoginClick() {
val url = "https://bank.example.com/login?session_id=abc123&redirect_uri=mywallet://post-login"
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
startActivity(intent)
}
The remaining section covers what happens after a successful bank login — including how to resume the session.
Now that the user has returned to the wallet app, the next step is to verify their login, resume the session, and optionally offer to register a passkey.
But before a passkey can be created, you must perform an external authentication using a LoginID authorization token. This confirms the user's identity and resumes the checkout session.
After the user successfully authenticates with their bank, your backend should validate the login and request an authorization token from LoginID. This token is used to resume the wallet session and optionally prompt the user to create a passkey.
The flow typically looks like this:
- User is redirected to your bank's login page.
- After login, the user is redirected back to the wallet (e.g.,
/post-login) with a session identifier. - Your backend verifies the session and makes a POST request to
/fido2/v2/mgmt/grant/external-authto get an authorization token from LoginID. This is where your API key listed on the prerequisites will be needed. See example implementation in code. - The frontend passes this token to the Wallet SDK to complete the authentication process via
performAction("external", { payload: token }).
This allows the session to resume and enables passkey registration for future passwordless access.
Once the authorization token has been obtained from your backend, pass it to the Wallet SDK to complete the external authentication process:
import com.loginid.mfa.LoginIDCheckoutMFA
import com.loginid.mfa.enums.ActionName
import com.loginid.mfa.models.PerformActionOptions
class PostBankLoginFragment : Fragment() {
private lateinit var lid: LoginIDCheckoutMFA
private var error: String = ""
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lid = LoginIDCheckoutMFA(this, "<LOGINID_BASE_URL>")
viewLifecycleOwner.lifecycleScope.launch { checkFlow() }
}
private suspend fun checkFlow() {
try {
// Example only — your app should call your backend after a successful bank login
// Backend validates the login and returns a LoginID authorization token.
val walletResult = continueSessionFromBankRedirect()
val authorizationToken = walletResult.authorizationToken
val callbackUrl = walletResult.callbackUrl
// [3b-1] User signs in via fallback method
val options = PerformActionOptions(payload = authorizationToken)
val result = lid.performAction(
action = ActionName.EXTERNAL,
options = options
)
if (result.nextAction == ActionName.PASSKEY_REG) {
// Navigate to "Add Passkey" screen
} else {
// Continue without adding a passkey
}
} catch (e: LoginIDError) {
error = e.message ?: ""
} catch (e: Exception) {
error = e.localizedMessage ?: ""
}
}
}
Once authenticated, this example navigates the user to the PasskeyCreate Fragment. Your implementation may differ depending on how you handle routing and view structure.
[3b-2] Optionally Create a Passkey
You can pass in a display name during passkey creation.
The display name is a user-friendly label that helps users recognize the correct passkey from a list, like a familiar nickname.
val result = lid.performAction(
action = ActionName.PASSKEY_REG,
activity = this@MainActivity,
options = PerformActionOptions(displayName = "Wallet User")
)
This is a follow-up from the previous section. We navigate to the PasskeyCreate Fragment after verifying bank login and finishing external authentication.
import com.loginid.mfa.LoginIDCheckoutMFA
import com.loginid.mfa.enums.ActionName
class PasskeyCreateFragment : Fragment() {
private lateinit var lid: LoginIDCheckoutMFA
private var error: String = ""
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lid = LoginIDCheckoutMFA(this, "<LOGINID_BASE_URL>")
}
private fun onCreatePasskeyClick() {
viewLifecycleOwner.lifecycleScope.launch {
try {
// [3b-2] Create a new passkey after bank login
val result = lid.performAction(
action = ActionName.PASSKEY_REG,
activity = this@MainActivity
)
if (result.accessToken != null) {
// [3b-3] Continue to finalize payment
finalizeTransaction(accessToken = result.accessToken!!)
}
} catch (e: LoginIDError) {
error = e.message ?: ""
} catch (e: Exception) {
error = e.localizedMessage ?: "Passkey registration failed."
}
}
}
private fun onSkipClick() {
// User chose not to add a passkey
// Navigate back or continue the flow as desired
}
}
After the passkey is optionally created, complete the flow by following the Finalizing Result to Merchant step. On future checkouts, LoginID will allow the flow to go directly to transaction confirmation.
Finalize and Return Result to Merchant
Once the transaction has been confirmed (via passkey or fallback flow), finalize the payment on your wallet backend and return the result to the merchant application.
Here’s an example of how you might handle this:
// Example: Finalizing transaction result and returning to merchant
suspend fun finalizeTransaction(
payloadSignature: String? = null,
accessToken: String? = null
) {
// Confirm the payment on your wallet backend (stubbed)
val result = walletBackend.confirmPayment(
payloadSignature = payloadSignature,
accessToken = accessToken
)
// Redirect back to merchant with encoded result
val uri = Uri.parse("${result.callback}?token=${result.token}&passkey=${result.passkey}")
withContext(Dispatchers.Main) {
startActivity(Intent(Intent.ACTION_VIEW, uri))
}
}
Conclusion
The Wallet SDK for Android streamlines checkout by combining authentication and payment approval into a single flow. With beginFlow, the SDK determines the right next action — either confirming the transaction with a passkey or guiding the user through fallback login.
Passkey-based transaction confirmation serves as both sign-in and checkout approval in one step, delivering a fast and secure experience for returning users. Fallback methods and passkey creation ensure new or recovering users can still complete their transactions.
This provides a smooth, secure checkout that reduces friction while strengthening trust.
For additional support or clarification, you can reach us anytime at support@loginid.io.
Having issues? Visit our troubleshooting section for common questions and solutions.