React+Redux アプリに Google Sign-In を組み込んでみた

React+Redux アプリに Google Sign-In の機能を組み込んでみました。

はじめに

Google Sign-In とは、Googleが提供している認証系ツールの総称である Google Identity Platform のツールの1つで、スマホアプリやWebアプリの認証をGoogleアカウントでできるようにするものです。

参考:

itexplorer.hateblo.jp

React, Redux, Google Sign-In という組み合わせだと、さすがに情報が少なく、結構試行錯誤しましたが、最終的にはなんとか方法が見つかりました。

準備

まずは、Google API Console でクレデンシャルを登録する必要があります。

以下のページで説明されています:

Creating a Google API Console project and client ID  |  Google Sign-In for Websites  |  Google Developers

Google Platform Library の組み込み

一般的なWebクライアントでの設定方法は以下のページで説明されています:

Integrating Google Sign-In into your web app  |  Google Sign-In for Websites  |  Google Developers

ですが、このやり方だと React+Redux の環境で使いづらいことがわかったので、Google Platform Library だけを組み込みます:

<script src="https://apis.google.com/js/platform.js" async defer></script>

React Component の中に入れるとタイミングが遅れるようなので、僕は index.html に入れました。

追記(2017-07-24)

async defer を指定すると、タイミングが遅れることがあるようです。

なので、僕は外すことにしました。

追記終了

上のGoogleの手順では、この後、metaタグにクライアントIDを入れ、ボタンを設置することになっていますが、そうするとカスタマイズの余地が少なくなってしまうので、これはやらずに、自分で処理を書くことにしました。

サインインの実装

Reduxの構成では、Component でサインインボタンを設置し、Container の mapDispatchToProps でボタンの onClick ハンドラーを定義し、その中で Google Sign-In の認証処理を行うことになります。

// Container
const GOOGLE_SIGN_IN_PARAMS = {
  client_id: 'xxxxx',
  ux_mode: 'redirect',  // popupはブラウザーでブロックされる
};

const mapDispatchToProps = (dispatch: Dispatch<Action>) => {
  return {
    onSignInClick: () => {
      signIn(onInitFailure, onSignInSuccess, onSignInFailure, onAlreadySignedIn);
    },
  };
};

const signIn = (onInitFailure: IFunc, onSignInSuccess: IFunc, onSignInFailure: IFunc, onAlreadySignedIn: IFunc) => {
  const gapi = (<any>window).gapi;
  gapi.load('auth2', () => {
    gapi.auth2.init(GOOGLE_SIGN_IN_PARAMS).then(
      // 初期化成功
      (gAuth: any) => {
        if (gAuth.isSignedIn.get()) {
          // サインイン済み
          onAlreadySignedIn(gAuth);
        } else {
          // 未サインイン
          gAuth.signIn()
            .then(
              (gUser: any) => onSignInSuccess(gUser),
              (err: any) => onSignInFailure(err),
            );
        }
      },
      // 初期化失敗
      (err: any) => onInitFailure(err),
    );
  });
};

処理内容はすぐにわかると思いますが、ポイントは、load() → init() → isSignedIn.get() → signIn() という順番で行う必要があるということです。

必要に応じて、各ハンドラーでアクションをディスパッチします。

APIやパラメーターについては、以下のページに詳しく書かれています:

Google Sign-In JavaScript client reference  |  Google Sign-In for Websites  |  Google Developers

追記 2017-07-31

GOOGLE_SIGN_IN_PARAMSux_mode についてですが、コメントでは、popupはブラウザーでブロックされると書きましたが、popupにする方法が見つかりました。

ボタンのハンドラーでできるだけすぐに signIn() を実行するようにするとうまくいきました:

const signIn = (onSignInSuccess: IFunc, onSignInFailure: IFunc, onAlreadySignedIn: IFunc) => {
  // 前提:すでに gapi.auth2.init() を実行済み
  const gAuth = (<any>window).gapi.auth2.getAuthInstance();
  if (gAuth.isSignedIn.get()) {
    // サインイン済み
    onAlreadySignedIn(gAuth);
  } else {
    // 未サインイン
    gAuth.signIn()
      .then(
        (gUser: any) => onSignInSuccess(gUser),
        (err: any) => onSignInFailure(err),
      );
  }
};

