Skip to main content

Android SDK - Accept Payments

Learn how to accept payments in your Android application using the Omise Android SDK. This guide covers creating charges, handling 3D Secure authentication, and implementing various payment flows with Kotlin and Java.

Overviewโ€‹

The Omise Android SDK provides native Android interfaces for accepting payments in mobile applications. The SDK handles tokenization, 3D Secure flows, and provides customizable payment UI components.

Key Featuresโ€‹

  • Native Android Implementation - Built with Kotlin for modern Android development
  • Secure Tokenization - Client-side card tokenization
  • 3D Secure 2.0 Support - Seamless authentication with Chrome Custom Tabs
  • Google Pay Integration - Built-in Google Pay support
  • Material Design UI - Modern, customizable payment components
  • Jetpack Compose Support - Compose-ready components

Prerequisitesโ€‹

Before implementing payment acceptance:

  1. Complete SDK installation (see Developer Tools > Android SDK)
  2. Configure your public key
  3. Set up backend for charge creation
  4. Test with development credentials

Payment Flow Overviewโ€‹

The typical Android payment flow:

User enters card details
โ†“
SDK tokenizes card (client-side)
โ†“
Token sent to your backend
โ†“
Backend creates charge with token
โ†“
3D Secure authentication (if required)
โ†“
Payment completed

Creating a Basic Chargeโ€‹

Step 1: Collect and Tokenize Cardโ€‹

import co.omise.android.OmiseClient
import co.omise.android.models.Token
import co.omise.android.models.CardParam

class PaymentActivity : AppCompatActivity() {

private lateinit var omiseClient: OmiseClient

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_payment)

omiseClient = OmiseClient("pkey_test_123")
}

fun processPayment(amount: Long, currency: String) {
val cardParam = CardParam(
name = "John Appleseed",
number = "4242424242424242",
expirationMonth = 12,
expirationYear = 2025,
securityCode = "123"
)

showLoading()

omiseClient.send(cardParam, object : RequestListener<Token> {
override fun onRequestSucceed(model: Token) {
hideLoading()
createCharge(model.id, amount, currency)
}

override fun onRequestFailed(throwable: Throwable) {
hideLoading()
handleError(throwable)
}
})
}

fun createCharge(tokenId: String, amount: Long, currency: String) {
val request = ChargeRequest(
token = tokenId,
amount = amount,
currency = currency,
returnUri = "myapp://payment/complete"
)

ApiClient.createCharge(request) { result ->
when (result) {
is Result.Success -> handleChargeResponse(result.data)
is Result.Error -> handleError(result.exception)
}
}
}
}

Step 2: Using Pre-built Payment Formโ€‹

import co.omise.android.ui.CreditCardActivity
import co.omise.android.models.Token

class CheckoutActivity : AppCompatActivity() {

companion object {
private const val REQUEST_CC = 100
}

fun showPaymentForm() {
val intent = Intent(this, CreditCardActivity::class.java).apply {
putExtra(CreditCardActivity.EXTRA_PKEY, "pkey_test_123")
putExtra(CreditCardActivity.EXTRA_AMOUNT, 100000L)
putExtra(CreditCardActivity.EXTRA_CURRENCY, "THB")
}

startActivityForResult(intent, REQUEST_CC)
}

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)

if (requestCode == REQUEST_CC && resultCode == RESULT_OK) {
val token = data?.getParcelableExtra<Token>(CreditCardActivity.EXTRA_TOKEN)
token?.let { processToken(it) }
}
}

fun processToken(token: Token) {
// Send to backend to create charge
val chargeData = mapOf(
"source" to token.id,
"amount" to 100000,
"currency" to "THB",
"return_uri" to "myapp://payment/complete"
)

ApiClient.post("/charges", chargeData) { result ->
handleChargeResult(result)
}
}
}

Java Implementationโ€‹

import co.omise.android.OmiseClient;
import co.omise.android.models.Token;
import co.omise.android.models.CardParam;

public class PaymentActivity extends AppCompatActivity {

private OmiseClient omiseClient;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_payment);

omiseClient = new OmiseClient("pkey_test_123");
}

public void processPayment(long amount, String currency) {
CardParam cardParam = new CardParam(
"John Appleseed",
"4242424242424242",
12,
2025,
"123"
);

showLoading();

omiseClient.send(cardParam, new RequestListener<Token>() {
@Override
public void onRequestSucceed(Token token) {
hideLoading();
createCharge(token.getId(), amount, currency);
}

@Override
public void onRequestFailed(Throwable throwable) {
hideLoading();
handleError(throwable);
}
});
}
}

