wheatandcatの開発ブログ

React Nativeで開発しているペペロミア & memoirの技術系記事を投稿してます

ExpoでAndroidのstandaloneのみGoogle SignInが失敗する

気づいたら、Expoで実行時にAndroidのstandaloneのみGoogle SignInが失敗しています。

元々Expoの以下の機能を使用してGoogle SignInをしていました。

docs.expo.io

以下ではログインできました。

  • ExpoクライアントでAndroid起動時はログインできる
  • iOSはExpoクライアントでもTestFlightでもログインできる

確か、実装時にAndroidのstandaloneもログインできたよなぁ。。。と思いながら、認証周りの設定を見直したけど直らなかったので、ググったら以下が原因でした。

github.com

結論からいうとExpo SDK33以降は以下のようにする必要があるみたいです。

Expoとstandaloneで動作が変わるのは気持ち悪いですが、 ここは仕方なさそうなので改修しました。

■改修pull request

github.com

最終的なコードはこうなりました。

import * as GoogleSignIn from "expo-google-sign-in";
import * as Google from "expo-google-app-auth";
import { AsyncStorage, Platform } from "react-native";
import React, { createContext, Component } from "react";
import Constants from "expo-constants";
import { Sentry } from "react-native-sentry";
import * as firebase from "firebase";

const Context = createContext({});
const { Provider } = Context;

interface Props {}

interface State {
  uid: string;
  email: string;
}

const isStandaloneAndAndroid = () => {
  return Platform.OS === "android" && Constants.appOwnership !== "expo";
};

export default class extends Component<Props, State> {
  state = {
    email: "",
    uid: ""
  };

  async componentDidMount() {
    if (isStandaloneAndAndroid()) {
      const androidClientId = process.env.GOOGLE_LOGIN_ANDROID_CLIENT_ID;
      try {
        await GoogleSignIn.initAsync({
          clientId: String(androidClientId)
        });
      } catch ({ message }) {
        Sentry.captureMessage(JSON.stringify(message));
      }
    }

    const loggedIn = await this.loggedIn();

    if (loggedIn && !this.state.uid) {
      const uid = await AsyncStorage.getItem("uid");
      if (uid) {
        this.setState({
          uid
        });
      }
    }

    if (loggedIn && !this.state.email) {
      const email = await AsyncStorage.getItem("email");
      if (email) {
        this.setState({
          email
        });
      }
    }
  }

  onGoogleLogin = async () => {
    if (isStandaloneAndAndroid()) {
      // TODO: AndroidのstandaloneのみGoogleSignInを使わないとエラーになる
      // https://github.com/expo/expo/issues/3253

      await GoogleSignIn.askForPlayServicesAsync();
      const result = await GoogleSignIn.signInAsync();

      if (result.type === "success" && result.user && result.user.auth) {
        const { idToken, accessToken } = result.user.auth;
        await this.firebaseLogin(idToken || "", accessToken || "");
      } else {
        Sentry.captureMessage(JSON.stringify(result));
      }
    } else {
      const androidClientId = process.env.GOOGLE_LOGIN_ANDROID_CLIENT_ID;
      const iosClientId = process.env.GOOGLE_LOGIN_IOS_CLIENT_ID;
      const result = await Google.logInAsync({
        clientId:
          Platform.OS === "ios" ? String(iosClientId) : String(androidClientId),
        iosClientId,
        androidClientId,
        scopes: ["profile", "email"]
      });

      if (result.type === "success") {
        const { idToken, accessToken } = result;
        await this.firebaseLogin(idToken || "", accessToken || "");
      } else {
        Sentry.captureMessage(JSON.stringify(result));
      }
    }
  };

  firebaseLogin = async (idToken: string, accessToken: string) => {
    const credential = firebase.auth.GoogleAuthProvider.credential(
      idToken,
      accessToken
    );

    await firebase
      .auth()
      .signInAndRetrieveDataWithCredential(credential)
      .catch(error => {
        Sentry.captureMessage(JSON.stringify(error));
      });

    await this.setSession(true);
  };

  loggedIn = async () => {
    const idToken = await this.getIdToken();

    return Boolean(idToken);
  };

  logout = async () => {
    await firebase.auth().signOut();

    await AsyncStorage.removeItem("id_token");
    await AsyncStorage.removeItem("expiration");
    await AsyncStorage.removeItem("email");
  };

  setSession = async (refresh = false) => {
    const user = firebase.auth().currentUser;
    if (!user) {
      return;
    }

    if (user.email) {
      await AsyncStorage.setItem("email", user.email);
      await AsyncStorage.setItem("uid", user.uid);
      this.setState({
        email: user.email,
        uid: user.uid
      });
    }

    const idToken = await user.getIdToken(refresh);
    await AsyncStorage.setItem("id_token", idToken);
    await AsyncStorage.setItem(
      "expiration",
      String(new Date().getTime() + 60 * 60)
    );

    return idToken;
  };

  getIdToken = async () => {
    const idToken = await AsyncStorage.getItem("id_token");
    if (!idToken) {
      return null;
    }

    const expiration = await AsyncStorage.getItem("expiration");
    if (Number(expiration) > new Date().getTime()) {
      return idToken;
    }

    return this.setSession(true);
  };

  render() {
    return (
      <Provider
        value={{
          onGoogleLogin: this.onGoogleLogin,
          getIdToken: this.getIdToken,
          loggedIn: this.loggedIn,
          logout: this.logout,
          email: this.state.email,
          uid: this.state.uid
        }}
      >
        {this.props.children}
      </Provider>
    );
  }
}

export const Consumer = Context.Consumer;

趣味プロダクトだから良いけど、仕事のアプリでリリース前のタイミングで 起きたら発狂しそう