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

iOS SDK - 支払いの受付

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

概要

Omise iOS SDKは、iOSアプリケーションで支払いを受け付けるためのネイティブSwiftおよびObjective-Cインターフェースを提供します。このSDKは以下を処理します:

  • 支払い情報のトークン化
  • 3D Secure認証フロー
  • Apple Pay統合
  • カードスキャン機能
  • 決済方法選択UI

主な機能

  • ネイティブiOS実装 - 最適なパフォーマンスのためにSwiftで構築
  • 安全なトークン作成 - PCIコンプライアンスの負担なしのクライアント側トークン化
  • 3D Secure 2.0サポート - シームレスな認証処理
  • Apple Pay対応 - 組み込みのApple Payサポート
  • カスタマイズ可能なUI - 事前構築された決済フォームまたはカスタム実装
  • オフラインサポート - 後で処理するための支払いキュー

前提条件

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

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

決済フローの概要

典型的なiOS決済フロー:

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

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

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

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

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

支払い完了

基本的な請求の作成

ステップ1: カード情報の収集

import OmiseSDK

class PaymentViewController: UIViewController {

func processPayment(amount: Int64, currency: String) {
// 支払いリクエストを作成
let request = OmiseTokenRequest(
name: "John Appleseed",
number: "4242424242424242",
expirationMonth: 12,
expirationYear: 2025,
securityCode: "123"
)

// トークンを作成
let client = OmiseSDKClient(publicKey: "pkey_test_123")

client.send(request) { [weak self] result in
switch result {
case .success(let token):
self?.createCharge(with: token.id, amount: amount, currency: currency)
case .failure(let error):
self?.handleError(error)
}
}
}

func createCharge(with tokenId: String, amount: Int64, currency: String) {
// バックエンドに送信
let parameters: [String: Any] = [
"token": tokenId,
"amount": amount,
"currency": currency,
"return_uri": "myapp://payment/complete"
]

// バックエンドへのAPIコールを実行
APIClient.shared.post("/charges", parameters: parameters) { result in
switch result {
case .success(let charge):
self.handleChargeResult(charge)
case .failure(let error):
self.handleError(error)
}
}
}
}

ステップ2: 事前構築された決済フォームの使用

import OmiseSDK

class CheckoutViewController: UIViewController {

func presentPaymentForm() {
let capability = loadCapability() // バックエンドから読み込む

let paymentConfiguration = PaymentCreatorConfiguration(
publicKey: "pkey_test_123",
amount: 100000, // 最小通貨単位で1,000.00
currency: .thb,
capability: capability
)

let paymentController = PaymentCreatorController.makePaymentCreatorController(
configuration: paymentConfiguration
) { [weak self] result in
self?.handlePaymentResult(result)
}

present(paymentController, animated: true)
}

func handlePaymentResult(_ result: Result<PaymentToken, Error>) {
switch result {
case .success(let token):
// トークンをバックエンドに送信して請求を作成
createChargeOnBackend(token: token)

case .failure(let error):
showError(error.localizedDescription)
}
}

func createChargeOnBackend(token: PaymentToken) {
let parameters: [String: Any] = [
"source": token.id,
"amount": 100000,
"currency": "THB",
"return_uri": "myapp://payment/complete"
]

APIClient.shared.post("/charges", parameters: parameters) { result in
// 請求作成結果を処理
self.processChargeResponse(result)
}
}
}

3D Secure認証の処理

3D Secureフローの実装

import OmiseSDK
import SafariServices

