【Next.js】StripeとFirebaseにユーザー&クレカ登録同期処理ハンズオン
Next.jsで決済処理を実装したいという場合、FirebaseとStripeへ同時にユーザー登録をしたいという場合があるはずです。(厳密には、Stripeは顧客登録)
今回はStripeとFirebase Authenticationへのユーザー登録を同期させ、その後、クレジットカードを登録するという仕組みが体験できるハンズオンを進めていきます。
- 前提として
- 全体の仕組み
- ベースプロジェクトの準備
- Firebaseの準備
- StripeとFirebase Authenticationのユーザー登録同期
- StripeのtokenをFirestoreに格納する
- token作成をトリガーにしたStripe sourceの作成
前提として
以下の項目については詳しく説明しませんので、理解しているという前提で進めていきます。
- Firebaseの設定
- Firebase Authentication
- HOC(高階コンポーネント)
- Stripeへのアカウント登録
また、以下の記事を参考にしていただければ、上記の前提知識についても身に付けられるかと思います。
全体の仕組み
複雑ですが、以下の仕組みで今回の実装を進めていきます。
ベースプロジェクトの準備
まずはベースとなるNext.jsのプロジェクトをgit cloneしましょう。
$ git clone git@github.com:mesh1nek0x0/stripe-nextjs-on-firebase.git
次にプロジェクト内に移動し、今回使用するブランチへ移動します。
$ cd stripe-nextjs-on-firebase/ $ git checkout start
Firebaseの準備
次にFirebaseのプロジェクトを作成し、Firebase SDK snippetをプロジェクトの設定から取得しましょう。
(例)Firebase SDK snippet
<!-- The core Firebase JS SDK is always required and must be listed first --> <script src="https://www.gstatic.com/firebasejs/7.1.0/firebase-app.js"></script> <!-- TODO: Add SDKs for Firebase products that you want to use https://firebase.google.com/docs/web/setup#available-libraries --> <script src="https://www.gstatic.com/firebasejs/7.1.0/firebase-analytics.js"></script> <script> // Your web app's Firebase configuration var firebaseConfig = { apiKey: "xxxxxxxxxxxxxxxxxxxxxxxxxxxx", authDomain: "xxxxxxxxxxxxxxxxxxxxxxxxxxxx", databaseURL: "xxxxxxxxxxxxxxxxxxxxxxxxxxxx", projectId: "practice-pf-funclub", storageBucket: "xxxxxxxxxxxxxxxxxxxxxxxxxxxx", messagingSenderId: "xxxxxxxxxxxxxxxxxxxxxxxxxxxx", appId: "xxxxxxxxxxxxxxxxxxxxxxxxxxxx", measurementId: "xxxxxxxxxxxxxxxxxxxxxxxxxxxx" }; // Initialize Firebase firebase.initializeApp(firebaseConfig); firebase.analytics(); </script>
次に.firebasercの中身を自身のFirebaseプロジェクト名に書き換えましょう。
{ "projects": { "default": "practice-pf-funclub" } }
.envをルートディレクトリに作成し、Firebase SDK snippetから取得したデータを以下のように書き換えましょう。
.env
apiKey=xxxxxxxxxxxxxxxxxxxxxxxxxxxx
authDomain=xxxxxxxxxxxxxxxxxxxxxxxxxxxx
databaseURL=xxxxxxxxxxxxxxxxxxxxxxxxxxxx
projectId=xxxxxxxxxxxxxxxxxxxxxxxxxxxx
storageBucket=xxxxxxxxxxxxxxxxxxxxxxxxxxxx
messagingSenderId=xxxxxxxxxxxxxxxxxxxxxxxxxxxx
appId=xxxxxxxxxxxxxxxxxxxxxxxxxxxx
// stripeのパブリッシュキー(pkから始まるもの)も同時に設定
stripeKey=xxxxxxxxxxxxxxxxxxxxxxxxxxxx
次にlib/firebase/index.jsの設定に追記し、firestoreを使用できるようにしておきます。
lib/firebase/index.js
import firebase from "firebase/app"; import "firebase/auth"; import "firebase/firestore"; const config = { apiKey: process.env.apiKey, authDomain: process.env.authDomain, databaseURL: process.env.databaseURL, projectId: process.env.projectId, storageBucket: process.env.storageBucket, messagingSenderId: process.env.messagingSenderId }; if (!firebase.apps.length) { firebase.initializeApp(config); } const auth = firebase.auth(); const firestore = firebase.firestore(); export { auth, firestore, firebase };
Firebaseの料金プランを変更
実はFirebaseの料金プランがFreeのままの場合、Stripeのような外部へ通信することができません。
そのため、Firebaseのコンソールから料金プランを変更しておきましょう。
Blaze(従量課金制)だと、一定以下の通信は無料となるので、試しに使ってみる分にはこちらがおすすめです。
Firebase AuthenticationでGoogle登録を有効にする
Firebase Authenticationを使用してSNSユーザー登録を実装していきます。
そのため、FirebaseコンソールからFirebase AuthenticationのSNSユーザー認証におけるGoogleを有効にしておきましょう。
詳しくは以下の記事に記載されています。
nekorokkekun.hatenablog.com
StripeとFirebase Authenticationのユーザー登録同期
以下の順に処理を行っていきます。
- Next.js上でサインイン
- Firebase Authenticationでユーザーが作成される
- Firebase functionsのトリガーが起動
- Cloud Firestoreのstripe_customersコレクション以下にユーザーIDが保存される
- Stripeの顧客に登録される
上記の流れを図にすると以下のようになります。
工程は複雑ですが、実際はユーザー登録のみで一気に全て行えるように設定していきましょう。
現状の確認
まずは現在のNext.jsの画面を確認してみましょう。
$ yarn install $ yarn dev
これで http://localhost:3000 にアクセスしてみましょう。
すると以下のような画面が表示されます。
Firebase functions内の設定
まずはFirebase functionsの設定をしていきましょう。
Firebase functionsにStripeのSecret API Keyを設定
次にFirebase functionsの環境変数にStripeのSecret API Keyを設定します。
firebase functions:config:set stripe.token="sk_test_xxxxxxxxxxxxxxxxxxxxxx" ✔ Functions config updated. Please deploy your functions for the change to take effect by running firebase deploy --only functions
Firebase functions本体の設定
次にFirebase functions本体を設定しましょう。
functions/index.js
const functions = require("firebase-functions"); const admin = require("firebase-admin"); admin.initializeApp(); const stripe = require("stripe")(functions.config().stripe.token); const currency = functions.config().stripe.currency || "JPY"; exports.createStripeCustomer = functions.auth.user().onCreate(async user => { const customer = await stripe.customers.create({ email: user.email }); return admin .firestore() .collection("stripe_customers") .doc(user.uid) .set({ customer_id: customer.id }); });
こちらは細かく解説をしていきます。
const functions = require("firebase-functions"); const admin = require("firebase-admin"); admin.initializeApp(); const stripe = require("stripe")(functions.config().stripe.token); const currency = functions.config().stripe.currency || "JPY";
この辺りは必要なモジュールやライブラリなどを読んでいます。
functions.auth.user().onCreate
「Firebase Authenticationでのユーザーをトリガーに以下の処理を行う」という意味になります。
const customer = await stripe.customers.create({ email: user.email });
「Stripeの顧客作成を行う上でStripeのユーザーのemail情報にはFirebase Authenticationのユーザーのemailを設定する」という意味になります。
こちらで出てくるawaitの後の「stripe」は、先ほどaddしたstripeモジュールから呼び込んでいるものです。
return admin .firestore() .collection("stripe_customers") .doc(user.uid) .set({ customer_id: customer.id });
「Firestoreのstripe_customersコレクション > user.uidドキュメント以下にcustomer_idフィールドを作成して値を格納する」という意味になります。
このFirebase functionsを設定することで、ユーザー登録をすることによって一気にStripeとFirebase Authenticationにユーザー登録をし、Firestoreにcustomer_idを格納するというところまでの処理が進みます。
Firebase functionsのデプロイ
functions/index.jsの設定をFirebase functionsにデプロイ するために、functionsディレクトリ内で以下のコマンドを入力しましょう。
$ yarn deploy
これで「✔ functions[createStripeCustomer(us-central1)]: Successful update operation. 」と表示されればデプロイ 完了です。
StripeのtokenをFirestoreに格納する
Stripeは、顧客のクレジットカード情報を私たちの代わりに保管してくれます。
そして登録されたクレジットカード情報を操作するためにはtokenを発行しなければなりません。
tokenの説明についてわかりやすい解説があったため引用します。
Stripeによるクレジットカード決済では何よりも”トークン”を取得する事が重要となります。
サービス提供者側では直接クレジットカードの情報を取得・管理する必要はなく、Stripe側でそれらを全て管理してくれます。
その際、Stripeが取得したクレジットカード情報を操作する為に実用となるのがこの”トークン”となります。
単発の決済であれば決済する際にクレジットカード情報をStrip経由で取得し、その取得した”トークン”に対して課金処理を行う事で実現します。
一方で定期的な課金を行う場合、取得した”トークン”情報を”顧客(Customer)”に結びつけ、その”顧客(Customer)”に対して課金処理を行う事で実現する事ができます。このように、Stripeでクレジットカード決済を実装する場合、何よりもこの”トークン”の取得と管理について理解する必要があります。
引用元:
https://erorr.org/104/#i
以下のような流れでこちらの処理を実現します。
- Next.jsでクレジットカード情報を入力
- Stripeからtokenを発行
- Firestoreへtokenを格納
上記の流れを図にすると以下のようになります。
クレジットカード登録ページの作成
まずはクレジットカードを登録するフォームがあるページを作成しましょう。
最初にReact上でStripeを使用する際の依存パッケージのインストールを行います。
$ yarn add react-stripe-elements
components/CheckoutForm.js
import React, { Component } from "react"; import { CardElement, injectStripe } from "react-stripe-elements"; class CheckoutForm extends Component { constructor(props) { super(props); this.submit = this.submit.bind(this); } async submit(ev) { const { token, error } = await this.props.stripe.createToken(); this._element.clear(); console.log(token); } render() { return ( <div className="checkout"> <p>Would you like to complete the purchase?</p> <CardElement onReady={c => (this._element = c)} /> <button onClick={this.submit}>Purchase</button> </div> ); } } export default injectStripe(CheckoutForm);
こちらは、Stripeのクレジットカード入力フォームコンポーネントです。
exportの際に「injectStripe」というHOCの引数にCheckoutFormコンポーネントを入れています。
このinjectStripeは公式によると…
injectStripe高次コンポーネント(HOC)を使用して、エレメントツリーに支払いフォームコンポーネントを構築します。
injectStripe HOCは、Elementsグループを管理するthis.props.stripeプロパティを提供します。
挿入されたコンポーネント内で、次のいずれかを呼び出すことができます。
- this.props.stripe.createPaymentMethod
- this.props.stripe.createToken
- this.props.stripe.createSource
- this.props.stripe.handleCardPayment
- this.props.stripe.handleCardSetup
参考:
GitHub - stripe/react-stripe-elements: React components for Stripe.js and Stripe Elements
フォームに入力したクレジットカードのデータをpropsに格納し、指定された5つの情報をpropsから取り出すことができるとのことです。
そして以下の通り、injectStripeによってcreateTokenの情報をpropsから取り出していますね。
async submit(ev) { const { token, error } = await this.props.stripe.createToken(); this._element.clear(); console.log(token); }
pages/purchase.js
import React, { Component } from "react"; import Head from "next/head"; import { Elements, StripeProvider } from "react-stripe-elements"; import CheckoutForm from "../components/CheckoutForm"; export default class Purchase extends Component { constructor(props) { super(props); this.state = { stripe: null }; } componentDidMount() { // Create Stripe instance in componentDidMount // (componentDidMount only fires in browser/DOM environment) this.setState({ stripe: window.Stripe(process.env.stripeKey) }); } render() { return ( <StripeProvider stripe={this.state.stripe}> <div className="example"> <Head> <script src="https://js.stripe.com/v3/" /> </Head> <h1>React Stripe Elements Example</h1> <Elements> <CheckoutForm /> </Elements> </div> </StripeProvider> ); } }
componentDidMount() { // Create Stripe instance in componentDidMount // (componentDidMount only fires in browser/DOM environment) this.setState({ stripe: window.Stripe(process.env.stripeKey) }); }
stateのstripeにdotenvモジュールで環境変数のstripeKeyを呼び出し、設定しています。
現状の確認
ここまでできたら、
http://localhost:3000/purchase
にアクセスしてみましょう。
そうすると、クレジットカード番号・使用期限・CVCの入力フォームのあるページが表示されます。
tokenをユーザに紐付けて保存
現在、tokenはユーザに紐付いていません。
しかし、実際にはサインイン済みのユーザがクレジットカードの登録を行うため、サインインしたユーザとtokenが紐付いている状態が適切です。
そこでwithAuth(サインインユーザのみアクセスを認証するHOC)のstateに、サインインしたユーザの情報を保有させ、tokenを作成した際にユーザと紐付けを行いましょう。
ちなみにwithAuthというHOCについての解説は以下の記事に詳しく書かれています。
nekorokkekun.hatenablog.com
具体的には以下のようなコードになります。
components/helpers/withAuth.js
import React from "react"; import router from "next/router"; import { auth } from "../../lib/firebase"; const withAuth = Component => { return class extends React.Component { constructor(props) { super(props); this.state = { status: "LOADING", user: {} }; } componentDidMount() { auth.onAuthStateChanged(authUser => { if (authUser) { this.setState( { status: "SIGNED_IN", user: authUser }); } else { router.push("/"); } }); } renderContent() { const { status, user } = this.state; if (status == "LOADING") { return <h1>Loading ......</h1>; } else if (status == "SIGNED_IN") { return <Component {...this.props} currentUser={user} />; } } render() { return <>{this.renderContent()}</>; } }; }; export default withAuth;
こちらも重要なポイントの解説を行います。
if (authUser) { this.setState( { status: "SIGNED_IN", user: authUser });
こちらで、認証したユーザのみ、認証ユーザの情報をstateへセットしています。
return <Component {...this.props} currentUser={user} />;
stateにセットした認証ユーザの情報をpropsに渡し、別のコンポーネントで使用できるようにしましょう。
CheckoutForm.jsにwithAuth.jsを追記します。
import React, { Component } from "react"; import { CardElement, injectStripe } from "react-stripe-elements"; import withAuth from "../components/helpers/withAuth"; import { firestore } from "../lib/firebase"; class CheckoutForm extends Component { constructor(props) { super(props); this.submit = this.submit.bind(this); } async submit(ev) { const { token, error } = await this.props.stripe.createToken(); firestore .collection("stripe_customers") .doc(this.props.currentUser.uid) .collection("tokens") .add({ token: token.id }) .then(() => { this._element.clear(); }); } render() { return ( <div className="checkout"> <p>Would you like to complete the purchase?</p> <CardElement onReady={c => (this._element = c)} /> <button onClick={this.submit}>Purchase</button> </div> ); } } export default withAuth(injectStripe(CheckoutForm));
tokenがサインインユーザに紐付いているか確認
http://localhost:3000 へアクセスし、クレジットカード情報を入力してサブミットしてみましょう。
ダミーデータとして
カード番号: 4242424242424242
期限:242
CVC:なんでも
郵便番号:なんでも
でテストができます。
サブミット後、Firestoreのstripe_customers > ユーザーID > tokens の中にランダムな英数字が入っていれば成功です。
token作成をトリガーにしたStripe sourceの作成
Stripeでtokenを作成した際にsourceというものを同時に作成します。
このsourceについては以下のサイトで詳しく解説がなされているため、引用します。
sourceは買い手が決済するために使う「モノ」を抽象化した概念であると言えます。その具象としては、クレジットカードやACH(銀行振込のようなもの)があります。1回きりの決済としてsourceに対して直接課金することもできますし、買い手に対して複数回課金するために買い手にsourceを関連付けておくこともできます。
こちらでは以下の処理を実装していきます。
- tokenをFIrestoreに格納
- Firebase functionsがtoken作成をトリガーにしてStripeへsourceの作成依頼を送信
- Stripeが依頼を受けてsourceを作成
- 作成されたsourceがFirestoreに格納
上記を図にした場合、以下のようなものになります。
functionsに追記
上記の処理は全てFirebase functionsで行うため、実装自体もfucntions/index.js内に書き込みましょう。
functions/index.js
// すでに実装したものは省略 exports.addPaymentSource = functions.firestore .document("/stripe_customers/{userId}/tokens/{pushId}") .onCreate(async (snap, context) => { const source = snap.data(); const token = source.token; if (source === null) { return null; } try { const snapshot = await admin .firestore() .collection("stripe_customers") .doc(context.params.userId) .get(); const customer = snapshot.data().customer_id; const response = await stripe.customers.createSource(customer, { source: token }); return admin .firestore() .collection("stripe_customers") .doc(context.params.userId) .collection("sources") .doc(response.fingerprint) .set(response, { merge: true }); } catch (error) { await snap.ref.set({ error: userFacingMessage(error) }, { merge: true }); console.error(error); console.log(`user: ${context.params.userId}`); } }); function userFacingMessage(error) { return error.type ? error.message : "An error occurred, developers have been alerted"; }
Firebase functionsにデプロイ
以下のコマンドでFirebase functionsにデプロイ しましょう。
$ cd functions $ yarn deploy
以下のように表示されていればデプロイ 成功です。
✔ functions[createStripeCustomer(us-central1)]: Successful update operation. ✔ functions[addPaymentSource(us-central1)]: Successful create operation. ✔ Deploy complete!
現状の確認
一度、Firebase Authenticationのユーザを削除した後にもう一度ユーザ認証を行い、クレジットカード情報の登録をしてみましょう。
FIrestoreのstripe_customers > ユーザID > sourcesに英数字が登録されていれば成功です。
これでStripeとFirebaseにユーザー&クレカ登録同期処理ハンズオンは完了しました。お疲れ様でした!
【Next.js】Head内に要素を組み込む(外部スクリプトとか)
Next.jsでHead内にCSSや外部script、titleタグなどを組み込みたかったので調べてみました。
結論としては、
import Head from 'next/head'
をインポートし、
render関数内などに、
<Head>
<script src="https://js.stripe.com/v2/"></script>
</Head>
こんな感じでHeadタグを作って、組み込みたい要素を入れてあげるだけでOKです!
【Firebase Functions】functions@: The engine "node" is incompatible with this module. Expected version "8". Got "10.16.3"エラーが吐き出される
Next.js内でFirebase Functionsを使用するためにyarn serveを行うと以下のようなエラーが。
error functions@: The engine "node" is incompatible with this module. Expected version "8". Got "10.16.3" error Commands cannot run with an incompatible environment.
Firestoreが求めているnodeのバージョンが8なのですが、
実際に使用されているローカルのnodeのバージョンが10.16.3ですよ、というエラーでした。
nodeを8にダウングレードすると他のプロジェクトに影響が出るかもしれないということでanyenv、nodenvを入れてみたのですが正しく作動しなかったため以下のような対策をしました。
functions/package.json
"engines": { "node": "8" },
上記、プロジェクト直下のfunctions/package.jsonを以下のように改変
"engines": { "node": "10" },
これでnode10で動くようになりました。
【Laravel5.8】ログアウトのredirect先をカスタマイズしたい
Laravel5.8のmake:authで作成したログアウト機能で、redirect先をカスタマイズしたい際の方法です。
/magotaku/app/Http/Controllers/Auth/LoginController.php
<?php namespace App\Http\Controllers\Auth; use App\Http\Controllers\Controller; use Illuminate\Foundation\Auth\AuthenticatesUsers; use Illuminate\Http\Request; // 追加 class LoginController extends Controller { /* |-------------------------------------------------------------------------- | Login Controller |-------------------------------------------------------------------------- | | This controller handles authenticating users for the application and | redirecting them to your home screen. The controller uses a trait | to conveniently provide its functionality to your applications. | */ // 書き換え use AuthenticatesUsers { logout as performLogout; } /** * Where to redirect users after login. * * @var string */ protected $redirectTo = '/home'; /** * Create a new controller instance. * * @return void */ public function __construct() { $this->middleware('guest')->except('logout'); } // logoutファンクションを作成 public function logout(Request $request) { $this->performLogout($request); // redirect先は人それぞれカスタマイズ return redirect('/home'); } }
以上です。
参考
qiita.com
【Next.js】FirebaseのAuthenticationで登録したユーザー情報を描画する
前回の記事では、Next.jsにFirebaseのAuthenticationを使ってユーザー登録機能を追加しました。
nekorokkekun.hatenablog.com
そこで「登録したユーザーの情報を動的に描画するマイページ」を作りましょう。
前提
先ほどもご紹介した前回の記事でログイン機能とHOCの実装まではコピペでも構わないので済ませておいてください。
nekorokkekun.hatenablog.com
ユーザー情報の取得
とは言っても書き方はとても簡単です。
pages/dashboard.js
import React from 'react'; import withAuth from "../src/helpers/withAuth"; // firebaseをimport import { firebase, auth } from "../src/firebase"; class Dashboard extends React.Component { render() { // firebase.auth().currentUserでログインユーザーの情報が取得可能 const user = firebase.auth().currentUser; return( <div> <h2>アカウント情報</h2> <h4>アカウント名</h4> {user.displayName} <h4>メールアドレス</h4> <p> {user.email} </p> <img src={user.photoURL} /> </div> ) } } export default withAuth(Dashboard);
これで以下のような画面が表示されたはずです。
firebase.auth().currentUserの中身
firebase.auth().currentUserでログインユーザーの情報を取得しています。
中身はオブジェクト型になっており、先ほどのコードに登場した「displayName」や「email」「photoURL」なども全て登録したユーザーの情報から引き出されています。
他にも色々な情報が入っていますので、必要に応じて取得すると良いでしょう。
【Next.js】Firestoreから任意のデータを取得して描画する
今回はNext.jsでFirebaseから任意のデータを取得して描画する方法について解説をしていきます。
ウェブアプリを作成する際、レイアウトは決めておいてコンテンツの名前や説明文などはデータを取得してレイアウトにはめ込みたい、という場合がありますね。
このように動的なページを作成するための方法を以下で紹介します。
前提
Firebase Cloud Firestore保存したデータを取得することになります。
すでにFirebaseでプロジェクトが作成されており、Firestoreにデータがいくつか登録されているという前提で進みます。
まだFirebaseのプロジェクト作成やデータの登録ができていないという場合は以下の記事を読み、
- Firebaseでプロジェクトの作成
- データの登録
- Next.jsプロジェクトの作成
- FirebaseとNext.jsプロジェクトのDB接続
を済ませておいてください。
nekorokkekun.hatenablog.com
nekorokkekun.hatenablog.com
また上記ではわざわざデータ登録するためにUIを作成していますが、プロジェクトを作成後、サイドメニューからDatabaseを選ぶことでブラウザからコレクション・ドキュメントの登録(=データの登録)を行うことができます!
Firestoreの確認
まずはFirestoreにデータが保存できるかどうかの確認をしましょう。
ブラウザ上でFirebaseのコンソールからプロジェクト>サイドメニューのDatabaseで確認できます。
実際に見ると以下のようにデータが登録されていることがわかります。
FirestoreはNoSQLなのでMySQLなどのようにテーブル、カラム、レコードといったものはないのですが、イメージとしては
- コレクション = テーブル
- ドキュメント = レコード
- データ = カラムと実際のデータ
と考えるとしっくり方も多いかもしれません。
Firestoreでは以下のようなイラストで図解を出してくれています。
データの一覧表示
まずは先程確認したデータを一覧表示できるページを作成しましょう。
pages/index.js
import { db } from '../lib/db'; import React from 'react'; import Link from 'next/link'; export default class Index extends React.Component { static async getInitialProps() { let result = await db.collection('fanPages') .get() .then(snapshot => { let data = [] snapshot.forEach((doc) => { data.push( Object.assign({ id: doc.id }, doc.data()) ) }) return data }).catch(error => { return [] }) return {datas: result} } render() { const firestoreDatas = this.props.datas return ( <div> <h3>Firestoreのデータ一覧</h3> <div> <ul> {firestoreDatas.map(fanPage => <li key={fanPage.id}> <Link href="/p/[detailid]" as={`/p/${fanPage.id}`}> <a>{fanPage.artistName}</a> </Link> </li> )} </ul> </div> </div> ); } }
これで以下のような画面が表示されたのではないでしょうか。
長いのでいくつかに分けて解説をします。
getInitialPropsでFirestoreからデータの取得
static async getInitialProps() { let result = await db.collection('fanPages') .get() .then(snapshot => { let data = [] snapshot.forEach((doc) => { data.push( Object.assign({ id: doc.id }, doc.data()) ) }) return data }).catch(error => { return [] }) return {datas: result} }
getInitialPropsはクラスコンポーネントで使用できるメソッドです。
コンポーネント生成時にpropsを自動取得してくれるという便利なメソッドなので覚えておきたいですね。
肝心の中身も見ていきましょう。
Firestoreからデータ取得するコレクションを指定
static async getInitialProps() { let result = await db.collection('fanPages') .get() .then(
まず「db.collection('fanPages')」でコレクション「fanPages」からデータ取得を行うことを宣言します。
「db」は変数で、Firestoreの接続設定を他ファイルから取得済みです。詳しくはこちらをご覧ください。
また上記のコードでは非同期処理も行なっています。
「async」と「await」が書かれている部分ですね。
重要なのは「await」を宣言する場所で、意味としては
「result以降の処理が終わるまで結果をreturnしないでね」
ということになります。
Firestoreからのデータ取得は時間が掛かりますので、awaitを配置しておかなければ、データ取得前に「result」がreturnされてしまうため、結果「空っぽのresult」しか返ってこないということになります。
Firestoreのデータを取得して配列に入れる
snapshot => { let data = [] snapshot.forEach((doc) => { data.push( Object.assign({ id: doc.id }, doc.data()) ) }) return data
snapshotとは?
snapshot => {
こんな1行がありますが、「snapshot」とはFirestoreのその「瞬間」のデータが格納されているものです。
データ取得の際に写真を取るイメージで、その瞬間のデータをsnapshotと名付けているんですね。
データをドキュメントごとに配列に収める
data.push( Object.assign({ id: doc.id }, doc.data())
snapshotの1行上で「let data = []」と、空の配列を用意しました。
その後、Object.assignでdataの中にオブジェクト形式でFirestoreのデータをドキュメントごとに格納しています。
実際、この状態のdataをconsole.logで見てみると以下のように出力されています。
[ { id: 'JNTwv1d5W0g1yXQr667j', artistName: 'test1', body: 'test1', category: 'singer', monthlyFee: '1000', pageName: 'test1' }, { id: 'aW749fTXcm99meV7x2WB', artistName: 'test2', body: 'test2', category: 'calligrapher', monthlyFee: '2000', pageName: 'test2' } ]
一番外側に来ているがdata = で定義した配列ですね。
そして、その中にドキュメントごとにオブジェクト型に整形されたFirestoreのデータが格納されていることがわかります。
ここで配列にわざわざ格納するのは、後ほどmap関数で展開するためです。
そして最後に「return data」で中身の入ったdataが「let result」で定義した「result」の中に入りました。
エラーの場合は空の配列を返す
}).catch(error => { return [] })
もしFirestoreからのデータ取得の際にエラーが発生した場合には、「return []」で空の配列をresultに格納する設定にしています。
resultを返してpropsとして扱えるようにする
return {datas: result}
先程までの処理でデータがdata配列に入り、その配列がresultという変数に格納されました。(エラーの場合は空のdata配列がresult変数に格納されています。)
そのresultをpropsである「datas」に渡します。(名前は任意です)
こうすることでコンポーネント内でpropsとして格納したFirebaseのデータを扱うことができるようになるというわけですね。
render関数で取得したデータを描画
render() { const firestoreDatas = this.props.datas return ( <div> <h3>Firestoreのデータ一覧</h3> <div> <ul> {firestoreDatas.map(fanPage => <li key={fanPage.id}> <Link href="/p/[detailid]" as={`/p/${fanPage.id}`}> <a>{fanPage.artistName}</a> </Link> </li> )} </ul> </div> </div> ); } }
次にrender関数で取得したデータを描画しています。こちらも細かく分けてみていきますね。
propsを定数に格納
const datas = this.props.datas
先程getInitialPropsで返されたpropsは「this.props.datas」という呼び方で参照する(props値を使用する)ことができます。
しかし名称として長いため、「datas」という定数に置き換えています。
map関数でpropsを展開
{datas.map(fanPage => // 省略 )}
map関数は配列の中身を展開することができるメソッドです。
先程dataをconsole.logで確認した際を思い出してください。
[ { id: 'JNTwv1d5W0g1yXQr667j', artistName: 'test1', body: 'test1', category: 'singer', monthlyFee: '1000', pageName: 'test1' }, { id: 'aW749fTXcm99meV7x2WB', artistName: 'test2', body: 'test2', category: 'calligrapher', monthlyFee: '2000', pageName: 'test2' } ]
このようになっていました。この中身をオブジェクトごとに1つずつ展開することができるのです。
今はオブジェクト型データが2つだけしか入っていないためあまりメリットは感じられませんが、大量のデータを処理する際にはとても便利な関数です。
そして「datas.map(fanPage」と書くことによって、datasの中のオブジェクトを1つずつ「fanPage」という変数に格納します。
foreachでいうところの「foreach 配列 as 変数」という書き方に似ていますね。
mapで展開されたデータから値を参照
<li key={fanPage.id}> <Link href="/p/[detailid]" as={`/p/${fanPage.id}`}> <a>{fanPage.artistName}</a> </Link> </li>
上記のような書き方でリストレンダリングすることができます。
{ id: 'JNTwv1d5W0g1yXQr667j', artistName: 'test1', body: 'test1', category: 'singer', monthlyFee: '1000', pageName: 'test1' },
上記のようにオブジェクトにデータが入っているため、「fanPage.id」「fanPage.body」などといった書き方で個別にデータを参照することができるのです。
Linkの書き方について
<Link href="/p/[detailid]" as={`/p/${fanPage.id}`}>
Linkの書き方にも注目しましょう。
hrefでは実際にpages/p/[detailid].jsというファイルにアクセスするということが明示されています。しかし実際にユーザーがURLで確認できるのは「/p/JNTwv1d5W0g1yXQr667j」などといった独自URLです。
このような書き方をすることによって動的なページを作成することができるようになっています。
動的なページの作成
次にURLごとにページの中身が切り替わる動的なページを作成していきます。
pages/p/[detail].jsを作成しましょう。
import { db } from '../../lib/db'; import React from 'react'; export default class Detail extends React.Component { static async getInitialProps({query}) { let result = await db.collection("fanPages") .doc(query.detail) .get() .then(function(doc) { if (doc.exists) { return doc.data(); } else { console.log('not exists'); } }).catch(error => { console.log(error) return [] }) return {detail: result} } render() { const detail = this.props.detail; return ( <React.Fragment> <div> <h1>{detail.artistName}</h1> <p> {detail.body} </p> <ul> <li>{detail.category}</li> <li>{detail.monthlyFee}</li> <li>{detail.pageName}</li> </ul> </div> </React.Fragment> ); } }
これで以下のような画面が出てきたのではないでしょうか。
こちらも分けて解説をしていきます。
getInitialPropsでURLクエリパラメータから得たIDを元にデータを取得
まずはURLクエリパラメータから渡ってきたデータIDを元に、Firestoreから任意のデータを取得します。
先程扱ったgetInitialPropsと異なる点が何点かありますね。
getInitialPropsには引数がある
実はgetInitialPropsには動的なページ作成に必要な情報を引数として自動的に生成してくれる機能があります。
static async getInitialProps({query}) {
上記の例では「query」という引数を受け取っています。
このqueryをconsole.logで見てみると…
{ detail: 'JNTwv1d5W0g1yXQr667j' }
と出力されました。
URLクエリパラメータでも使用した一意のデータIDですね。
このデータIDだけにアクセスしたければ「query.detail」と書いてあげればOKですね。
他にもgetInitialPropsには様々な引数があります。
Next.jsのドキュメントには以下のような記述がありました。
nitialProps receives a context object with the following properties:
pathname - path section of URL
query - query string section of URL parsed as an object
asPath - String of the actual path (including the query) shows in the browser
req - HTTP request object (server only)
res - HTTP response object (server only)
err - Error object if any error is encountered during the rendering
Documentation - Getting Started | Next.js
console.logで中を見てみるとかなり膨大な量のデータを引数で受け取れるということがわかりますので、こちらも検証してみてくださいね。
ドキュメントを指定したデータの取得
db.collection("fanPages") .doc(query.detail) .get() .then(function(doc) { if (doc.exists) { return doc.data(); } else { console.log('not exists'); }
先程はコレクションのみの指定でしたが、さらにデータIDを引数にしてドキュメントを指定することができます。
MySQLで言う所の
Select * from fanPages where id = 1;
の「where」以降の条件付けとでも言えばいいでしょうか。
whereがなければテーブル(コレクション)全てのデータを取得することとなり、それは先程行った全件取得と同じですね。
そしてドキュメントの指定をする場合には「.doc(query.detail)」といった書き方が必要です。引数の「query.detail」には先程説明した通り、一意のデータIDが入っています。
後は先ほど説明したgetInitialPropsとほとんど変わりませんので割愛します。
render関数で描画
const detail = this.props.detail; return ( <React.Fragment> <div> <h1>{detail.artistName}</h1> <p> {detail.body} </p> <ul> <li>{detail.category}</li> <li>{detail.monthlyFee}</li> <li>{detail.pageName}</li> </ul> </div>
propsで受けとったデータは再度、定数に格納して「detail.artistName」といった形で参照可能です。
これでFirestoreから任意のデータを取得して描画することができました。お疲れ様でした!
【Next.js】Firebase AuthenticationとHOCを使用してサインイン機能を実装する
本記事では、Next.jsプロジェクトにFirebase Authenticationを組み込み、サインイン機能を実装するとともに、HOC(高階コンポーネント)を使用してサインインユーザーのみが閲覧できるページを作成していきます。
- Next.jsプロジェクトの準備
- サインインユーザーのみ見られるコンポーネントの作成
- 必要となるパッケージやDB接続ファイルの準備
- FirebaseでAuthentication設定
- index.jsの書き換え
- HOC(高階コンポーネント)の作成
- dashboard.jsへ追記
- index.jsに追記
- サインインをしてみる
Next.jsプロジェクトの準備
まずはNext.jsプロジェクトの準備をしていきましょう。
npx create-next-app next-firebase-auth cd next-firebase-auth yarn devhttp://localhost:3000/
http://localhost:3000/にアクセスしてみると以下のような画面が出てくるはずです。
サインインユーザーのみ見られるコンポーネントの作成
次にサインインユーザーのみが見ることのできるコンポーネントを作成していきましょう。
cd pages touch dashboard.js
pages/dashboard.js
import React from 'react'; import Nav from '../components/nav'; class Dashboard extends React.Component { render() { return ( <div> <Nav /> <h1>Dashboard Page</h1> <p>You can't go into this page if you are not authenticated.</p> </div> ) } } export default Dashboard;
まだHOCに設定していないためアクセス可能です。
必要となるパッケージやDB接続ファイルの準備
次にFirebaseの接続に必要となるパッケージやDB接続用ファイルの準備をしていきます。
まずはルートディレクトリに.envファイルを作成します。
touch .env
ここにFirebaseでプロジェクトを作成した際に発行された
たちを定数として収めていきます。
Firebaseにおけるプロジェクトの作成から上記のキーの発行手順については以下の記事に書かれています。
nekorokkekun.hatenablog.com
取得したキーたちを以下のように.envに書き込んでいきましょう。
.env
FIREBASE_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx FIREBASE_AUTH_DOMAIN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx FIREBASE_DATABASE_URL=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx FIREBASE_PROJECT_ID=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx FIREBASE_STORAGE_BUCKET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx FIREBASE_MESSAGING_SENDER_ID=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx FIREBASE_APP_ID=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
次にnext.config.jsをルートディレクトリ直下に作成します。
touch next.config.js
next.config.js
require("dotenv").config(); const path = require("path"); const Dotenv = require("dotenv-webpack"); module.exports = { webpack: config => { config.plugins = config.plugins || []; config.plugins = [ ...config.plugins, // Read the .env file new Dotenv({ path: path.join(__dirname, ".env"), systemvars: true }) ]; return config; } };
最後にFirebaseへ接続するためのファイルを作成しましょう。
// ルートディレクトリから mkdir src cd src mkdir firebase cd firebase touch index.js
src/firebase/index.js
import firebase from "firebase/app"; import "firebase/auth"; const config = { apiKey: process.env.FIREBASE_API_KEY, authDomain: process.env.FIREBASE_AUTH_DOMAIN, databaseURL: process.env.FIREBASE_DATABASE_URL, projectId: process.env.FIREBASE_PROJECT_ID, storageBucket: process.env.FIREBASE_STORAGE_BUCKET, messagingSenderId: process.env.FIREBASE_MESSAGING_SENDER_ID, appId: process.env.FIREBASE_APP_ID }; if (!firebase.apps.length) { firebase.initializeApp(config); } const auth = firebase.auth(); export { auth, firebase };
これでprocess.env.〇〇(定数名)という形でプロジェクト内の他ファイルから.envで設定した定数名を使用するための下準備ができました!
そして以下の2つのパッケージをインストールすることで.envで設定した定数がprocess.env.〇〇(定数名)といった形でプロジェクト内の他のファイルからアクセス可能となります。
yarn add firebase dotenv yarn add dotenv-webpack
FirebaseでAuthentication設定
次にFirebaseのコンソール上からAuthenticationの設定をしておきましょう。
先ほど作成したFirebaseのプロジェクトにアクセスします。
そしてサイドメニューの「Authentication」をクリックしましょう。
画像の順番に従い、
① タブメニューの「ログイン方法」をクリック
② 「有効にする」をオン
③ プロジェクトのサポートメールを設定
という順序で設定を完了させましょう。
index.jsの書き換え
独自のUIにしていますが、ポイントはfirebaseからauthをimportしているというところです。
import React from "react"; import Link from "next/link"; import Nav from "../components/nav"; import { auth, firebase } from "../src/firebase"; import Head from "next/head" class Home extends React.Component { render() { return ( <div> <Head title="Home" /> <Nav /> <div className="hero"> <h1 className="title"> Welcome to Firebase Authentication in Next.js! </h1> <p className="description"> Click on the Dashboard link to visit the dashboard page. </p> <div className="row"> <Link href="/dashboard"> <a className="card"> <h3>Go to Dashboard→</h3> <p>Visit Dashboard</p> </a> </Link> </div> </div> <style jsx>{` .hero { width: 100%; color: #333; } .title { margin: 0; width: 100%; padding-top: 80px; line-height: 1.15; font-size: 48px; } .title, .description { text-align: center; } .row { max-width: 880px; margin: 80px auto 40px; display: flex; flex-direction: row; justify-content: space-around; } .card { padding: 18px 18px 24px; width: 220px; text-align: left; text-decoration: none; color: #434343; border: 1px solid #9b9b9b; } .card:hover { border-color: #067df7; } .card h3 { margin: 0; color: #067df7; font-size: 18px; } .card p { margin: 0; padding: 12px 0 0; font-size: 13px; color: #333; } `}</style> </div> ); } } export default Home;
HOC(高階コンポーネント)の作成
いよいよHOC(高階コンポーネント)を作成します。
高階コンポーネントについて今回は詳しく解説しませんが、「通常のコンポーネントに機能を追加するもの」という認識でOKです。
今回のHOCで追加する機能が「サインイン後に見ることができる」というものですね。
// src直下で mkdir helpers cd helpers touch withAuth.js
src/helpers/withAuth.js
import React from "react"; import router from "next/router"; import { auth } from "../firebase"; const withAuth = Component => { return class extends React.Component { constructor(props) { super(props); this.state = { status: "LOADING" }; } componentDidMount() { auth.onAuthStateChanged(authUser => { if (authUser) { this.setState({ status: "SIGNED_IN" }); } else { router.push("/"); } }); } renderContent() { const { status } = this.state; if (status == "LOADING") { return <h1>Loading ......</h1>; } else if (status == "SIGNED_IN") { return <Component {...this.props} />; } } render() { return <>{this.renderContent()}</>; } }; }; export default withAuth;
こちらは細かく解説していきましょう。
引数でComponentを受ける
const withAuth = Component => {
後ほど「サインイン後しか見られないコンポーネント」を設定する際に
export default withAuth(コンポーネント名)
という書き方でexportします。
この「コンポーネント名」で指定されたコンポーネントが、withAuthの引数に設定された「Component」を指しています。
ユーザー認証されるまではstateで読み込みが「Loading」
constructor(props) { super(props); this.state = { status: "LOADING" }; }
stateの状態は基本的に「LOADING」(=見認証の状態)にしておき、
ユーザー認証されて初めてstateが「SIGNED IN」の状態となります。
authUserで認証の可否を判別
componentDidMount() { auth.onAuthStateChanged(authUser => { if (authUser) { this.setState({ status: "SIGNED_IN" }); } else { router.push("/"); } }); }
サインインが成功した場合、「authUser」インスタンスの中には、ユーザー情報が入ることになります。そうするとif文でstateのstatusが「SIGNED_ID」になり、コンテンツを見ることができるようになります。
uid(=ユーザーID)やメールアドレスなどが入っているため、サインイン後にも利用できそうですね!
もしユーザーが未認証の場合にはauthUserは空のままのため、elseの方に行き、そのままトップページに遷移することとなります。
statusがLOADINGのままだと、LOADINGという画面が表示されることとなります。それが以下のものですね。
renderContent() { const { status } = this.state; if (status == "LOADING") { return <h1>Loading ......</h1>; } else if (status == "SIGNED_IN") { return <Component {...this.props} />; } }
そしてstatusがSIGNED_INだと、HOCでサインインユーザーのみが見られるComponentを表示できるようになっています。
dashboard.jsへ追記
先ほど作成したdashboard.jsにwithAuth.jsを合体させる形で追記しましょう。
pages/dashboard.js
import React from 'react'; import Nav from '../components/nav'; // 追加 import withAuth from "../src/helpers/withAuth"; class Dashboard extends React.Component { render() { return ( <div> <Nav /> <h1>Dashboard Page</h1> <p>You can't go into this page if you are not authenticated.</p> </div> ) } } // withAuthの引数にdashboard.jsを追加 export default withAuth(Dashboard);
これでサインインユーザーのみがdashboard.jsを見られるような設定となりました!
index.jsに追記
次にサインインなどの処理を行うためのコードをpages/index.jsに追記します。
pages/index.js
import React from "react"; import Link from "next/link"; import Nav from "../components/nav"; import { auth, firebase } from "../src/firebase"; import Head from "next/head" class Home extends React.Component { // ここから下を追加 handleSignIn = () => { var provider = new firebase.auth.GoogleAuthProvider(); provider.addScope("https://www.googleapis.com/auth/contacts.readonly"); auth .signInWithPopup(provider) .then(() => { alert("You are signed In"); }) .catch(err => { alert("OOps something went wrong check your console"); console.log(err); }); }; handleSignout = () => { auth .signOut() .then(function() { alert("Logout successful"); }) .catch(function(error) { alert("OOps something went wrong check your console"); console.log(err); }); }; // ここまでを追加 render() { return ( <div> <Head title="Home" /> <Nav /> <div className="hero"> <h1 className="title"> Welcome to Firebase Authentication in Next.js! </h1> <p className="description"> Click on the Dashboard link to visit the dashboard page. </p> <div className="row"> <Link href="/dashboard"> <a className="card"> <h3>Go to Dashboard→</h3> <p>Visit Dashboard</p> </a> </Link> {/* ここから下2行を追加 */} <button onClick={this.handleSignIn}>Sign In using google</button> <button onClick={this.handleSignout}>Signout</button> </div> </div> <style jsx>{` .hero { width: 100%; color: #333; } .title { margin: 0; width: 100%; padding-top: 80px; line-height: 1.15; font-size: 48px; } .title, .description { text-align: center; } .row { max-width: 880px; margin: 80px auto 40px; display: flex; flex-direction: row; justify-content: space-around; } .card { padding: 18px 18px 24px; width: 220px; text-align: left; text-decoration: none; color: #434343; border: 1px solid #9b9b9b; } .card:hover { border-color: #067df7; } .card h3 { margin: 0; color: #067df7; font-size: 18px; } .card p { margin: 0; padding: 12px 0 0; font-size: 13px; color: #333; } `}</style> </div> ); } } export default Home;
こちらも少し解説をします。
以下はサインイン・サインアウトの処理を行うイベントの設定です。
handleSignInでは、ボタンを押すとサインインモーダルウィンドウがポップアップし、サインイン処理ができるという流れになります。
こちらは私たちが作り込む必要はありません。
handleSignIn = () => { var provider = new firebase.auth.GoogleAuthProvider(); provider.addScope("https://www.googleapis.com/auth/contacts.readonly"); auth .signInWithPopup(provider) .then(() => { alert("You are signed In"); }) .catch(err => { alert("OOps something went wrong check your console"); console.log(err); }); };
handleSignoutも同じくで、Signout()メソッドを使えば簡単にユーザーを未承認の状態に戻してくれます。
handleSignout = () => { auth .signOut() .then(function() { alert("Logout successful"); }) .catch(function(error) { alert("OOps something went wrong check your console"); console.log(err); }); };
そして、上記2つのイベントを呼び出すのが以下のbuttonですね。
<button onClick={this.handleSignIn}>Sign In using google</button> <button onClick={this.handleSignout}>Signout</button>