Android での利用

PAY.JP Android SDKは、PAY.JP を Android アプリに導入するための SDK です。

SDK の提供する「カード情報のトークン化」機能を利用することで、カード情報を事業者さまのサーバーで扱うことなく、安全に支払い処理をおこなうことができます。

ここでは、アプリに SDK を組み込み、クレジットカード情報をトークン化する方法について説明します。

なお、以下のサンプルコードは Kotlin によって記述されています。Java のサンプルコードは、GitHub のリポジトリを参照してください。

payjp/payjp-android: PAY.JP Android SDK

インストール

build.gradle の dependencies に以下を追加します。バージョンは最新のものを指定してください。

implementation "jp.pay:payjp-android:$latest_version"
// カードのスキャン機能を利用する場合のみ追加
implementation "jp.pay:payjp-android-cardio:$latest_version"

SDKの初期化

まず、android.app.Application クラスを継承したアプリケーションクラスの onCreate() で、PAY.JP の公開鍵を設定して SDK を初期化します。

SDK のあらゆる操作を行う前に、アプリケーションクラスの onCreate() 内で Payjp.init() を実行する必要があります。

デバッグオプション等を設定したい場合は、Payjp.init(String) の代わりに Payjp.init(PayjpConfiguration) を利用してください。

以下のサンプルコードでは、テストモードの公開鍵を設定しています。

class SampleApplication: Application() {

    override fun onCreate() {
        super.onCreate()
        // または Payjp.init("pk_test_0383a1b8f91e8a6e3ea0e2a9")
        Payjp.init(PayjpConfiguration.Builder("pk_test_0383a1b8f91e8a6e3ea0e2a9")
            .setDebugEnabled(BuildConfig.DEBUG)
            .setLocale(Locale.JAPAN)
            .build())
    }
}

アプリに組み込む方法を選択する

アプリに SDK を組み込み、トークンを取得する方法は以下の3つから選択できます。事業者さまの要件に合わせて最適な方法を選んでください。

1. 用意されたカードフォーム画面を利用する(推奨)

これから組み込みを始める場合は、この方法を推奨します。

SDK によってコントロールされたフォーム画面を表示します。カード情報のトークン化は、フォーム画面内で SDK がおこなうため、アプリは生成されたトークンをサーバーに送信するだけという最も簡単な方法です。 フォームのスタイルは、Theme を編集してカスタマイズ可能です。

カードフォーム画面を利用する

2. アプリで用意した画面にカードフォームを組み込む

カードフォームの Fragment をアプリの画面に追加する方法です。この場合もカードフォームは SDK が提供するので、アプリはカード情報を扱う必要がありません。 追加の支払い情報などを合わせて表示したい等、1 の方法では実現できないような画面が必要な場合はこちらを推奨します。

画面にカードフォームを組み込む

3. SDKのUIを使わずに直接トークンを生成する

1、2 いずれの方法でも要件を満たせない場合、カード情報から直接トークンを生成することも可能です。

直接トークンを生成する

1. カードフォーム画面を利用する場合(推奨)

STEP 1: フォーム画面を開始する

カードフォーム画面のフローを開始するには、Payjp.cardForm().start() を呼び出します。正常に画面が表示されない場合、SDKの初期化に問題のある可能性があります。

// ActivityやFragmentから呼び出す
fun onClickAddCard() {
    Payjp.cardForm().start(activity = this@SampleActivity, face = PayjpCardForm.FACE_MULTI_LINE)
}

カードフォーム画面は、face によって 2 種類の表示タイプを指定できます。何も指定しない場合は FACE_MULTI_LINE が適用されます。フォームやボタンの色は、Theme によって変更できます(見た目を変更する)。

face スクリーンショット
FACE_MULTI_LINE
FACE_CARD_DISPLAY

STEP 2: フォームの結果を受け取る

カードフォーム画面を呼び出した Activity または Fragment の onActivityResult にカードフォームの結果が渡されます。以下のように、Payjp.cardForm().handleResult を使って結果を取得します。

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    Payjp.cardForm().handleResult(data, PayjpCardFormResultCallback { result ->
        if (result.isSuccess()) {
            val token = result.retrieveToken()
            Log.i(TAG, "token => $token")
        }
    })
    super.onActivityResult(requestCode, resultCode, data)
}