class PaymentProcessor {

var authorizingViewController: SFSafariViewController?

func handleChargeResponse(_ charge: Charge) {
guard charge.status != .successful else {
// 3DSなしで支払いが完了
showSuccess()
return
}

// 3D Secure認証が必要かチェック
if charge.status == .pending,
let authorizeURL = charge.authorizeURL {
present3DSecure(url: authorizeURL)
}
}

func present3DSecure(url: URL) {
let safariVC = SFSafariViewController(url: url)
safariVC.delegate = self
authorizingViewController = safariVC

present(safariVC, animated: true)
}

// 3D Secureからの戻りを処理
func application(_ app: UIApplication,
open url: URL,
options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {

guard url.scheme == "myapp",
url.host == "payment" else {
return false
}

// Safariビューコントローラーを閉じる
authorizingViewController?.dismiss(animated: true) {
// バックエンドで支払いステータスを確認
self.verifyPaymentStatus()
}

return true
}

func verifyPaymentStatus() {
// バックエンドから請求ステータスを確認
APIClient.shared.get("/charges/verify") { result in
switch result {
case .success(let charge):
if charge.status == .successful {
self.showSuccess()
} else if charge.status == .failed {
self.showError("支払いの検証に失敗しました")
}
case .failure(let error):
self.showError(error.localizedDescription)
}
}
}
}

extension PaymentProcessor: SFSafariViewControllerDelegate {
func safariViewControllerDidFinish(_ controller: SFSafariViewController) {
// ユーザーが3DSをキャンセル
showError("認証がキャンセルされました")
}
}

URLスキームの設定

Info.plistで:

<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>myapp</string>
</array>
<key>CFBundleURLName</key>
<string>com.mycompany.myapp</string>
</dict>
</array>

保存されたカードでの支払い

保存されたカードでの請求

class SavedCardPayment {

func chargeWithSavedCard(customerId: String, cardId: String, amount: Int64) {
let parameters: [String: Any] = [
"customer": customerId,
"card": cardId,
"amount": amount,
"currency": "THB",
"return_uri": "myapp://payment/complete"
]

APIClient.shared.post("/charges", parameters: parameters) { result in
switch result {
case .success(let charge):
self.handleChargeResponse(charge)
case .failure(let error):
self.handleError(error)
}
}
}

func displaySavedCards(customerId: String) {
APIClient.shared.get("/customers/\(customerId)/cards") { result in
switch result {
case .success(let cards):
self.showCardSelection(cards)
case .failure(let error):
self.handleError(error)
}
}
}

func showCardSelection(_ cards: [Card]) {
let alert = UIAlertController(
title: "カードを選択",
message: "決済方法を選択してください",
preferredStyle: .actionSheet
)

for card in cards {
let action = UIAlertAction(
title: "\(card.brand) •••• \(card.lastDigits)",
style: .default
) { _ in
self.chargeSelectedCard(card)
}
alert.addAction(action)
}

alert.addAction(UIAlertAction(title: "キャンセル", style: .cancel))
present(alert, animated: true)
}
}

新しいカードの保存

func saveCardForFutureUse(customerId: String) {
let request = OmiseTokenRequest(
name: "John Appleseed",
number: "4242424242424242",
expirationMonth: 12,
expirationYear: 2025,
securityCode: "123"
)

let client = OmiseSDKClient(publicKey: "pkey_test_123")

client.send(request) { result in
switch result {
case .success(let token):
self.attachCardToCustomer(customerId: customerId, tokenId: token.id)
case .failure(let error):
self.handleError(error)
}
}
}

func attachCardToCustomer(customerId: String, tokenId: String) {
let parameters = ["card": tokenId]

APIClient.shared.post("/customers/\(customerId)/cards", parameters: parameters) { result in
switch result {
case .success:
self.showSuccess("カードが正常に保存されました")
case .failure(let error):
self.handleError(error)
}
}
}

カスタム決済フォーム

カスタムUIの構築

import OmiseSDK

class CustomPaymentForm: UIViewController {

@IBOutlet weak var cardNumberField: UITextField!
@IBOutlet weak var expiryField: UITextField!
@IBOutlet weak var cvvField: UITextField!
@IBOutlet weak var cardholderField: UITextField!
@IBOutlet weak var payButton: UIButton!

let client = OmiseSDKClient(publicKey: "pkey_test_123")

@IBAction func payButtonTapped(_ sender: UIButton) {
guard validateForm() else {
showError("すべての必須フィールドを入力してください")
return
}

createToken()
}

func validateForm() -> Bool {
guard let number = cardNumberField.text, !number.isEmpty,
let expiry = expiryField.text, !expiry.isEmpty,
let cvv = cvvField.text, !cvv.isEmpty,
let name = cardholderField.text, !name.isEmpty else {
return false
}

// Luhnアルゴリズムを使用してカード番号を検証
let isValid = OmiseCardValidator.validate(number: number)
return isValid
}

func createToken() {
let expiry = parseExpiry(expiryField.text ?? "")

let request = OmiseTokenRequest(
name: cardholderField.text ?? "",
number: cardNumberField.text ?? "",
expirationMonth: expiry.month,
expirationYear: expiry.year,
securityCode: cvvField.text ?? ""
)

showLoading()

client.send(request) { [weak self] result in
self?.hideLoading()

switch result {
case .success(let token):
self?.processPayment(with: token.id)
case .failure(let error):
self?.showError(error.localizedDescription)
}
}
}

func parseExpiry(_ text: String) -> (month: Int, year: Int) {
let components = text.components(separatedBy: "/")
let month = Int(components[0].trimmingCharacters(in: .whitespaces)) ?? 1
let year = Int(components[1].trimmingCharacters(in: .whitespaces)) ?? 2025
return (month, year)
}
}

カード番号のフォーマット

extension CustomPaymentForm: UITextFieldDelegate {

func textField(_ textField: UITextField,
shouldChangeCharactersIn range: NSRange,
replacementString string: String) -> Bool {

guard textField == cardNumberField else {
return true
}

let currentText = textField.text ?? ""
let newText = (currentText as NSString).replacingCharacters(in: range, with: string)
let digitsOnly = newText.components(separatedBy: .decimalDigits.inverted).joined()

// 16桁に制限
guard digitsOnly.count <= 16 else {
return false
}

// スペースでフォーマット
let formatted = formatCardNumber(digitsOnly)
textField.text = formatted

// カードブランドアイコンを更新
updateCardBrandIcon(digitsOnly)

return false
}

func formatCardNumber(_ number: String) -> String {
var formatted = ""
for (index, char) in number.enumerated() {
if index > 0 && index % 4 == 0 {
formatted += " "
}
formatted.append(char)
}
return formatted
}

func updateCardBrandIcon(_ number: String) {
let brand = OmiseCardBrand.detect(from: number)
// カードブランドアイコンでUIを更新
cardBrandImageView.image = UIImage(named: brand.rawValue)
}
}

Apple Pay統合

Apple Payの実装

import PassKit

class ApplePayProcessor: NSObject {

func startApplePay(amount: Decimal, currency: String) {
let request = PKPaymentRequest()
request.merchantIdentifier = "merchant.com.mycompany.myapp"
request.supportedNetworks = [.visa, .masterCard, .amex]
request.merchantCapabilities = .capability3DS
request.countryCode = "TH"
request.currencyCode = currency

let item = PKPaymentSummaryItem(
label: "合計",
amount: NSDecimalNumber(decimal: amount)
)
request.paymentSummaryItems = [item]

guard let controller = PKPaymentAuthorizationController(paymentRequest: request) else {
return
}

controller.delegate = self
controller.present()
}
}

extension ApplePayProcessor: PKPaymentAuthorizationControllerDelegate {

func paymentAuthorizationController(
_ controller: PKPaymentAuthorizationController,
didAuthorizePayment payment: PKPayment,
handler completion: @escaping (PKPaymentAuthorizationResult) -> Void
) {
// Apple PayトークンをOmiseトークンに変換
processApplePayToken(payment.token) { result in
switch result {
case .success:
completion(PKPaymentAuthorizationResult(status: .success, errors: nil))
case .failure(let error):
let errors = [error]
completion(PKPaymentAuthorizationResult(status: .failure, errors: errors))
}
}
}

func paymentAuthorizationControllerDidFinish(
_ controller: PKPaymentAuthorizationController
) {
controller.dismiss()
}

func processApplePayToken(_ token: PKPaymentToken,
completion: @escaping (Result<Void, Error>) -> Void) {
let paymentData = token.paymentData

// Omiseトークンと請求を作成するためにバックエンドに送信
APIClient.shared.post("/apple-pay/process", parameters: [
"payment_data": paymentData.base64EncodedString()
]) { result in
completion(result.map { _ in () })
}
}
}

エラー処理

包括的なエラー処理

enum PaymentError: LocalizedError {
case tokenizationFailed(Error)
case chargeFailed(String)
case authenticationFailed
case networkError(Error)
case invalidCard
case insufficientFunds
case cardDeclined

var errorDescription: String? {
switch self {
case .tokenizationFailed(let error):
return "カードの処理に失敗しました: \(error.localizedDescription)"
case .chargeFailed(let message):
return message
case .authenticationFailed:
return "支払い認証に失敗しました"
case .networkError:
return "ネットワークエラー。接続を確認してください。"
case .invalidCard:
return "無効なカード情報です"
case .insufficientFunds:
return "残高不足です"
case .cardDeclined:
return "銀行によってカードが拒否されました"
}
}
}

class PaymentErrorHandler {

func handleError(_ error: Error) {
if let omiseError = error as? OmiseError {
handleOmiseError(omiseError)
} else {
showGenericError(error)
}
}

func handleOmiseError(_ error: OmiseError) {
switch error.code {
case .invalidCard:
showError("カード情報を確認してください")
case .insufficientFunds:
showError("カードの残高が不足しています")
case .stolenOrLostCard:
showError("カードが紛失または盗難として報告されています")
case .failedProcessing:
showError("支払い処理に失敗しました")
default:
showError(error.message)
}

// 分析のためにログを記録
logError(error)
}

func showError(_ message: String) {
let alert = UIAlertController(
title: "支払いエラー",
message: message,
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "OK", style: .default))

// アラートを表示
if let topVC = UIApplication.topViewController() {
topVC.present(alert, animated: true)
}
}
}

