🙂

TypeScript+React+Redux+Firebase AuthでLINEミニアプリ (liff)開発

2020/10/05に公開

Zenn初めての投稿です。よろしくお願いします。

モリモリなタイトルとなりましたが、
LINE Mini AppをFirebase Authenticationを用いて、TypeScript+React+Reduxで開発してみました。
↓デモ動画です。

LINEミニアプリ(LINE Mini app)とは

LINE内で使えるWebアプリです。LINEボットと連携して、商品やチケットの購入をさせるなどできることが広がります。
スマホアプリやWebアプリと比べてLINEボットからかん単に使えるため圧倒的UX向上につながります。

LIFFとは

LINE Front-end Frameworkの略です。
公式サイトによると、LINEミニアプリが動作するプラットフォームという位置付けです。

なぜFirebase Authenticationを用いたのか

Firebase AuthenticationはスマホアプリやWebなど様々なクライアントに対して簡単にユーザー認証を導入できます。スマホアプリにも同じユーザー認証を対応させたいのでFirebase Authenticationを使用しました。

開発方法

Firebase Authenticationの導入

src/firebaseAuth.tsでセットアップします。
環境変数から情報を読み込みます。

import * as firebase from 'firebase/app';
import 'firebase/auth';

export const app = firebase.initializeApp({
    apiKey: process.env.REACT_APP_FIREBASE_KEY,
    authDomain: process.env.REACT_APP_FIREBASE_DOMAIN,
    databaseURL: process.env.REACT_APP_FIREBASE_DATABASE,
    projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID,
    storageBucket: process.env.REACT_APP_FIREBASE_STORAGE_BUCKET,
    messagingSenderId: process.env.REACT_APP_FIREBASE_SENDER_ID,
});

Store

RootState(CombinedState)

export interface CombinedState {
    router: RouterState;
    authStore: AuthState;
}

RouterState

今回には特に関係ないが、ルーティングを使うならば必要です。
connected-react-routerで定義されたRouterStateを使いました。

AuthState

firebaseのユーザーとLINEのユーザーの情報を入れておけます。

export interface AuthState {
    user: firebase.User | null;
    lineProfile: LineProfile | null;
}
export interface LineProfile {
    userId: string;
    displayName: string;
    pictureUrl?: string | undefined;
    statusMessage?: string | undefined;
}

Action

typescript-fsaを使って、Actionを作成します。
自動で、actions.signUp.startedactions.signUp.doneactions.signUp.failedが作られます。

import actionCreatorFactory from 'typescript-fsa';
import 'firebase/auth';
import { LineProfile } from './reducers/authStore';

const actionCreator = actionCreatorFactory();

export interface SignUpResult {
    user: firebase.User;
    lineProfile: LineProfile;
}

export const actions = {
    signUp: actionCreator.async<null, SignUpResult>('SIGN_UP'),
};

signUp

Firebase AuthとLINEを連携させます。
LINEと連携するにはサーバー側でCustomTokenを生成する必要があります。
Firebaseのドキュメントのカスタム トークンを作成するを参照ください。
そして、サーバーからCustomTokenを受け取ったら、signInWithCustomToken()でログインできます。
その後、LINEのProfileを取得しています。

