Help us understand the problem. What is going on with this article?

「Firebase + stripe + iOS」でクレジットカード決済機能を作る

firebase_stripe.png

Firebase Firestoreでデータを管理しているiOSアプリに決済機能をつけたいと思い、stripeを使うことにしました。そこで得た「Firebase + stripe」だけで決済機能を作る方法を紹介したいと思います。

ゴールは以下のような感じ。

今回扱うのはstripeの「PAYMENTS」

stripeにはいくつかのサービスがあって、今回扱うのは「PAYMENTS」です。

スクリーンショット 2019-06-03 15.22.26.png
https://stripe.com/jp

それぞれの用途は、

PAYMENTS

グッズを買うなどの単発の決済

BILLING

毎月のサブスクリプションなど

CONNECT

フリマアプリ等のCtoCの決済

となっていて、今回はユーザーが単発で決済して運営がそれを受け取るという用途の機能開発なのでPAYMENTSを使います。

手順

手順は以下のようになります。

  1. stripe上に顧客(customer)を作成する
  2. 決済時にcustomerIdを使ってワンタイムトークンをリクエストする
  3. ワンタイムトークンを使ってカード情報を取得
  4. customerIdとカード情報を使って決済リクエストする

省略して書くと、

  1. 顧客の作成
  2. ワンタイムトークン発行
  3. カード情報の取得
  4. 決済

となります。まず、これら一連の流れに必要なAPI作成の話をした後に、iOSから行うこれらの手順を1つずつ紹介していきます。

FunctionsでAPIを作成する

公式のはじめに: 最初の関数を作成してデプロイするを参考にして、Cloud Functionsのプロジェクトを作ります。今回はTypeScriptで作りました。

そしてindex.tsにて3つのAPIを作ります。

  1. customerを作ってcustomerIdをフロントに返すAPI
  2. ワンタイムトークンを発行するAPI
  3. 決済するAPI
index.ts
const stripe = require('stripe')(functions.config().stripe.token);

// MARK: - stripeのcustomerを作ってcustomerIdを返す
exports.createStripeCustomer = functions.https.onCall(async (data, context) => {
    const email = data.email;
    const customer = await stripe.customers.create({email: email});
    const customerId = customer.id;
    return { customerId: customerId }
});

// MARK: - Stripeのワンタイムトークンを発行する
exports.createStripeEphemeralKeys = functions.https.onCall((data, context) => {
    const customerId = data.customerId;
    const stripe_version = data.stripe_version;
    return stripe.ephemeralKeys
        .create({
            customer: customerId,
            stripe_version: stripe_version
        })
});

// MARK: - Stripeの決済する
exports.createStripeCharge = functions.https.onCall((data, context) => {
    const customer = data.customerId;
    const source = data.sourceId;
    const amount = data.amount;

    return stripe.charges.create({
        customer: customer,
        source: source,
        amount: amount,
        currency: "jpy",
    })
});

なぜわざわざstripeの処理を全てFunctionsの中に書くのか?

実は、stripeのシークレットキーをiOSアプリの中に書いちゃえば、これらのAPIをCloud Functionsに書く必要はなく、直接Stripeと通信すればいいのですが、それは、セキュリティ的に推奨されないので、stripeのシークレットキーをFirebase Cloud Functionsの環境変数として置いてdeployしています。

ちなみに環境変数の設定方法は、環境の構成に書いてあって、今回の場合は、

firebase functions:config:set stripe.token="<ここにシークレットキー>"

とコマンドで打って設定しています。

functions.https.onCallトリガーを使った理由

Firebase Cloud FunctionsにはFirestoreへの書き込みをトリガーに発火するfunctionも書けますが、他の処理とのトランザクション処理をフロントでやっちゃいたかったので、アプリから関数を呼び出すを参考に、Cloud FunctionsのiOS SDKを使って直接APIとして呼び出すことにしました。

deployに成功すると、以下のようにFirebaseの管理画面上で確認できます。上の4つの関数は書き込みトリガーなのに対して今回作ったStripe用の関数はHTTPリクエストなのが特徴的ですね。(もちろん場合によって書き込みトリガーでも良いと思います)

スクリーンショット 2019-05-22 13.40.29.png

iOS側の実装

iOS側の実装方法を説明していきます。

顧客の作成

今回は、最初のユーザー作成時にStripeの顧客も作成するようにしました。FirebaseのAuthでユーザーを作成し、そこで得たemailアドレスを使って先ほどのAPIを叩きcustomerIdを取得、最後に細かいプロフィール情報は、FireStoreのUsersというcollectionに格納、その中のパラメータの1つとしてcustomerIdを作って入れるという手順です。

以下のメソッドでは、先ほど作ったcreateStripeCustomerにemailを渡してstripeのcustomerIdを作っています。

import FirebaseFunctions

lazy var functions = Functions.functions()

func createCustomerId(email: String, completion: ((String?, Error?) -> Void)?){
    let data: [String: Any] = [
        "email": email
    ]
    functions.httpsCallable("createStripeCustomer")
        .call(data) { result, error in

            if let error = error {
                completion(nil, error)
            } else if let data = result?.data as? [String: Any],
                let customerId = data["customerId"] as? String {
                completion(customerId, nil)
            }
    }
}

ワンタイムトークン発行

Stripeでは決済する際にワンタイムトークンが必要です。customerIdは既にFirestoreのUserの中に入っているので、それを使ってワンタイムトークンを発行します。