一般的な使用例

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

class SubscriptionPayment {

func processSubscriptionPayment(plan: String, customerId: String) {
// カードでの最初の支払い
let parameters: [String: Any] = [
"customer": customerId,
"amount": 99900, // 999.00 THB
"currency": "THB",
"description": "月額\(plan)サブスクリプション",
"metadata": [
"plan": plan,
"type": "subscription"
]
]

APIClient.shared.post("/charges", parameters: parameters) { result in
switch result {
case .success(let charge):
if charge.status == .successful {
// バックエンドでスケジュールを作成
self.createRecurringSchedule(customerId: customerId, plan: plan)
}
case .failure(let error):
self.handleError(error)
}
}
}

func createRecurringSchedule(customerId: String, plan: String) {
let parameters: [String: Any] = [
"customer": customerId,
"amount": 99900,
"period": "month",
"description": "\(plan)サブスクリプション"
]

APIClient.shared.post("/schedules", parameters: parameters) { result in
self.handleScheduleCreation(result)
}
}
}

1回限りの購入

class OneTimePurchase {

func processCheckout(items: [CartItem]) {
let total = items.reduce(0) { $0 + $1.price }

// 決済フォームを表示
showPaymentForm(amount: total) { tokenId in
self.createOrder(items: items, tokenId: tokenId)
}
}

func createOrder(items: [CartItem], tokenId: String) {
let orderItems = items.map { [
"id": $0.id,
"quantity": $0.quantity
]}

let parameters: [String: Any] = [
"token": tokenId,
"items": orderItems,
"return_uri": "myapp://order/complete"
]

APIClient.shared.post("/orders", parameters: parameters) { result in
self.handleOrderCreation(result)
}
}
}

