メインコンテンツへスキップ

Android SDK - 支払いの受付

Omise Android SDKを使用してAndroidアプリケーションで支払いを受け付ける方法を学びます。このガイドでは、請求の作成、3D Secure認証の処理、KotlinとJavaを使用したさまざまな決済フローの実装について説明します。

概要

Omise Android SDKは、モバイルアプリケーションで支払いを受け付けるためのネイティブAndroidインターフェースを提供します。このSDKはトークン化、3D Secureフロー、およびカスタマイズ可能な決済UIコンポーネントを処理します。

主な機能

  • ネイティブAndroid実装 - モダンなAndroid開発のためにKotlinで構築
  • 安全なトークン化 - クライアント側のカードトークン化
  • 3D Secure 2.0サポート - Chrome Custom Tabsによるシームレスな認証
  • Google Pay統合 - 組み込みのGoogle Payサポート
  • Material Design UI - モダンでカスタマイズ可能な決済コンポーネント
  • Jetpack Composeサポート - Compose対応コンポーネント

前提条件

支払い受付を実装する前に:

  1. SDK のインストールを完了する(開発者ツール > Android SDK を参照)
  2. 公開鍵を設定する
  3. 請求作成のためのバックエンドをセットアップする
  4. 開発用認証情報でテストする

決済フローの概要

典型的なAndroid決済フロー:

ユーザーがカード情報を入力

SDKがカードをトークン化(クライアント側)

トークンをバックエンドに送信

バックエンドがトークンで請求を作成

3D Secure認証(必要な場合)

支払い完了

基本的な請求の作成

ステップ1: カードの収集とトークン化

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) {
// バックエンドに送信して請求を作成
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フローの実装

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("支払いに失敗しました")
}
}
}

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") {
// ユーザーが3D Secureから戻った
verifyPaymentStatus()
}
}
}

fun verifyPaymentStatus() {
showLoading("支払いを検証中...")

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

when (result) {
is Result.Success -> {
if (result.data.status == "successful") {
showSuccess()
} else {
showError("支払いの検証に失敗しました")
}
}
is Result.Error -> handleError(result.exception)
}
}
}
}

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>

WebViewを使用した3D Secure(代替方法)

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) {
// 支払いが完了、ステータスを確認
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("支払いを処理中...")

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

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

カードリストアダプター

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("カードを保存中...")

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("カードが正常に保存されました")
finish()
}
is Result.Error -> handleError(result.exception)
}
}
}

カスタム決済フォーム

ViewBindingを使用したカスタムUIの構築

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() {
// カード番号のフォーマット
binding.cardNumber.addTextChangedListener(CardNumberTextWatcher())

// 有効期限のフォーマット
binding.expiry.addTextChangedListener(ExpiryTextWatcher())

// CVVの検証
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 = "無効なカード番号です"
return false
}

if (!CardValidator.isValidCvv(cvv)) {
binding.cvv.error = "無効なCVVです"
return false
}

if (name.isBlank()) {
binding.cardholderName.error = "カード名義人が必要です"
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)
}
}

カード番号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実装

@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("カード番号") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.fillMaxWidth()
)

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

OutlinedTextField(
value = cardholderName,
onValueChange = { cardholderName = it },
label = { Text("カード名義人") },
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("今すぐ支払う")
}
}
}
}

Google Pay統合

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("支払いがキャンセルされました")
}
AutoResolveHelper.RESULT_ERROR -> {
AutoResolveHelper.getStatusFromIntent(data)?.let {
handleError(it.statusMessage ?: "不明なエラー")
}
}
}
}
}
}

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

// Omiseソースと請求を作成するためにバックエンドに送信
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ユーティリティクラス

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) // 本番環境にはPRODUCTIONを使用
.build()

return Wallet.getPaymentsClient(activity, walletOptions)
}
}

エラー処理

包括的なエラー処理

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 ->
"カードの処理に失敗しました: ${error.message}"
is PaymentError.ChargeError ->
error.message
is PaymentError.AuthenticationFailed ->
"支払い認証に失敗しました"
is PaymentError.NetworkError ->
"ネットワークエラー。接続を確認してください。"
is PaymentError.InvalidCard ->
"無効なカード情報です"
is PaymentError.InsufficientFunds ->
"カードの残高が不足しています"
is PaymentError.CardDeclined ->
"銀行によってカードが拒否されました"
else ->
"エラーが発生しました: ${error.message}"
}