ここではStripeの公式ドキュメントUsing iOS Standard UI Components
を見ながら実装していきます。

まず、stripe-ios SDKをcocoapods等でプロジェクトにいれておきます。

STPCustomerEphemeralKeyProviderに準拠したStripeProviderを作成し、ここで必須メソッドとなるcreateCustomerKeyの中で、先ほど作ったfunctionsのAPIを叩くようにします。この中に書いておくと、必要なタイミングで勝手に呼ばれて叩いてくれます。

import Stripe
import FirebaseFunctions

class StripeProvider: NSObject, STPCustomerEphemeralKeyProvider {
    lazy var functions = Functions.functions()
    let customerId: String

    init(customerId: String){
        self.customerId = customerId
    }

    func createCustomerKey(withAPIVersion apiVersion: String, completion: @escaping STPJSONResponseCompletionBlock) {
        let data: [String: Any] = [
            "customerId": customerId,
            "stripe_version": apiVersion
        ]
        functions
            .httpsCallable("createStripeEphemeralKeys")
            .call(data) { result, error in

                if let error = error {
                    completion(nil, error)
                } else if let data = result?.data as? [String: Any] {
                    completion(data, nil)
                }
        }
    }
}

決済をしたいViewControllerにて、ボタンの押下をトリガーに以下のように実装します。

ViewController
import Stripe

private var paymentContext: STPPaymentContext?

@IBAction func stripeButtonTapped(_ sender: Any) {
    let customerId = "firestoreから取得"
    let customerContext = STPCustomerContext(keyProvider: StripeProvider(customerId: customerId))
    paymentContext = STPPaymentContext(customerContext: customerContext)
    paymentContext!.delegate = self
    paymentContext!.hostViewController = self
    paymentContext!.paymentAmount = 5000
    paymentContext!.presentPaymentOptionsViewController()
}

すると、SDKに用意されたクレジットカード追加のUIが出てきます。これ、驚くべきことにSDKにデフォルトで用意されてるUIなんですよ!いい感じですよね。

stripeのドキュメントにも用意されているテストで使えるcard一覧で使える番号でテストしてみましょう。

カード情報の取得

一度カードを登録すると、stripeのサーバーに登録され、そのカードが次から選択できます。カードを選択すると、STPPaymentContextDelegateのpaymentContextDidChangeメソッドが呼び出され、カード番号の下4桁、カード会社の画像が得られるので、UIでフィードバックできます。

extension ViewController: STPPaymentContextDelegate {
    func paymentContextDidChange(_ paymentContext: STPPaymentContext) {
        cardNameLabel.text = paymentContext.selectedPaymentOption?.label
        cardImageView.image = paymentContext.selectedPaymentOption?.image
    }

    func paymentContext(_ paymentContext: STPPaymentContext, didCreatePaymentResult paymentResult: STPPaymentResult, completion: @escaping STPErrorBlock) {
        // 省略
    }

    func paymentContext(_ paymentContext: STPPaymentContext, didFinishWith status: STPPaymentStatus, error: Error?) {
        // 省略
    }
}

決済

最後に決済です。

paymentContextをViewControllerのグローバル変数として保持しておいて以下のように使います。

paymentContext?.requestPayment()

これをボタンのアクションに埋めます。

@IBAction func payButtonTapped(_ sender: Any) {
    paymentContext?.requestPayment()
}

すると決済確認のリクエストが走るので、この結果をまたdelegateで受けます。

ここではまだ決済が完了しません。paymentResultが作られ、stripeIdが受け取れるので、これとcustomerIdをさらにfunctionsのAPIに渡して、実際の決済はAPIの方でやってもらいます。

lazy var functions = Functions.functions()

extension ViewController: STPPaymentContextDelegate {
    func paymentContextDidChange(_ paymentContext: STPPaymentContext) {
        // 省略
    }

    // paymentContext?.requestPayment()が押されたら呼ばれる
    func paymentContext(_ paymentContext: STPPaymentContext, didCreatePaymentResult paymentResult: STPPaymentResult, completion: @escaping STPErrorBlock) {
        let sourceId = paymentResult.source.stripeID
        let paymentAmount = paymentContext.paymentAmount
        self.functions.httpsCallable("createStripeCharge")
            .call(data) { result, error in

                if let error = error {
                    completion(error)
                } else {
                    completion(nil)
                }
        }
    }

    // 上のcompletionをトリガーに呼ばれる
    func paymentContext(_ paymentContext: STPPaymentContext, didFinishWith status: STPPaymentStatus, error: Error?) {
        switch status {
        case .error:
            self.showErrorDialog(error!) // 独自
        case .success:
            self.showOKDialog(title: "決済に成功しました") // 独自
        case .userCancellation:
            break
        @unknown default:
            break
        }
    }
}

これが一連の流れです。

決済が成功すると、以下のようにstripeの管理画面上で確認ができます。

スクリーンショット 2019-05-20 18.53.34.png

サンプルコードは以下です。
https://github.com/kboy-silvergym/stripe-payments-firebase

参考記事

k-boy
YouTuberをやっています。基本はiOSを5年やってますが今の仕事はFlutterだけです。GithubではARKitのサンプルコードを公開したり、UdemyでARKitの動画講座も作ってます。
https://www.youtube.com/channel/UCevPBAKPBSgJIHU-vSeltlw
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした