Android SDK - ยอมรับการชำระเงิน
เรียนรู้วิธีการยอมรับการชำระเงินในแอปพลิเคชัน Android ของคุณโดยใช้ Omise Android SDK คู่มือนี้ครอบคลุมการสร้างการเรียกเก็บเงิน การจัดการการตรวจสอบสิทธิ์ 3D Secure และการนำไปใช้ในการไหลการชำระเงินต่างๆ พร้อม Kotlin และ Java
ภาพรวม
Omise Android SDK มีอินเตอร์เฟซ Android แบบเนทีฟสำหรับยอมรับการชำระเงินในแอปพลิเคชันมือถือ SDK นี้จัดการ tokenization, 3D Secure flows และมีส่วนประกอบ UI การชำระเงินที่ปรับแต่งได้
ฟีเจอร์หลัก
- การใช้งาน Android แบบเนทีฟ - สร้างด้วย Kotlin เพื่อการพัฒนา Android สมัยใหม่
- Tokenization ที่ปลอดภัย - Tokenization บัตรฝั่งไคลเอนต์
- การสนับสนุน 3D Secure 2.0 - การตรวจสอบสิทธิ์ที่ราบรื่นกับ Chrome Custom Tabs
- Google Pay Integration - การสนับสนุน Google Pay แบบในตัว
- Material Design UI - ส่วนประกอบการชำระเงินที่ทันสมัยและปรับแต่งได้
- Jetpack Compose Support - ส่วนประกอบที่พร้อม Compose
ข้อกำหนดเบื้องต้น
ก่อนที่จะใช้งานการยอมรับการชำระเงิน:
- ทำการติดตั้ง SDK เสร็จสิ้น (ดู Developer Tools > Android SDK)
- กำหนดค่าคีย์สาธารณะของคุณ
- ตั้งค่าแบ็กเอนด์สำหรับการสร้างการเรียกเก็บเงิน
- ทดสอบด้วยข้อมูลประจำตัวพัฒนา
ภาพรวมการไหล Android
ขั้นตอนการชำระเงิน Android ทั่วไป:
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
การสร้างการเรียกเก็บเงินพื้นฐาน
ขั้นตอนที่ 1: เก็บและ Tokenize บัตร
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)
}
}
}
}
ขั้นตอนที่ 2: ใช้แบบฟอร์มการชำระเงินที่สร้างไว้
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
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);
}
});
}
}
การจัดการการตรวจสอบสิทธิ์ 3D Secure
การใช้งาน 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)
}
}
}
}
กำหนดค่า Deep Link ใน 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 ด้วย WebView (ทางเลือก)
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()
}
}
การชำระเงินด้วยบัตรที่บันทึกไว้
การแสดงและการเรียกเก็บเงินจากบัตรที่บันทึกไว้
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
}
}
}
}
การบันทึกบัตรใหม่
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)
}
}
}
แบบฟอร์มการชำระเงินแบบกำหนดเอง
สร้าง UI แบบกำหนดเองด้วย 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
การใช้ 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)
}
}