Ref. Avoiding Popup Blocking when Authenticating with Google

追記終了

サインアウトの実装

サインアウトは、サインインと同様のコードになります:

// Container
const mapDispatchToProps = (dispatch: Dispatch<Action>) => {
  return {
    .....
    onSignOutClick: () => {
      signOut(onInitFailure, onSignOutSuccess, onSignOutFailure);
    },
  };
};

const signOut = (onInitFailure: IFunc, onSignOutSuccess: IFunc, onSignOutFailure: IFunc) => {
  const gapi = (<any>window).gapi;
  gapi.load('auth2', () => {
    gapi.auth2.init(GOOGLE_SIGN_IN_PARAMS).then(
      // 初期化成功
      (gAuth: any) => {
        gAuth.signOut()
          .then(
            () => onSignOutSuccess(),
            (err: any) => onSignOutFailure(),
          );
      },
      // 初期化失敗
      (err: any) => onInitFailure(err),
    );
  });
};

サインインの方と重複している処理があるので、クラス化した方が見通しが良くなると思います。

サインイン済みかどうかをチェックする

サインイン済みかどうかは、サインインの処理で使った gAuth.isSignedIn.get() で取得できますが、どこでそれを実行するかが問題です。

通常、ユーザー認証のあるWebアプリの場合、どのページでも最初に認証チェックを行う必要があるので、Component の componentDidMount() でチェックすることになります。

追記(2017-07-24)

componentDidMount() は render() の後で実行されるため、render() で認証状態を確認したい場合は componentWillMount() でチェックする必要があります。

追記終了

それをどこに入れるかですが、トップレベルのComponent/Container でやるか、認証チェック用の Component を作ってトップレベルの Component に入れることになると思います。

僕はトップレベルの App Component でやることにし、そのために App に Container の構造を追加しました。

// Component
class App extends React.Component<IProps> {
  render() {
    .....
  }

  componentDidMount() {
    this.props.onDidMount();
  }
}
// Container
const mapDispatchToProps = (dispatch: Dispatch<Action>) => {
  return {
    onDidMount: () => {
      checkAuth(dispatch);
    },
  };
};

const checkAuth = (dispatch: Dispatch<Action>) => {
  const gapi = (window as any).gapi;
  gapi.load('auth2', () => {
    gapi.auth2.init(GOOGLE_SIGN_IN_PARAMS).then(
     // 初期化成功
      (gAuth: any) => {
        let isSignedIn = gAuth.isSignedIn.get();
        // dispatch action
      },
      // 初期化失敗
      (err: any) => {
        // dispatch action
      },
    );
  });
};

const AppContainer = connect(
  mapStateToProps,
  mapDispatchToProps
)(App);

export default AppContainer;

あとがき

React+Redux のアプリに Google Sign-In の機能を組み込む方法は以上になります。

もしかすると、Reduxの非同期処理のパターンで、もう少しきれいに書けるかもしれませんが、まだ勉強していないので、現時点ではこのレベルになります。

あと、実は、react-google-login というライブラリーがあり、それでもGoogle Sign-In の機能を使用することができます。

ただし、react-google-login では、Component ですべてを実装しているため、Reduxの構造に合わせるのは難しいです。

また、サインアウトや状態チェックの機能がなかったりします。

ということで、今のところは自分で実装するしかなさそうです。

追記:Reduxの非同期処理での実装(2017-07-31)

その後、Reduxの非同期処理での実装についても調べてみました。

公式サイトの情報によると、Redux Thunk を使用するのが標準的な方法のようですが、Actionに非同期処理のロジックが入るのでちょっと違和感を感じました。

そこで、他の方法を調べた結果、redux-saga が良さそうに思えました。

redux-saga を使うと、Reduxの各要素とは別のものとして実装できるので、見通しが良いです。

Google Sign-In については、上記のコードではコールバックでざっと書きましたが、Promiseを使うとさらに読みやすくなります。

なお、Googleのサインイン処理を redux-saga で実装すると、ポップアップがブロックされてしまいます。

なので、ポップアップにする場合は、そこだけはUI要素のハンドラーで実装する必要があります。

Ref. redux-sagaで非同期処理と戦う - Qiita

(redux-saga については、上の記事が非常にわかりやすかったです。)