React Native Tech Blog

supported by maricuru (旧maricuru tech blogです)

React Native + FirebaseのSNSログイン機能の実装(GoogleとFacebook)

どうも、サトウダイキです。 前回の続きです。React Native + Firebaseのログイン実装について記載してきます。

前回はメールアドレスと電話番号の実装をしました。

tech.maricuru.com

今回はFacebookアカウントとGoogleアカウントでのログインについです。

Facebookアカウントでのログイン

FacebookログインはFirebaseの公式サイト通りやっていけば簡単に実装できます。公式サイトも合わせて確認しながら実装してみてください。

Authenticate Using Facebook Login with JavaScript  |  Firebase

Facebook for Developersの登録

まずは、Facebook for Developers サイトに登録して、アプリのアプリ ID とアプリシークレットを取得します。

Facebook for Developers

f:id:daiki-sato:20200207063245p:plain
topページ

f:id:daiki-sato:20200207063405p:plain
アプリの登録

f:id:daiki-sato:20200207063426p:plain
設定

Firebaseの設定

続いて、Fireabaseの設定です。

コンソールに移動して [Authentication] セクションを開きます。
[ログイン方法] タブで [Facebook] を有効にし、Facebook から取得した [アプリ ID] と [app secret] を指定します。

次に、Facebook for Developers サイトの [Product Settings] > [Facebook Login] 構成にある Facebook アプリ設定ページで、OAuth リダイレクト URI(my-app-12345.firebaseapp.com/__/auth/handler など)が [OAuth redirect URIs] のうちの 1 つとしてリストされていることを確認します。

パッケージのインストール

ターミナルを立ち上げ、今回必要なパッケージをインストールします。

yarn add expo-facebook

ロジックの実装

ここからは実装です。ロジック部分から作成していきます。

firebase.tsに追記

export const facebookConfig = { ApplicationKey: '-----' };

export const { FacebookAuthProvider } = firebase.auth;

※ApplicationKey: '-----' のところは自身の値を入力してください。

auth.tsに追記

import firebase, {
  facebookConfig,
  FacebookAuthProvider,
} from '../configs/firebase';

// Facebookでのユーザ登録
export const facebookSignUp = async () => {
  await Facebook.initializeAsync(facebookConfig.ApplicationKey, '-----');
  const { type, token } = await Facebook.logInWithReadPermissionsAsync(
    facebookConfig.ApplicationKey,
    {
      permissions: ['public_profile'],
    }
  ).catch(e => {
    console.log(e);
    throw e;
  });

  if (type === 'success') {
    try {
      const credential = FacebookAuthProvider.credential(token);
      const user = await firebase.auth().signInWithCredential(credential);
      return user;
    } catch (error) {
      console.log(error);
    }
  }
  return null;
};

※ApplicationKey: '-----' のところは自身の値を入力してください。

大きな流れとしては、initializeAsyncでログインして、signInWithCredentialでサインインします。ExpoとFirebse が関数を用意してくれているので非常に助かります。

UIの実装

続いてUI部分の実装です。

SignUpScreen.tsxに追記

import {
  emailSignUp,
  phoneSignUp,
  * facebookSignUp, *
} from '../utils/auth';

  ---- 中略 ---- 

  const onPressFacebookSignIn = async (): Promise<void> => {
    const user = await facebookSignUp();
    console.log(user);
  };

  ---- 中略 ---- 

<Button title="Facebook Login" onPress={onPressFacebookSignIn} />

ボタンを押した時に、先ほど作成したfacebookSignUpを呼び出します。

完成

f:id:daiki-sato:20200207062810p:plain
Facebookログインボタン追加

f:id:daiki-sato:20200207062852p:plain
ボタン押下後

f:id:daiki-sato:20200207062918p:plain
ログイン画面

これでFacebookログインは完成です。

Googleアカウントログイン

最後にGoogleアカウントでのログインについてです。こちらはFacebookログインほど簡単にはできません。実装量が多いです。

Google Developに登録

まずはGoogle Developコンソールにログインして、必要な作業をしていきます。

Google Cloud Platform

Create Credentialsをクリック

f:id:daiki-sato:20200207064948p:plain
Credentials

Credentialsを選択。 こちらの方法はFirebaseでは完結せず、Developpers Consoleでプロジェクトを作成し、認証情報を追加する必要があります。 [認証情報]タブ→[認証情報を作成]→[OAuthクライアントID]を選択しクライアントIDを作成します。Androidの場合はパッケージ名を、iOSの場合はバンドルIDをhost.exp.exponentと入力します。

f:id:daiki-sato:20200207063518p:plain