Handling 3D Secure Authenticationโ€‹

Implementing 3D Secure Flowโ€‹

import android.net.Uri
import androidx.browser.customtabs.CustomTabsIntent

class PaymentProcessor : AppCompatActivity() {

companion object {
private const val REQUEST_3DS = 200
}

fun handleChargeResponse(charge: Charge) {
when (charge.status) {
"successful" -> {
showSuccess()
}
"pending" -> {
charge.authorizeUri?.let { uri ->
launch3DSecure(uri)
}
}
"failed" -> {
showError("Payment failed")
}
}
}

fun launch3DSecure(authorizeUri: String) {
val uri = Uri.parse(authorizeUri)

val customTabsIntent = CustomTabsIntent.Builder()
.setShowTitle(true)
.setToolbarColor(ContextCompat.getColor(this, R.color.colorPrimary))
.build()

customTabsIntent.launchUrl(this, uri)
}

override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)

intent?.data?.let { uri ->
if (uri.scheme == "myapp" && uri.host == "payment") {
// User returned from 3D Secure
verifyPaymentStatus()
}
}
}

fun verifyPaymentStatus() {
showLoading("Verifying payment...")

ApiClient.get("/charges/verify") { result ->
hideLoading()

when (result) {
is Result.Success -> {
if (result.data.status == "successful") {
showSuccess()
} else {
showError("Payment verification failed")
}
}
is Result.Error -> handleError(result.exception)
}
}
}
}
<activity
android:name=".PaymentProcessor"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="myapp"
android:host="payment" />
</intent-filter>
</activity>

3D Secure with WebView (Alternative)โ€‹

import android.webkit.WebView
import android.webkit.WebViewClient

class Secure3DActivity : AppCompatActivity() {

private lateinit var webView: WebView

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_3ds)

val authorizeUri = intent.getStringExtra(EXTRA_AUTHORIZE_URI) ?: return

webView = findViewById(R.id.webview)
setup3DSecureWebView(authorizeUri)
}

fun setup3DSecureWebView(authorizeUri: String) {
webView.apply {
settings.javaScriptEnabled = true
webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(
view: WebView?,
url: String?
): Boolean {
if (url?.startsWith("myapp://") == true) {
handle3DSecureReturn(url)
return true
}
return false
}
}
loadUrl(authorizeUri)
}
}

fun handle3DSecureReturn(url: String) {
// Payment completed, verify status
val intent = Intent().apply {
putExtra(EXTRA_RESULT, "completed")
}
setResult(RESULT_OK, intent)
finish()
}
}

Saved Card Paymentsโ€‹

Displaying and Charging Saved Cardsโ€‹

class SavedCardPayment : AppCompatActivity() {

private lateinit var cardsAdapter: CardListAdapter

fun loadSavedCards(customerId: String) {
showLoading()

ApiClient.get("/customers/$customerId/cards") { result ->
hideLoading()

when (result) {
is Result.Success -> {
displayCards(result.data)
}
is Result.Error -> handleError(result.exception)
}
}
}

fun displayCards(cards: List<Card>) {
cardsAdapter = CardListAdapter(cards) { selectedCard ->
chargeSelectedCard(selectedCard)
}

recyclerView.adapter = cardsAdapter
}

fun chargeSelectedCard(card: Card) {
val chargeRequest = mapOf(
"customer" to card.customerId,
"card" to card.id,
"amount" to 100000L,
"currency" to "THB",
"return_uri" to "myapp://payment/complete"
)

showLoading("Processing payment...")

ApiClient.post("/charges", chargeRequest) { result ->
hideLoading()

when (result) {
is Result.Success -> handleChargeResponse(result.data)
is Result.Error -> handleError(result.exception)
}
}
}
}

Card List Adapterโ€‹

class CardListAdapter(
private val cards: List<Card>,
private val onCardSelected: (Card) -> Unit
) : RecyclerView.Adapter<CardListAdapter.CardViewHolder>() {

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CardViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_card, parent, false)
return CardViewHolder(view)
}

override fun onBindViewHolder(holder: CardViewHolder, position: Int) {
holder.bind(cards[position])
}

override fun getItemCount() = cards.size

inner class CardViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val brandIcon: ImageView = itemView.findViewById(R.id.brandIcon)
private val cardNumber: TextView = itemView.findViewById(R.id.cardNumber)
private val expiryDate: TextView = itemView.findViewById(R.id.expiryDate)