STEP 3: 生成されたトークンをサーバに送信する

通常、カード情報から生成されたトークンは事業者さまのサーバーに送信し、顧客データや支払いデータを作成する等といった処理を行います。 トークン取得後の一連の処理が何らかの事由(たとえばネットワークエラーなど)によって中断した場合、カードフォーム画面が終了していると、再度カードフォーム画面を表示し顧客に入力を求めなければなりません。

そのため、トークン取得後のアプリ内の処理を実行するハンドラをあらかじめ登録し、カードフォーム内でエラーをハンドリングする仕組みを提供しています。

まず、PayjpTokenBackgroundHandler を実装したクラスを作成します。 このクラスは、handleTokenInBackground という関数を実装する必要があります。この関数の中で、トークンをサーバーに送信するなどのハンドリングを行います。このときバックグラウンドで実行される関数のため、UI の操作を直接実行しないように注意してください。

handleTokenInBackground は結果に応じて CardFormStatus を返して終了します。カードフォームは、この CardFormStatus によってトークンハンドラー実行後の振る舞いを決定します。

CardFormStatus#Complete の場合、カードフォーム画面は終了します。CardFormStatus#Error の場合、渡されたメッセージを顧客に表示します。

class SampleSendTokenHandler() : PayjpTokenBackgroundHandler {
    override fun handleTokenInBackground(token: Token): CardFormStatus {
        try {
            apiService.saveToken(token)
            // トークンの保存に成功したらカードフォームを終了する
            return CardFormStatus.Complete()
        } catch (e: IOException) {
            // 失敗したらメッセージを表示する(カードフォームは終了しない)
            return CardFormStatus.Error("error message...")
        }
    }
}

ハンドラーを作成したら、SDK を初期化する際に作成したハンドラーをセットします。

class SampleApplication: Application() {

    override fun onCreate() {
        super.onCreate()
        Payjp.init(PayjpConfiguration.Builder("pk_test_0383a1b8f91e8a6e3ea0e2a9")
            .setDebugEnabled(BuildConfig.DEBUG)
            .setLocale(Locale.JAPAN)
            .setTokenBackgroundHandler(SampleSendTokenHandler()) // <-- この部分を追加
            .build())
    }
}

セットされていない場合、カードフォーム画面はトークンを作成後そのまま終了します。作成されたトークンは Payjp.cardForm().handleResult で取得できます。

以上がカードフォーム画面を使ったアプリへの組み込みの基本となります。

2. カードフォームをアプリの画面に組み込む場合

PayjpCardFormAbstractFragment は、カード情報の入力フォームを持つコンポーネントです。アプリの画面に追加し、任意のタイミングでトークンの作成をリクエストできます。カードフォーム画面のように利用ブランドの表示や登録ボタンなどは付属していませんが、その分より柔軟にアプリに組み込むことができるのがメリットとなります。

サンプルアプリの例(色や TextField のスタイルはカスタマイズ可能です)

STEP 1: フォームの組み込み

カードフォームを表示するには、カードフォームに必要なパラメーターをファクトリーメソッドに渡し、PayjpCardFormAbstractFragment を継承した具象クラスのインスタンスを作成します。

override fun onCreate() {
    // ...
    cardFormFragment = Payjp.cardForm().newCardFormFragment()
    supportFragmentManager.beginTransaction()
        .replace(R.id.card_form_view, cardFormFragment, TAG_CARD_FORM)
        .commit()
    submitButton.setOnClickListener {
        onClickSubmit()
    }
}

カードフォーム画面を利用する場合と同様に、引数に face を指定して表示タイプを切り替えることができます。指定しない場合は FACE_MULTI_LINE が適用されます。

STEP 2: フォームの送信

PayjpCardFormAbstractFragment#createToken は、Task<Token> オブジェクトを返します。 Task<Token> オブジェクトはトークン生成を同期/非同期で実行し、返り値またはコールバックで結果を取得します。トークンの生成に失敗した場合は、取得した例外クラスを利用してハンドリングしてください。詳細はエラーのハンドリングをご確認ください。

fun onClickSubmit() {
    if (!cardFormFragment.validateCardForm()) return
    cardFormFragment.createToken().enqueue(object : Task.Callback<Token> {
        override fun onSuccess(data: Token) {
            Log.i("CardFormViewSample", "token => $data")
        }

        override fun onError(throwable: Throwable) {
            Log.e("CardFormViewSample", "failure creating token", throwable)
        }
    })
}