f:id:daiki-sato:20200207063535p:plain

f:id:daiki-sato:20200207063554p:plain

f:id:daiki-sato:20200207063614p:plain

パッケージのインストール

今回の実装に必要はパッケージをインストールします。

yarn add expo-google-app-auth

 ロジックの実装

firabase.tsの追記

export const { FacebookAuthProvider, GoogleAuthProvider } = firebase.auth;

auth.tsの追記

import * as Google from 'expo-google-app-auth';

import firebase, {
  facebookConfig,
  FacebookAuthProvider,
  * GoogleAuthProvider,*
} from '../configs/firebase';

// Googelでのユーザ登録
const isUserEqual = (
  googleUser: Google.LogInResult,
  firebaseUser: firebase.User
): boolean => {
  if (firebaseUser) {
    const { providerData } = firebaseUser;
    // eslint-disable-next-line no-plusplus
    for (let i = 0; i < providerData.length; i++) {
      if (
        providerData[i].providerId ===
          firebase.auth.GoogleAuthProvider.PROVIDER_ID &&
        providerData[i].uid === googleUser.getBasicProfile().getId()
      ) {
        return true;
      }
    }
  }
  return false;
};

const onSignInGoolge = (googleUser: Google.LogInResult): void => {
  const unsubscribe = firebase.auth().onAuthStateChanged(firebaseUser => {
    unsubscribe();
    if (!isUserEqual(googleUser, firebaseUser)) {
      const credential = firebase.auth.GoogleAuthProvider.credential(
        googleUser.idToken,
        googleUser.accessToken
      );
      firebase
        .auth()
        .signInWithCredential(credential)
        .then(() => {
          console.log('success');
        })
        .catch(({ massage }) => alert('messgage', massage));
    } else {
      console.log('User already signed-in Firebase.');
    }
  });
};

export const goolgeSignUp = async () => {
  try {
    const result = await Google.logInAsync({
      behavior: 'web',
      androidClientId:
        '--------',
      iosClientId:
        '--------',
      scopes: ['profile', 'email'],
    });

    if (result.type === 'success') {
      onSignInGoolge(result);
      return result.accessToken;
    }
    return { cancelled: true };
  } catch (e) {
    return { error: true };
  }
};

※ --------の箇所は先ほ取得した自身の値を設定してください。
処理の大きな流れは、Googleアカウントにログインして、成功したらそのままFirebaseに登録しております。

 UIの実装

今作成したロジックを画面から呼び出せるようにします。

SignUpScreen.tsxの追記

import {
  emailSignUp,
  goolgeSignUp,
  phoneSignUp,
  facebookSignUp,
} from '../utils/auth';


  ---中略 ----

   const onPressGoolgeSignUp = async (): Promise<void> => {
    goolgeSignUp();
  };

  ---中略 ----

      <Button title="GoogleSignUp" onPress={onPressGoolgeSignUp} />

これでGoogleログインも完了です。

無事完了しました。最後に最終的コードを貼っておきます。

最終的なコード

configs/firebase.ts

import firebase from 'firebase';

export const firebaseConfig = {
  apiKey: '----',
  authDomain: '----',
  databaseURL: '----',
  projectId:'----',
  storageBucket: '----',
  messagingSenderId: '----',
  appId: '----',
  measurementId: '----',
};
export const facebookConfig = { ApplicationKey: '----' };
export const googleConfig = {
  androidClientId:
    '----',
  iosClientId:
    '----',
};

export const { FacebookAuthProvider, GoogleAuthProvider } = firebase.auth;

export default firebase;

utils/auth.ts

import * as Facebook from 'expo-facebook';
import { Linking } from 'expo';
import * as WebBrowser from 'expo-web-browser';
import * as Google from 'expo-google-app-auth';
import firebase, {
  facebookConfig,
  googleConfig,
  FacebookAuthProvider,
  GoogleAuthProvider,
} from '../configs/firebase';

// メールでのユーザ登録
export const emailSignUp = (email: string, password: string): void => {
  firebase
    .auth()
    .createUserWithEmailAndPassword(email, password)
    .then(user => {
      if (user) {
        console.log('Success to Signup');
      }
    })
    .catch(error => {
      console.log(error);
    });
};