fun bind(card: Card) {
brandIcon.setImageResource(getBrandIcon(card.brand))
cardNumber.text = "โ€ขโ€ขโ€ขโ€ข ${card.lastDigits}"
expiryDate.text = "${card.expirationMonth}/${card.expirationYear}"

itemView.setOnClickListener {
onCardSelected(card)
}
}

private fun getBrandIcon(brand: String): Int {
return when (brand.lowercase()) {
"visa" -> R.drawable.ic_visa
"mastercard" -> R.drawable.ic_mastercard
"amex" -> R.drawable.ic_amex
else -> R.drawable.ic_card_default
}
}
}
}

Saving a New Cardโ€‹

fun saveNewCard(customerId: String) {
val cardParam = CardParam(
name = binding.cardholderName.text.toString(),
number = binding.cardNumber.text.toString(),
expirationMonth = getExpiryMonth(),
expirationYear = getExpiryYear(),
securityCode = binding.cvv.text.toString()
)

showLoading("Saving card...")

omiseClient.send(cardParam, object : RequestListener<Token> {
override fun onRequestSucceed(token: Token) {
attachCardToCustomer(customerId, token.id)
}

override fun onRequestFailed(throwable: Throwable) {
hideLoading()
handleError(throwable)
}
})
}

fun attachCardToCustomer(customerId: String, tokenId: String) {
val params = mapOf("card" to tokenId)

ApiClient.post("/customers/$customerId/cards", params) { result ->
hideLoading()

when (result) {
is Result.Success -> {
showSuccess("Card saved successfully")
finish()
}
is Result.Error -> handleError(result.exception)
}
}
}

Custom Payment Formโ€‹

Building Custom UI with ViewBindingโ€‹

class CustomPaymentForm : AppCompatActivity() {

private lateinit var binding: ActivityCustomPaymentBinding
private lateinit var omiseClient: OmiseClient

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityCustomPaymentBinding.inflate(layoutInflater)
setContentView(binding.root)

omiseClient = OmiseClient("pkey_test_123")
setupViews()
}

fun setupViews() {
// Card number formatting
binding.cardNumber.addTextChangedListener(CardNumberTextWatcher())

// Expiry formatting
binding.expiry.addTextChangedListener(ExpiryTextWatcher())

// CVV validation
binding.cvv.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) {
validateCvv(s.toString())
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
})

binding.payButton.setOnClickListener {
if (validateForm()) {
createToken()
}
}
}

fun validateForm(): Boolean {
val cardNumber = binding.cardNumber.text.toString().replace(" ", "")
val cvv = binding.cvv.text.toString()
val name = binding.cardholderName.text.toString()

if (!CardValidator.isValidCardNumber(cardNumber)) {
binding.cardNumber.error = "Invalid card number"
return false
}

if (!CardValidator.isValidCvv(cvv)) {
binding.cvv.error = "Invalid CVV"
return false
}

if (name.isBlank()) {
binding.cardholderName.error = "Cardholder name required"
return false
}

return true
}

fun createToken() {
val (month, year) = parseExpiry(binding.expiry.text.toString())

val cardParam = CardParam(
name = binding.cardholderName.text.toString(),
number = binding.cardNumber.text.toString().replace(" ", ""),
expirationMonth = month,
expirationYear = year,
securityCode = binding.cvv.text.toString()
)

binding.payButton.isEnabled = false
binding.progressBar.visibility = View.VISIBLE

omiseClient.send(cardParam, object : RequestListener<Token> {
override fun onRequestSucceed(token: Token) {
binding.progressBar.visibility = View.GONE
processPayment(token.id)
}

override fun onRequestFailed(throwable: Throwable) {
binding.progressBar.visibility = View.GONE
binding.payButton.isEnabled = true
handleError(throwable)
}
})
}

fun parseExpiry(text: String): Pair<Int, Int> {
val parts = text.split("/")
val month = parts[0].trim().toIntOrNull() ?: 1
val year = parts[1].trim().toIntOrNull() ?: 2025
return Pair(month, year)
}
}

Card Number TextWatcherโ€‹

class CardNumberTextWatcher : TextWatcher {

private var isFormatting = false

override fun afterTextChanged(s: Editable?) {
if (isFormatting) return

isFormatting = true

val digitsOnly = s.toString().replace(" ", "")
val formatted = formatCardNumber(digitsOnly)

s?.replace(0, s.length, formatted)

isFormatting = false
}

override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}