showError(message)
logError(error)
}

fun showError(message: String) {
MaterialAlertDialogBuilder(context)
.setTitle("支払いエラー")
.setMessage(message)
.setPositiveButton("OK", null)
.show()
}

fun logError(error: Throwable) {
// 分析サービスにログを記録
FirebaseCrashlytics.getInstance().recordException(error)
}
}

一般的な使用例

サブスクリプション支払い

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 "月額$planサブスクリプション",
"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}サブスクリプション"
)

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

分割支払い

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

// すべての請求を処理
Observable.fromIterable(charges)
.flatMap { charge -> processChargeAsync(charge) }
.toList()
.subscribe(
{ results -> handleSplitPaymentComplete(results) },
{ error -> handleError(error) }
)
}

ベストプラクティス

セキュリティ

  1. 機密データを保存しない

    // ✅ 良い - トークンを使用
    val token = createToken(cardData)
    sendToBackend(token.id)

    // ❌ 悪い - カードデータを保存しない
    sharedPrefs.edit().putString("card_number", cardNumber).apply()
  2. 入力を検証する

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

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

    return true
    }
  3. ProGuard/R8を使用する

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

パフォーマンス

  1. コルーチンを使用する

    suspend fun createPayment(amount: Long) = withContext(Dispatchers.IO) {
    val token = omiseClient.sendSuspend(cardParam)
    val charge = apiClient.createCharge(token.id, amount)
    charge
    }
  2. 決済方法をキャッシュする

    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)
    }

トラブルシューティング

一般的な問題

トークン作成に失敗する

// 公開鍵を確認
val client = OmiseClient("pkey_test_123")

// カード詳細を検証
Log.d("Payment", "Card valid: ${CardValidator.isValidCardNumber(number)}")
Log.d("Payment", "CVV valid: ${CardValidator.isValidCvv(cvv)}")

3D Secureが機能しない

// マニフェストでディープリンクを確認
// 戻りURIが一致することを確認
// 3DSテストカード4000000000003063でテスト

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

Google Payが利用できない

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

FAQ

一般的な質問

Q: AndroidアプリにPCIコンプライアンスは必要ですか?

A: いいえ。Omise Android SDKはクライアント側でカードデータをトークン化するため、PCIコンプライアンス要件が大幅に軽減されます。ただし、セキュリティのベストプラクティスに従う必要があります。

Q: オフラインで支払いを受け付けることはできますか?

A: 部分的にサポート。カード情報をオフラインで収集し、後でトークン化できますが、請求の作成にはインターネット接続が必要です。

Q: どのAndroidバージョンがサポートされていますか?

A: Omise Android SDKはAndroid 5.0(APIレベル21)以上をサポートします。最良の結果を得るには、Android 8.0(API 26)以上をターゲットにしてください。

Q: 支払いをテストする方法は?

A: テストカード番号(4242 4242 4242 4242)と任意の将来の有効期限、任意の3桁のCVVを使用します。テスト公開鍵(pkey_test_xxx)を使用してください。

Q: 決済UIをカスタマイズできますか?

A: はい。事前構築されたコンポーネントをカスタマイズして使用するか、トークン化のみにSDKを使用して独自のUIを構築できます。

Q: トークンは再利用可能ですか?

A: いいえ。トークンは請求の作成または顧客への添付に使用された後、有効期限が切れます。

実装に関する質問

Q: KotlinとJavaのどちらを使用すべきですか?

A: より良いnullセーフティとモダンなAndroid開発のためにKotlinをお勧めしますが、SDKはJavaを完全にサポートしています。

Q: Jetpack Composeを使用できますか?

A: はい。SDKはComposeと互換性があります。相互運用を使用するか、SDKの周りにカスタムComposeコンポーネントを構築してください。

Q: 3D Secureのタイムアウトを処理する方法は?

A: ディープリンクハンドラーでタイムアウト検出を実装し、ユーザーに再試行オプションを提供してください。

Q: サンドボックスで3D Secureをテストできますか?

A: はい。テストモードで3D Secure認証をトリガーするには、テストカード4000000000003063を使用してください。

関連リソース

次のステップ