export const signUp = () => {
    return (dispatch: Dispatch): void => {
        app.auth().onAuthStateChanged((currentUser) => {
            liff.init({
                liffId: process.env.REACT_APP_LIFF_ID as string,
            })
                .then(() => {
                    if (!liff.isLoggedIn()) {
                        liff.login({});
                    }
                    if (currentUser) {
                        liff.getProfile()
                            .then((profile: LineProfile) => {
                                dispatch(
                                    actions.signUp.done({
                                        params: null,
                                        result: {
                                            user: currentUser,
                                            lineProfile: profile,
                                        },
                                    })
                                );
                            })
                            .catch((err) => console.log(err));
                        return;
                    }
                    const accessToken = liff.getAccessToken();
                    axios
                        .post<FirebaseTokenResponse>(
                            'https://サーバーのホスト/users/verifyToken',
                            {
                                token: accessToken,
                            }
                        )
                        .then((res) => {
                            console.log(res);
                            app.auth()
                                .signInWithCustomToken(res.data.firebase_token)
                                .then((res) => {
                                    liff.getProfile()
                                        .then((profile: LineProfile) => {
                                            dispatch(
                                                actions.signUp.done({
                                                    params: null,
                                                    result: {
                                                        user: res.user as firebase.User,
                                                        lineProfile: profile,
                                                    },
                                                })
                                            );
                                        })
                                        .catch((e) => {
                                            console.log(e);
                                        });
                                })
                                .catch((e) => {
                                    console.log(e);
                                });
                        })
                        .catch((err) => console.log(err));
                })
                .catch((err) => console.log(err));
        });
    };
};

Reducer

RootReducer(CombinedReducer)

import { combineReducers, Reducer } from 'redux';
import authStore from './authStore';
import { connectRouter, RouterState } from 'connected-react-router';
import { createBrowserHistory } from 'history';
import { AuthState } from './authStore';

export const history = createBrowserHistory();

export type CombineReducerMap<S extends unknown> = {
    [K in keyof S]: Reducer<S[K]>;
};

export interface CombinedState {
    router: RouterState;
    authStore: AuthState;
}

const reducerMap: CombineReducerMap<CombinedState> = {
    router: connectRouter(history) as Reducer,
    authStore: authStore,
};

export default combineReducers<CombinedState>(reducerMap);

AuthReducer

typescript-fsa-reducersを用いて、いい感じに reducerを定義します。

const initialState: AuthState = {
    user: null,
    lineProfile: null,
};

const reducer = reducerWithInitialState(initialState)
    .case(actions.signUp.started, (state, _) => {
        return { ...state };
    })
    .case(actions.signUp.done, (state, payload) => {
        return {
            ...state,
            user: payload.result.user,
            lineProfile: payload.result.lineProfile,
        };
    })
    .case(actions.signUp.failed, (state, _) => {
        return {
            ...state,
        };
    });
export default reducer;

View

React Hooks APIを使って、Actionを実行します。

import React, { useEffect } from 'react';
import { connect } from 'react-redux';
import { getSignUpStore } from '../redux/selectors';
import { signUp } from '../redux/actions';
import { AuthState } from '../redux/reducers/authStore';
import { CombinedState } from '../redux/reducers';

const SignUpPage = ({
    signUpStore,
    signUp,
}: {
    signUpStore: AuthState;
    signUp: () => void;
}) => {
    useEffect(() => {
        signUp();
    }, [signUp]);

    return (
        <div>
            {signUpStore.lineProfile && signUpStore.user ? (
                <p>あなたは「{signUpStore.lineProfile.displayName}</p>
            ) : (
                <p>ログイン中</p>
            )}
        </div>
    );
};

const mapStateToProps = (state: CombinedState) => {
    const signUpStore = getSignUpStore(state);
    return { signUpStore };
};

export default connect(mapStateToProps, { signUp })(SignUpPage);

デプロイ

Netlifyにデプロイしました。

LINEで使う方法

LINE DevelopersからLIFFアプリのチャネルを作ります。
詳しくはLINEの公式ドキュメントのLIFFアプリをチャネルに追加するを参照してください。
設定にEndpoint URLをNetlifyのURLにします。
そして、https://liff.line.me/から始まるLIFF URLという文字列が設定されます。それをLINEのチャットに送りそのリンクを押すと、LINE内で使うことができます。

また、LINEボットのリッチメニューを使うとそれっぽくなります。
LINE Official Account Managerからリッチメニューを作成してみましょう。
リッチメニューの作成画面に移ります。
コンテンツ設定のテンプレートの大の右下を選択します。

そして、画像作成ボタンを押して適当に画像を作ります。

アクションはリンクタイプを選び、LIFFのリンクを入力します。

そうすることでLIFFアプリが開けるリッチメニューを作れました。

Discussion