// 電話番号で登録
export const phoneSignUp = async (
  phoneNumber: string
): Promise<firebase.auth.ConfirmationResult | null> => {
  const captchaUrl = `https://------/captcha.html?appurl=${Linking.makeUrl(
    ''
  )}`;

  let token: string | null = null;
  const listener = ({ url }): void => {
    WebBrowser.dismissBrowser();
    const tokenEncoded = Linking.parse(url).queryParams.token;
    if (tokenEncoded) token = decodeURIComponent(tokenEncoded);
  };

  Linking.addEventListener('url', listener);
  await WebBrowser.openBrowserAsync(captchaUrl);
  Linking.removeEventListener('url', listener);
  if (token) {
    // fake firebase.auth.ApplicationVerifier
    const captchaVerifier = {
      type: 'recaptcha',
      verify: () => Promise.resolve(token),
    };
    try {
      const confirmationResult = await firebase
        .auth()
        .signInWithPhoneNumber(phoneNumber, captchaVerifier);
      return confirmationResult;
    } catch (e) {
      console.warn(e);
      return null;
    }
  }
  return null;
};

// Facebookでのユーザ登録
export const facebookSignUp = async () => {
  await Facebook.initializeAsync(facebookConfig.ApplicationKey, '------');
  const { type, token } = await Facebook.logInWithReadPermissionsAsync(
    facebookConfig.ApplicationKey,
    {
      permissions: ['public_profile'],
    }
  ).catch(e => {
    console.log(e);
    throw e;
  });

  if (type === 'success') {
    try {
      const credential = FacebookAuthProvider.credential(token);
      const user = await firebase.auth().signInWithCredential(credential);
      return user;
    } catch (error) {
      console.log(error);
    }
  }
  return null;
};

// Googelでのユーザ登録
const isUserEqual = (
  googleUser: Google.LogInResult,
  firebaseUser: firebase.User
): boolean => {
  if (firebaseUser) {
    const { providerData } = firebaseUser;
    // eslint-disable-next-line no-plusplus
    for (let i = 0; i < providerData.length; i++) {
      if (
        providerData[i].providerId === GoogleAuthProvider.PROVIDER_ID &&
        providerData[i].uid === googleUser.getBasicProfile().getId()
      ) {
        return true;
      }
    }
  }
  return false;
};

const onSignInGoolge = (googleUser: Google.LogInResult): void => {
  const unsubscribe = firebase.auth().onAuthStateChanged(firebaseUser => {
    unsubscribe();
    if (!isUserEqual(googleUser, firebaseUser)) {
      const credential = GoogleAuthProvider.credential(
        googleUser.idToken,
        googleUser.accessToken
      );
      firebase
        .auth()
        .signInWithCredential(credential)
        .then(() => {
          console.log('success');
        })
        .catch(e => console.log('e', e));
    } else {
      console.log('User already signed-in Firebase.');
    }
  });
};

export const goolgeSignUp = async () => {
  try {
    const result = await Google.logInAsync({
      behavior: 'web',
      androidClientId: googleConfig.androidClientId,
      iosClientId: googleConfig.iosClientId,
      scopes: ['profile', 'email'],
    });

    if (result.type === 'success') {
      onSignInGoolge(result);
      return result.accessToken;
    }
    return { cancelled: true };
  } catch (e) {
    return { error: true };
  }
};

export default firebase;

screens/SignUpScreen.tsx

import React, { useState } from 'react';
import { View, StyleSheet, TextInput, Button } from 'react-native';
import { NavigationStackProp } from 'react-navigation-stack';
import {
  emailSignUp,
  goolgeSignUp,
  phoneSignUp,
  facebookSignUp,
} from '../utils/auth';

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
    padding: 16,
  },
  textInput: {
    fontSize: 14,
    color: 'black',
    width: '100%',
    borderBottomWidth: 1,
    borderBottomColor: 'black',
    margin: 16,
  },
});

const SignUpScreen: React.FC<{ navigation: NavigationStackProp }> = ({
  navigation,
}): JSX.Element => {
  const [email, setEmail] = useState();
  const [password, setPassword] = useState();
  const [phoneNumber, setPhoneNumber] = useState('');
  const [confirmResult, setConfirmResult] = useState(null);

  const onPressGoolgeSignUp = async (): Promise<void> => {
    await goolgeSignUp();
  };

  const onPressPhoneSignUp = async (): Promise<void> => {
    const result = await phoneSignUp(phoneNumber);
    if (result) {
      setConfirmResult(result);
      navigation.navigate('VerificationCode', { confirmResult: result });
    } else {
      console.log('error');
    }
  };

  const onPressFacebookSignIn = async (): Promise<void> => {
    const user = await facebookSignUp();
    console.log(user);
  };

  return (
    <View style={styles.container}>
      <TextInput
        style={styles.textInput}
        value={email}
        onChangeText={(text: string): void => setEmail(text)}
        editable
        maxLength={50}
        placeholder="Email"
        autoCapitalize="none"
        keyboardType="email-address"
        returnKeyType="done"
      />
      <TextInput
        style={styles.textInput}
        value={password}
        onChangeText={(text: string): void => setPassword(text)}
        editable
        maxLength={20}
        placeholder="Password"
        autoCapitalize="none"
        secureTextEntry
        returnKeyType="done"
      />
      <Button title="登録" onPress={(): void => emailSignUp(email, password)} />

      <TextInput
        style={styles.textInput}
        value={phoneNumber}
        onChangeText={(text: string): void => setPhoneNumber(text)}
        editable
        maxLength={50}
        placeholder="Phone"
        autoCapitalize="none"
        keyboardType="phone-pad"
        returnKeyType="done"
      />
      <Button title="PhoneSignUp" onPress={onPressPhoneSignUp} />
      <Button title="Facebook Login" onPress={onPressFacebookSignIn} />
      <Button title="GoogleSignUp" onPress={onPressGoolgeSignUp} />
    </View>
  );
};