複数通貨チェックアウト

class MultiCurrencyCheckout {

func processInternationalPayment(amount: Int64, currency: String) {
// 必要に応じて通貨を変換
convertCurrency(amount: amount, from: "THB", to: currency) { convertedAmount in
self.createPayment(amount: convertedAmount, currency: currency)
}
}

func createPayment(amount: Int64, currency: String) {
let parameters: [String: Any] = [
"amount": amount,
"currency": currency,
"description": "国際購入"
]

showPaymentForm(parameters: parameters)
}
}

ベストプラクティス

セキュリティ

  1. カードデータを保存しない

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

    // ❌ 悪い - 生のカードデータを保存しない
    UserDefaults.standard.set(cardNumber, forKey: "card")
  2. クライアントとサーバーで検証

    func validateCard() -> Bool {
    guard OmiseCardValidator.validate(number: cardNumber) else {
    return false
    }

    guard OmiseCardValidator.validateCVV(cvv, for: cardBrand) else {
    return false
    }

    return true
    }
  3. 安全な通信を使用

    let configuration = URLSessionConfiguration.default
    configuration.tlsMinimumSupportedProtocolVersion = .TLSv12

ユーザーエクスペリエンス

  1. 明確なローディング状態を表示

    func showPaymentProgress() {
    let hud = MBProgressHUD.showAdded(to: view, animated: true)
    hud.label.text = "支払いを処理中..."
    hud.detailsLabel.text = "お待ちください"
    }
  2. 支払いフィードバックを提供

    func showPaymentSuccess() {
    let alert = UIAlertController(
    title: "支払い成功",
    message: "お支払いが処理されました",
    preferredStyle: .alert
    )

    // ハプティックフィードバック
    let generator = UINotificationFeedbackGenerator()
    generator.notificationOccurred(.success)
    }
  3. ネットワークの問題を処理

    func retryPayment() {
    let alert = UIAlertController(
    title: "接続の問題",
    message: "支払いを処理できません。再試行しますか?",
    preferredStyle: .alert
    )

    alert.addAction(UIAlertAction(title: "再試行", style: .default) { _ in
    self.processPayment()
    })

    alert.addAction(UIAlertAction(title: "キャンセル", style: .cancel))
    present(alert, animated: true)
    }