STEP 3: フォームのバリデーション

カードフォームは、入力内容が適切か入力のたびに検証します。 検証の結果のみを得るには、PayjpCardFormAbstractFragment#isValid を利用します。

PayjpCardFormAbstractFragment#validateCardForm は、暗黙の入力エラーを表示し、検証結果を返します。たとえば、送信ボタンを押したタイミングで入力の検証を行い、未入力の項目があればエラーメッセージを表示したい場合はこちらを利用します。

また、検証結果の変更を受け取りたい場合は、PayjpCardFormView.OnValidateInputListener#onValidateInput をカードフォームのホストに実装します。

override fun onValidateInput(view: PayjpCardFormView, isValid: Boolean) {
    // 入力が正しくなければ送信ボタンを無効にする
    button_create_token.isEnabled = isValid
}

フォームをカスタマイズする

クレジットカード情報をカメラで読み取る

カメラでクレジットカードの券面をスキャンして自動で入力する機能を追加できます。

この機能を利用するには、Card IOのAndroid SDKと PAY.JP Android SDK の Card IO プラグインを追加します(いずれも最新のバージョンを利用してください)。

implementation "jp.pay:payjp-android:$latest_version"
implementation "jp.pay:payjp-android-cardio:$latest_version"
implementation "io.card:android-sdk:x.y.z"

SDK 初期化時にプラグインを設定します。

Payjp.init(PayjpConfiguration.Builder(API_KEY)
    .setCardScannerPlugin(PayjpCardScannerPlugin)
    .build())

これでカードフォームに番号読み取りのカメラアイコンが表示されます。

カード保有者名の入力を非表示にする

PayjpCardFormAbstractFragment を利用する場合、カード保有者名は Payjp.cardForm().newCardFormFragment() の引数または PayjpCardFormAbstractFragment#setCardHolderNameInputEnabled で表示/非表示の切り替えができます(デフォルトは表示)。

カードフォーム画面を利用する場合は非表示にできません。

見た目を変更する

カードフォーム画面のスタイルは Payjp.Theme.CardForm という Theme を編集することでカスタマイズ可能です。 アプリのリソースに以下のように定義します。

<resources>
    <style name="Payjp.Theme.CardForm" parent="Payjp.Theme.BaseCardForm">
        <!-- ActionBarやボタン、カーソルなどの色 -->
        <item name="colorPrimary">@color/primaryColor</item>
        <!-- StatusBarなどの色 -->
        <item name="colorPrimaryDark">@color/primaryDarkColor</item>
        <!-- ProgressBarなどの色 -->
        <item name="colorSecondary">@color/secondaryColor</item>
        <!-- TextInputのスタイル -->
        <item name="textInputStyle">
            @style/Widget.MaterialComponents.TextInputLayout.OutlinedBox
        </item>
        <!-- 背景の色 -->
        <item name="android:windowBackground">@color/backgroundColor</item>
    </style>
</resources>

PayjpCardFormAbstractFragment はアプリの Theme が適用されるため、アプリの Theme を変更することでカスタマイズ可能です。

3. カードフォームを利用せずトークン化する場合

