Firebase Firestoreでデータを管理しているiOSアプリに決済機能をつけたいと思い、stripeを使うことにしました。そこで得た「Firebase + stripe」だけで決済機能を作る方法を紹介したいと思います。
ゴールは以下のような感じ。
今回扱うのはstripeの「PAYMENTS」
stripeにはいくつかのサービスがあって、今回扱うのは「PAYMENTS」です。
https://stripe.com/jpそれぞれの用途は、
PAYMENTS
グッズを買うなどの単発の決済
BILLING
毎月のサブスクリプションなど
CONNECT
フリマアプリ等のCtoCの決済
となっていて、今回はユーザーが単発で決済して運営がそれを受け取るという用途の機能開発なのでPAYMENTSを使います。
手順
手順は以下のようになります。
- stripe上に顧客(customer)を作成する
- 決済時にcustomerIdを使ってワンタイムトークンをリクエストする
- ワンタイムトークンを使ってカード情報を取得
- customerIdとカード情報を使って決済リクエストする
省略して書くと、
- 顧客の作成
- ワンタイムトークン発行
- カード情報の取得
- 決済
となります。まず、これら一連の流れに必要なAPI作成の話をした後に、iOSから行うこれらの手順を1つずつ紹介していきます。
FunctionsでAPIを作成する
公式のはじめに: 最初の関数を作成してデプロイするを参考にして、Cloud Functionsのプロジェクトを作ります。今回はTypeScriptで作りました。
そしてindex.ts
にて3つのAPIを作ります。
- customerを作ってcustomerIdをフロントに返すAPI
- ワンタイムトークンを発行するAPI
- 決済するAPI
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リクエストなのが特徴的ですね。(もちろん場合によって書き込みトリガーでも良いと思います)
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にて、ボタンの押下をトリガーに以下のように実装します。
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の管理画面上で確認ができます。
サンプルコードは以下です。
https://github.com/kboy-silvergym/stripe-payments-firebase