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:
- Complete SDK installation (see Developer Tools > Android SDK)
- Configure your public key
- Set up backend for charge creation
- 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)
}
}
}
}
Configure Deep Link in AndroidManifest.xmlโ
<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โ
-
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() -
Validate Input
fun validateCard(number: String, cvv: String): Boolean {
if (!CardValidator.isValidCardNumber(number)) {
return false
}
if (!CardValidator.isValidCvv(cvv)) {
return false
}
return true
} -
Use ProGuard/R8
# In proguard-rules.pro
-keep class co.omise.android.models.** { *; }
-keep class co.omise.android.** { *; }
Performanceโ
-
Use Coroutines
suspend fun createPayment(amount: Long) = withContext(Dispatchers.IO) {
val token = omiseClient.sendSuspend(cardParam)
val charge = apiClient.createCharge(token.id, amount)
charge
} -
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.
Related Resourcesโ
- Developer Tools - Android SDK Setup
- Mobile Payments Overview
- Charges API Reference
- Tokens API Reference
- 3D Secure Guide
- Google Pay Integration
- Error Codes Reference
- Testing Guide