以下のカード情報から直接トークンを生成もできます。

  • クレジットカード番号(例: 4242424242424242
  • CVC(例: 123
  • 有効期限(月)(例: 02
  • 有効期限(年)(例: 2020
  • (任意)カード保有者名(例: PAY TARO

Payjp.token().createToken() を上記のカード情報を引数に呼び出します。 Task<Token> オブジェクトは同期/非同期でトークンの生成処理を実行し、返り値またはコールバックで生成結果を取得できます。トークンの生成に失敗した場合は、取得した例外クラスを利用してハンドリングしてください。詳細は エラーのハンドリング をご確認ください。

エラーのハンドリング

カードフォーム画面を利用する以外の方法でアプリに組み込む場合、トークン生成に失敗した際のエラーハンドリングが必要です。取得した例外クラスに応じて適切にハンドリングしてください。

API からエラーレスポンスを受け取った場合は PayjpApiException クラスの例外として HTTP ステータスコードや構造化されたレスポンス情報を扱うことができます。PayjpApiException#httpStatusCode で HTTP ステータスコードを、PayjpApiException#apiError で PAYJP のエラーコードなどが参照可能です。

ApiError クラスの codeAPIリファレンス のエラーコードに相当します。同様に message はエラーメッセージに相当しますが、このメッセージはエンドユーザー向けのメッセージではないことに注意してください。

また、PayjpApiException にはサブクラスが存在します。 送信したカード情報に問題がある場合、PayjpCardException クラスの例外を返します。たとえば入力したカード番号に誤りがある、カードの有効期限が切れている等、エラーコードに応じてユーザーにフィードバックしてください。

PayjpRateLimitExceptionレートリミット 超過を表す例外クラスです。時間をあけた再試行を提案してください(例:混雑のため、一時的に利用が制限されています。少し時間をおいて再度お試しください。)。

以下は例外クラスに応じてハンドリングするサンプルコードです。

fun handleErrorMessage(t: Throwable) {
    when (t) {
        is PayjpCardException -> showMessageByCode(t.apiError.code)
        is PayjpRateLimitException -> showMessage(
            "混雑のため、一時的に利用が制限されています。少し時間をおいて再度お試しください。"
        )
        is PayjpApiException -> {
            sendlog("statusCode:" + t.httpStatusCode)
            sendlog("payErrorCode:" + t.apiError.code)
            when (t.httpStatusCode) {
                in 500 until 600 -> showMessage("カード決済システムに問題が発生しています。")
                else -> showMessage("カードの認証に問題が発生しました。")
            }
        }
        is IOException -> showMessage("ネットワークへの接続を確認してください。")
        else -> showMessage("問題が発生しました。")
    }
}

生成済みのトークン情報を取得する

トークン ID を利用してすでに生成済みのトークンを取得するには、Payjp.token().getToken を呼び出します。

Payjp.token().getToken(id = tokenId)
    .enqueue(object : Task.Callback<Token> {
        override fun onSuccess(data: Token) {
            Log.i("PAY.JP", "token => $data")
        }

        override fun onError(throwable: Throwable) {
            Log.e("PAY.JP", "failure getting token", throwable)
        }
    })

Kotlin Coroutineと一緒に利用する

トークンの生成などコールバック形式で提供しているインタフェースを Kotlin Coroutine の suspend function で利用できるプラグインを提供しています。

implementation "jp.pay:payjp-android:$latest_version"
implementation "jp.pay:payjp-android-coroutine:$latest_version"

このプラグインには Task オブジェクトを返す SDK のインタフェースに対応する suspend function が定義されています。以下はトークンの生成の例です。

private fun createToken() = launch {
    updateLoadingUI()
    try {
        val token = withContext(Dispatchers.IO) {
            // createToken()の代わりにsuspend functionを使う
            cardFormFragment.createTokenSuspend()
        }
        updateSuccessUI(token)
    } catch (t: Throwable) {
        updateErrorUI(t, "failure creating token")
    }
}

トークン作成リクエストのバーストを抑制する

カード情報入力フォームでユーザーの連打などにより意図せず連続でトークンの作成をリクエストしてしまうと、レートリミット の制限に達する可能性があります。

SDK が提供するカードフォーム画面はあらかじめ対策がなされていますが、カードフォーム画面を利用しない方法でアプリに組み込む場合は、PayjpTokenOperationStatus を利用してユーザーによる連打などで意図せずトークンの作成を連続リクエストしないよう抑制できます。

Payjp.token().getTokenOperationObserver() でステータスの更新を監視します。 PayjpTokenOperationStatusACCEPTABLE のときにトークンを作成できるように UI を更新してください。

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    // ...
    Payjp.token().getTokenOperationObserver().addListener { status ->
        toggleButton(enabled = status == PayjpTokenOperationStatus.ACCEPTABLE)
    }
}

override fun onDestroy() {
    // ...
    Payjp.token().getTokenOperationObserver().removeAllListeners()
    super.onDestroy()
}

PayjpTokenOperationObserverService.addListener で追加したリスナーは removeListener あるいは removeAllListeners で削除してください。

ソースコード・サンプルコード

SDK は GitHub で公開されています。 SDK のソースコードのほか、Java/Kotlin のサンプルコードをご覧いただけます。