private fun formatCardNumber(number: String): String {
val builder = StringBuilder()
for (i in number.indices) {
if (i > 0 && i % 4 == 0) {
builder.append(" ")
}
builder.append(number[i])
}
return builder.toString()
}
}

Jetpack Compose Implementationโ€‹

@Composable
fun PaymentForm(
onPaymentComplete: (String) -> Unit,
modifier: Modifier = Modifier
) {
var cardNumber by remember { mutableStateOf("") }
var cardholderName by remember { mutableStateOf("") }
var expiry by remember { mutableStateOf("") }
var cvv by remember { mutableStateOf("") }
var isProcessing by remember { mutableStateOf(false) }

Column(
modifier = modifier
.fillMaxWidth()
.padding(16.dp)
) {
OutlinedTextField(
value = cardNumber,
onValueChange = { cardNumber = formatCardNumber(it) },
label = { Text("Card Number") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.fillMaxWidth()
)

Spacer(modifier = Modifier.height(16.dp))

OutlinedTextField(
value = cardholderName,
onValueChange = { cardholderName = it },
label = { Text("Cardholder Name") },
modifier = Modifier.fillMaxWidth()
)

Spacer(modifier = Modifier.height(16.dp))

Row(modifier = Modifier.fillMaxWidth()) {
OutlinedTextField(
value = expiry,
onValueChange = { expiry = formatExpiry(it) },
label = { Text("MM/YY") },
modifier = Modifier.weight(1f)
)

Spacer(modifier = Modifier.width(16.dp))

OutlinedTextField(
value = cvv,
onValueChange = { if (it.length <= 4) cvv = it },
label = { Text("CVV") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.weight(1f)
)
}

Spacer(modifier = Modifier.height(24.dp))

Button(
onClick = {
isProcessing = true
processPayment(
cardNumber, cardholderName, expiry, cvv
) { tokenId ->
isProcessing = false
onPaymentComplete(tokenId)
}
},
enabled = !isProcessing,
modifier = Modifier.fillMaxWidth()
) {
if (isProcessing) {
CircularProgressIndicator(
modifier = Modifier.size(20.dp),
color = Color.White
)
} else {
Text("Pay Now")
}
}
}
}

Google Pay Integrationโ€‹

Implementing Google Payโ€‹

import com.google.android.gms.wallet.*

class GooglePayProcessor : AppCompatActivity() {

private lateinit var paymentsClient: PaymentsClient

companion object {
private const val LOAD_PAYMENT_DATA_REQUEST_CODE = 300
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

paymentsClient = PaymentsUtil.createPaymentsClient(this)
checkGooglePayAvailability()
}

fun checkGooglePayAvailability() {
val request = IsReadyToPayRequest.fromJson(
PaymentsUtil.getIsReadyToPayRequest().toString()
)

paymentsClient.isReadyToPay(request).addOnCompleteListener { task ->
if (task.isSuccessful) {
showGooglePayButton()
}
}
}

fun requestPayment(amount: String, currency: String) {
val paymentDataRequest = PaymentDataRequest.fromJson(
PaymentsUtil.getPaymentDataRequest(amount, currency).toString()
)

AutoResolveHelper.resolveTask(
paymentsClient.loadPaymentData(paymentDataRequest),
this,
LOAD_PAYMENT_DATA_REQUEST_CODE
)
}

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)

when (requestCode) {
LOAD_PAYMENT_DATA_REQUEST_CODE -> {
when (resultCode) {
RESULT_OK -> {
data?.let { intent ->
PaymentData.getFromIntent(intent)?.let { paymentData ->
handleGooglePaySuccess(paymentData)
}
}
}
RESULT_CANCELED -> {
showError("Payment cancelled")
}
AutoResolveHelper.RESULT_ERROR -> {
AutoResolveHelper.getStatusFromIntent(data)?.let {
handleError(it.statusMessage ?: "Unknown error")
}
}
}
}
}
}

fun handleGooglePaySuccess(paymentData: PaymentData) {
val paymentInfo = paymentData.toJson()

// Send to backend to create Omise source and charge
val params = mapOf(
"type" to "googlepay",
"payment_data" to paymentInfo,
"amount" to 100000L,
"currency" to "THB"
)

ApiClient.post("/charges/googlepay", params) { result ->
when (result) {
is Result.Success -> showSuccess()
is Result.Error -> handleError(result.exception)
}
}
}
}

Google Pay Utility Classโ€‹