export default SignUpScreen;

src/VerificationCodeScreen.tsx

import React, { useState } from 'react';
import { View, Text, StyleSheet, TextInput, Button } from 'react-native';
import { NavigationStackProp } from 'react-navigation-stack';

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
    padding: 16,
  },
  textInput: {
    fontSize: 14,
    color: 'black',
    width: '100%',
    borderBottomWidth: 1,
    borderBottomColor: 'black',
    margin: 16,
  },
});

const VerificationCodeScreen: React.FC<{ navigation: NavigationStackProp }> = ({
  navigation,
}): JSX.Element => {
  const [code, setCode] = useState('');

  const onPressConfirmCode = (): void => {
    if (code.length) {
      const { params = {} } = navigation.state;

      params.confirmResult
        .confirm(code)
        .then(user => {
          console.log('Code Confirmed!');
        })
        .catch(error => console.log(`Code Confirm Error: ${error.message}`));
    }
  };

  return (
    <View style={styles.container}>
      <Text>Enter verification code below:</Text>
      <TextInput
        style={styles.textInput}
        value={code}
        onChangeText={(value): void => setCode(value)}
        editable
        maxLength={50}
        placeholder="Code"
        autoCapitalize="none"
        keyboardType="numeric"
        returnKeyType="done"
      />
      <Button
        title="Confirm Code"
        color="#841584"
        onPress={onPressConfirmCode}
      />
    </View>
  );
};

export default VerificationCodeScreen;

public/captcha.html

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Welcome to Firebase Hosting</title>

  <!-- update the version number as needed -->
  <script defer src="/__/firebase/7.8.0/firebase-app.js"></script>
  <!-- include only the Firebase features as you need -->
  <script defer src="/__/firebase/7.8.0/firebase-auth.js"></script>
  <script defer src="/__/firebase/7.8.0/firebase-database.js"></script>
  <script defer src="/__/firebase/7.8.0/firebase-messaging.js"></script>
  <script defer src="/__/firebase/7.8.0/firebase-storage.js"></script>
  <!-- initialize the SDK after all desired features are loaded -->
  <script defer src="/__/firebase/init.js"></script>

  <style media="screen">
    body { background: #ECEFF1; color: rgba(0,0,0,0.87); font-family: Roboto, Helvetica, Arial, sans-serif; margin: 0; padding: 0; }
  </style>
</head>
<body>
<p>Please, enter captcha for continue<p/>
<button id="continue-btn" style="display:none">Continue to app</button>
<script>
  function getToken(callback) {
    const containerId = 'captcha';
    const container = document.createElement('div');
    container.id = containerId;
    document.body.appendChild(container);
    var captcha = new firebase.auth.RecaptchaVerifier(containerId, {
      'size': 'normal',
      'callback': function(token) {
        callback(token);
      },
      'expired-callback': function() {
        callback('');
      }
    });
    captcha.render().then(function() {
      captcha.verify();
    });
  }

  function sendTokenToApp(token) {
    const baseUri = decodeURIComponent(location.search.replace(/^\?appurl\=/, ''));
    const finalUrl = location.href = baseUri + '/?token=' + encodeURIComponent(token);
    const continueBtn = document.querySelector('#continue-btn');
    continueBtn.onclick = function() {
      window.open(finalUrl, '_blank');
    };
    continueBtn.style.display = 'block';
  }

  document.addEventListener('DOMContentLoaded', function() {
    getToken(sendTokenToApp);
  });
</script>
</body>
</html>  

※ ---- ----の箇所は全て自身の値に書き換えてください。

まとめ

いかがだったでしょうか。複雑な実装も多かったと思いますが、これで一通りのログイン制御はできると思います。ではまた次回。