パフォーマンス

  1. トークン作成を最適化

    private var tokenCreationTask: URLSessionTask?

    func createToken() {
    tokenCreationTask?.cancel()

    tokenCreationTask = client.send(request) { result in
    // 結果を処理
    }
    }
  2. 決済方法をキャッシュ

    private var cachedCards: [Card]?
    private var cacheTimestamp: Date?

    func loadSavedCards(forceRefresh: Bool = false) {
    if !forceRefresh,
    let cached = cachedCards,
    let timestamp = cacheTimestamp,
    Date().timeIntervalSince(timestamp) < 300 {
    displayCards(cached)
    return
    }

    fetchCardsFromServer()
    }

トラブルシューティング

一般的な問題

トークン作成に失敗する

// 公開鍵設定を確認
let client = OmiseSDKClient(publicKey: "pkey_test_123")

// カード詳細を確認
print("Card number valid: \(OmiseCardValidator.validate(number: number))")
print("CVV valid: \(OmiseCardValidator.validateCVV(cvv, for: brand))")

3D Secureが機能しない

// 戻りURIが登録されていることを確認
// Info.plistでURLスキームを確認
// 認証URLが有効であることを確認

if let url = charge.authorizeURL {
print("Auth URL: \(url)")
present3DSecure(url: url)
}

Apple Payが利用できない

if PKPaymentAuthorizationController.canMakePayments() {
if PKPaymentAuthorizationController.canMakePayments(
usingNetworks: [.visa, .masterCard]
) {
showApplePayButton()
}
} else {
print("このデバイスではApple Payが利用できません")
}

デバッグモード

#if DEBUG
extension OmiseSDKClient {
func enableDebugMode() {
// すべてのリクエストをログに記録
self.logger = { request in
print("API Request: \(request)")
}
}
}
#endif

FAQ

一般的な質問

Q: iOSアプリでPCIコンプライアンスを処理する必要がありますか?

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

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

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

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

A: Omise iOS SDKはiOS 12.0以降をサポートします。最良の結果と最新機能のために、iOS 14.0以降をサポートすることをお勧めします。

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

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

Q: 決済フォームのUIをカスタマイズできますか?

A: はい。カスタマイズオプション付きの事前構築された決済コントローラーを使用するか、トークン化のみにSDKを使用して独自のUIを構築できます。

Q: トークンはどれくらいの期間有効ですか?

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

3D Secureに関する質問

Q: すべての支払いに3D Secureが必要ですか?

A: すべての支払いに3D Secureが必要なわけではありません。カード発行者、金額、および加盟店の設定によって異なります。実装は両方のフローを処理する必要があります。

Q: ユーザーが3D Secureをキャンセルした場合はどうなりますか?

A: 支払いは失敗し、請求ステータスはpendingまたはfailedのままになります。Safariビューコントローラーデリゲートでこれを処理してください。

Q: 3D Secure認証をテストできますか?

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

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

A: 認証ハンドラーでタイムアウト検出を実装し、ユーザーに支払いを再試行またはキャンセルするオプションを提供してください。

Apple Payに関する質問

Q: Apple Payには特別な承認が必要ですか?

A: はい。Apple Developer アカウント、マーチャント ID 設定、および支払い処理証明書が必要です。Apple Pay ドキュメントを参照してください。

Q: テストモードでApple Payを使用できますか?

A: はい、ただしテスト証明書を設定し、Sandbox環境の認証情報を使用する必要があります。

Q: Apple Payとカード支払いの違いは何ですか?

A: Apple Payはトークン化されたカードデータを使用し、強化されたセキュリティを提供します。実装フローは異なりますが、バックエンドの請求作成は似ています。

関連リソース

次のステップ