object PaymentsUtil {

private const val GATEWAY_TOKENIZATION_NAME = "omise"

fun getIsReadyToPayRequest(): JSONObject {
return JSONObject().apply {
put("apiVersion", 2)
put("apiVersionMinor", 0)
put("allowedPaymentMethods", JSONArray().put(getBaseCardPaymentMethod()))
}
}

fun getPaymentDataRequest(price: String, currency: String): JSONObject {
return JSONObject().apply {
put("apiVersion", 2)
put("apiVersionMinor", 0)
put("allowedPaymentMethods", JSONArray().put(getCardPaymentMethod()))
put("transactionInfo", getTransactionInfo(price, currency))
put("merchantInfo", getMerchantInfo())
}
}

private fun getBaseCardPaymentMethod(): JSONObject {
return JSONObject().apply {
put("type", "CARD")
put("parameters", JSONObject().apply {
put("allowedAuthMethods", JSONArray().put("PAN_ONLY").put("CRYPTOGRAM_3DS"))
put("allowedCardNetworks", JSONArray()
.put("AMEX")
.put("DISCOVER")
.put("MASTERCARD")
.put("VISA"))
})
}
}

private fun getCardPaymentMethod(): JSONObject {
return getBaseCardPaymentMethod().apply {
put("tokenizationSpecification", JSONObject().apply {
put("type", "PAYMENT_GATEWAY")
put("parameters", JSONObject().apply {
put("gateway", GATEWAY_TOKENIZATION_NAME)
put("gatewayMerchantId", "your_merchant_id")
})
})
}
}

private fun getTransactionInfo(price: String, currency: String): JSONObject {
return JSONObject().apply {
put("totalPrice", price)
put("totalPriceStatus", "FINAL")
put("currencyCode", currency)
}
}

private fun getMerchantInfo(): JSONObject {
return JSONObject().apply {
put("merchantName", "Your App Name")
}
}

fun createPaymentsClient(activity: Activity): PaymentsClient {
val walletOptions = Wallet.WalletOptions.Builder()
.setEnvironment(WalletConstants.ENVIRONMENT_TEST) // Use PRODUCTION for live
.build()

return Wallet.getPaymentsClient(activity, walletOptions)
}
}

Error Handlingโ€‹

Comprehensive Error Handlingโ€‹

sealed class PaymentError : Exception() {
data class TokenizationError(override val message: String) : PaymentError()
data class ChargeError(override val message: String) : PaymentError()
object AuthenticationFailed : PaymentError()
data class NetworkError(override val cause: Throwable?) : PaymentError()
object InvalidCard : PaymentError()
object InsufficientFunds : PaymentError()
object CardDeclined : PaymentError()
}

class PaymentErrorHandler(private val context: Context) {

fun handle(error: Throwable) {
val message = when (error) {
is PaymentError.TokenizationError ->
"Failed to process card: ${error.message}"
is PaymentError.ChargeError ->
error.message
is PaymentError.AuthenticationFailed ->
"Payment authentication failed"
is PaymentError.NetworkError ->
"Network error. Please check your connection."
is PaymentError.InvalidCard ->
"Invalid card information"
is PaymentError.InsufficientFunds ->
"Insufficient funds on card"
is PaymentError.CardDeclined ->
"Card declined by bank"
else ->
"An error occurred: ${error.message}"
}

showError(message)
logError(error)
}

fun showError(message: String) {
MaterialAlertDialogBuilder(context)
.setTitle("Payment Error")
.setMessage(message)
.setPositiveButton("OK", null)
.show()
}

fun logError(error: Throwable) {
// Log to your analytics service
FirebaseCrashlytics.getInstance().recordException(error)
}
}

Common Use Casesโ€‹

Subscription Paymentโ€‹

class SubscriptionPayment(private val context: Context) {

fun processSubscription(
plan: String,
customerId: String,
amount: Long
) {
val params = mapOf(
"customer" to customerId,
"amount" to amount,
"currency" to "THB",
"description" to "Monthly $plan subscription",
"metadata" to mapOf(
"plan" to plan,
"type" to "subscription"
)
)

ApiClient.post("/charges", params) { result ->
when (result) {
is Result.Success -> {
if (result.data.status == "successful") {
createRecurringSchedule(customerId, plan, amount)
}
}
is Result.Error -> handleError(result.exception)
}
}
}

fun createRecurringSchedule(
customerId: String,
plan: String,
amount: Long
) {
val params = mapOf(
"customer" to customerId,
"amount" to amount,
"period" to "month",
"description" to "$plan subscription"
)

ApiClient.post("/schedules", params) { result ->
handleScheduleCreation(result)
}
}
}

