An Android + Jetpack Compose sample that demonstrates how to authenticate with Crossmint, manage wallets, and submit EVM transactions using the Crossmint Kotlin SDK.
- Android Studio Hedgehog (or newer) with the Android SDK and an emulator or device running API 24+.
- JDK 11 (bundled with current Android Studio versions).
- A Crossmint developer account (see Get your Crossmint API key).
- (Optional) An email inbox you can use for one-time-passcode (OTP) authentication during sign-in.
- Sign in to the Crossmint staging console and create an account if you do not already have one.
- If prompted, create a project for your app.
- Open the project overview and create (or copy) the client API key — this quickstart runs entirely on-device, so it expects the client key that the Crossmint SDK uses for authenticated calls. When creating the key, pick
App type → Mobileand register your Android package name (for this sample:com.crossmint.kotlin.quickstart) so the credential only works for your app. - When you're ready for production, replicate the project in the production console to obtain live client credentials.
Paste that key into local.properties as described below.
- Copy the template:
cp local.properties.example local.properties. - Update the paths and credentials inside
local.properties:sdk.dir=/absolute/path/to/Android/sdk CROSSMINT_API_KEY=your_api_key
- Never commit
local.properties— it contains machine-specific secrets and is already gitignored.
The build reads CROSSMINT_API_KEY from BuildConfig, which the Compose UI injects into the Crossmint SDK during startup.
- Android Studio: Open the project folder, let Gradle sync, then click Run and choose an emulator or USB device.
- Command line: From the repo root, install a debug build on a connected device/emulator:
If you only need to build, use
./gradlew installDebug
./gradlew assembleDebug.
On first launch you will be prompted for an email. Crossmint sends an OTP to that inbox, which the app uses to complete authentication before loading wallets.
CrossmintSDKProvider
.Builder(BuildConfig.CROSSMINT_API_KEY)
.developmentMode()
.onTEERequired { onOTPSubmit, onDismiss ->
OTPDialog(onOTPSubmit = onOTPSubmit, onDismiss = onDismiss)
}
.build { QuickstartContent() }Location: app/src/main/java/com/crossmint/kotlin/quickstart/QuickstartApp.kt
BuildConfig.CROSSMINT_API_KEYresolves fromlocal.properties..developmentMode()points the SDK at Crossmint’s sandbox environment.onTEERequiredis optional; include it only when your signer type requires OTP-based signing (email or phone) so the SDK can surface the verification dialog.
All wallet functionality lives in WalletViewModel (app/src/main/java/com/crossmint/kotlin/quickstart/wallet/WalletViewModel.kt). The snippets below highlight the most important calls.
viewModelScope.launch {
when (val result = crossmintWallets.getWallet(chain)) {
is Result.Success -> {
_uiState.value = _uiState.value.copy(
wallet = result.value,
isLoading = false,
errorMessage = null,
isEmpty = false,
)
}
is Result.Failure -> {
val isEmpty = result.error is WalletError.WalletNotFound
_uiState.value = _uiState.value.copy(
wallet = null,
isLoading = false,
errorMessage = if (!isEmpty) result.error.message else null,
isEmpty = isEmpty,
)
}
}
}chainis an instance ofChain/EVMChain(the sample usesEVMChain.BaseSepolia).Result.Failureincludes typedWalletErrorvalues (e.g.,WalletNotFound).
viewModelScope.launch {
when (val result = crossmintWallets.createWallet(chain, signer)) {
is Result.Success -> _uiState.value = _uiState.value.copy(wallet = result.value)
is Result.Failure -> _uiState.value = _uiState.value.copy(errorMessage = result.error.message)
}
}signeris aSignerDatadescribing how the user will sign transactions (the sample uses a placeholder phone signer).- On success, subsequent calls can reuse the returned
Wallet.
viewModelScope.launch {
val wallet = _uiState.value.wallet ?: return@launch
when (val result = wallet.send(recipient, tokenLocator, amount)) {
is Result.Success -> {
_uiState.value = _uiState.value.copy(transaction = result.value)
fetchTransaction(wallet, result.value.id)
}
is Result.Failure -> _uiState.value = _uiState.value.copy(transactionError = result.error)
}
}recipientis the destination address.tokenLocatoridentifies the asset (e.g.,eth:base-sepolia).amountis aDoublerepresenting the quantity to send.- On success the sample immediately fetches full transaction details.
private suspend fun fetchTransaction(wallet: Wallet, transactionId: String) {
when (val result = wallet.getTransaction(transactionId)) {
is Result.Success -> _uiState.value = _uiState.value.copy(transaction = result.value)
is Result.Failure -> _uiState.value = _uiState.value.copy(transactionError = result.error)
}
}- Use the
Transaction.idreturned bywallet.send. - Errors propagate as typed
TransactionErrorvalues.
viewModelScope.launch {
val wallet = _uiState.value.wallet ?: return@launch
when (val result = wallet.approve(transactionId)) {
is Result.Success -> _uiState.value = _uiState.value.copy(transaction = result.value)
is Result.Failure -> _uiState.value = _uiState.value.copy(errorMessage = "Signing failed: ${result.error.message}")
}
}- Only call
approveafterfetchTransactionsucceeds so the transaction is fully loaded. - On success, the transaction status in
_uiStatereflects the signed state.
- OTP emails not arriving: Verify the email is registered with Crossmint’s sandbox and check spam folders.
WalletNotFounderrors: Create a wallet first (createWallet) or switch to a chain where the user already has one.- Gradle sync issues: Make sure the Android SDK path in
local.propertiesmatches your local installation and that you are using JDK 11.
Happy hacking with Crossmint on Android!