Split Paymentโ€‹

fun processSplitPayment(
totalAmount: Long,
splits: List<PaymentSplit>
) {
val charges = splits.map { split ->
createCharge(
amount = split.amount,
recipient = split.recipientId,
description = split.description
)
}

// Process all charges
Observable.fromIterable(charges)
.flatMap { charge -> processChargeAsync(charge) }
.toList()
.subscribe(
{ results -> handleSplitPaymentComplete(results) },
{ error -> handleError(error) }
)
}

Best Practicesโ€‹

Securityโ€‹

  1. Never Store Sensitive Data

    // โœ… Good - Use tokens
    val token = createToken(cardData)
    sendToBackend(token.id)

    // โŒ Bad - Don't store card data
    sharedPrefs.edit().putString("card_number", cardNumber).apply()
  2. Validate Input

    fun validateCard(number: String, cvv: String): Boolean {
    if (!CardValidator.isValidCardNumber(number)) {
    return false
    }

    if (!CardValidator.isValidCvv(cvv)) {
    return false
    }

    return true
    }
  3. Use ProGuard/R8

    # In proguard-rules.pro
    -keep class co.omise.android.models.** { *; }
    -keep class co.omise.android.** { *; }

Performanceโ€‹

  1. Use Coroutines

    suspend fun createPayment(amount: Long) = withContext(Dispatchers.IO) {
    val token = omiseClient.sendSuspend(cardParam)
    val charge = apiClient.createCharge(token.id, amount)
    charge
    }
  2. Cache Payment Methods

    private val cardCache = mutableMapOf<String, List<Card>>()

    fun loadCards(customerId: String, forceRefresh: Boolean = false) {
    if (!forceRefresh && cardCache.containsKey(customerId)) {
    displayCards(cardCache[customerId]!!)
    return
    }

    fetchCardsFromServer(customerId)
    }

Troubleshootingโ€‹

Common Issuesโ€‹

Token Creation Fails

// Check public key
val client = OmiseClient("pkey_test_123")

// Validate card details
Log.d("Payment", "Card valid: ${CardValidator.isValidCardNumber(number)}")
Log.d("Payment", "CVV valid: ${CardValidator.isValidCvv(cvv)}")

3D Secure Not Working

// Verify deep link in manifest
// Check return URI matches
// Test with 3DS test card: 4000000000003063

if (charge.authorizeUri != null) {
Log.d("Payment", "Auth URI: ${charge.authorizeUri}")
launch3DSecure(charge.authorizeUri!!)
}

Google Pay Not Available

paymentsClient.isReadyToPay(request).addOnCompleteListener { task ->
if (task.isSuccessful) {
Log.d("GooglePay", "Available")
} else {
Log.e("GooglePay", "Not available: ${task.exception}")
}
}

FAQโ€‹

General Questionsโ€‹

Q: Do I need PCI compliance for Android apps?

A: No. The Omise Android SDK tokenizes card data client-side, significantly reducing PCI compliance requirements. However, follow security best practices.

Q: Can I accept payments offline?

A: Partial support. You can collect card information offline and tokenize later, but charge creation requires internet connectivity.

Q: What Android versions are supported?

A: The Omise Android SDK supports Android 5.0 (API level 21) and above. For best results, target Android 8.0 (API 26) or higher.

Q: How do I test payments?

A: Use test card numbers (4242 4242 4242 4242) with any future expiry and any 3-digit CVV. Use your test public key (pkey_test_xxx).

Q: Can I customize the payment UI?

A: Yes. Use the pre-built components with customization or build your own UI using the SDK for tokenization only.

Q: Are tokens reusable?

A: No. Tokens expire after being used to create a charge or attached to a customer.

Implementation Questionsโ€‹

Q: Should I use Kotlin or Java?

A: We recommend Kotlin for better null safety and modern Android development, but the SDK fully supports Java.

Q: Can I use Jetpack Compose?

A: Yes. The SDK is compatible with Compose. Use interop or build custom Compose components around the SDK.

Q: How do I handle 3D Secure timeouts?

A: Implement timeout detection in your deep link handler and provide retry options to users.

Q: Can I test 3D Secure in sandbox?

A: Yes. Use test card 4000000000003063 to trigger 3D Secure authentication in test mode.

Next Stepsโ€‹