1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

chrome extensionからFirebase AuthenticationとFunctionsを利用する

Last updated at Posted at 2021-05-06

別記事にて簡易な記事閲覧アプリを作成しています。その一環でブラウザから今開いているタブのURLをアプリへ記事登録できるように簡単なchrome-extensionを作成しました。今回はその内容をまとめます。
アプリのソースはこちらになります。

全体像

image.png

ざっくりとした全体像を描くと上のようになります。

chrome-extension側ではバックグラウンド(background.ts)で実際のログインや登録したいURLのFirebaseへの送信等の機能を実装します。

popup.ts(あるいはトランスパイル後のjsファイル)chrome-extensionのmanifest.jsondefault_popup:経由で呼ばれるよう設定します。これより拡張機能アイコンをクリックしたときにLogin及び(URLの)Saveイベントを受け取るButton UIを表示させます。
ユーザーがログイン前の場合はLogin buttonを、ログイン後の場合はSave buttionを表示させます。

この時background.tspopup.ts間のリクエストやデータ通信はchrome-extensionのmessagestorage機能を使って実装します。

Firebase側ではAuthentication機能としてsignInWithPopup()機能を使います。background.tsからリクエストが来た際に認証を行い、ユーザー情報を返します。

またFunctionsではbackground.tsSaveToDb(User)から送られてきたURL情報をパースしFirestoreに保存します。これにより記事閲覧アプリから見れるようになります。

Chrome-extension詳細

環境

公式のチュートリアルで扱われているものは、そのままだとFirebase SDKを使えません。そのためwebpack等を用いてnodejsアプリとしてbundleできるように整備しました。

今回はchrome-extension-typescript-starterをベースとして利用させてもらい、Firebaseとのbundleを構成しました。

Firebase SDK自体のnodejs向けインストール方法は公式を参照して下さい。

一点嵌まったポイントとして、webpack経由で生成されたjavascriptファイルをchrome extension側から読ませた場合にEncoding is not UTF-8と表示されるエラーが発生する場合があります。これはFirebase SDKをwebpackでbundleした際にUTF-8でない文字が発生するためのようです(詳細)。今回は回避手段としてsed等を駆使して文字列検出と排除を行っています。

background.ts

このファイルでは主にFirebase Authenticationを利用したlogin、及びFirebase Functionsを利用したsaveToDbメソッドを実装します。

var auth = firebase.auth();
var functions = firebase.functions();

chrome.runtime.onMessage.addListener(
  function (request, sender, sendResponse) {
    if (request.name === "login") {
      // Login request.
      var provider = new firebase.auth.GoogleAuthProvider();
      provider.addScope('https://www.googleapis.com/auth/contacts.readonly');

      // Chrome extension only supports popup sign in.
      auth.signInWithPopup(provider).then(function (result) {
        var user = result.user;
        var status = '200';
        sendResponse({ user, status });
      }).catch(function (error) {
        // Handle Errors here.
        var errorCode = error.code;
        if (errorCode === 'auth/account-exists-with-different-credential') {
          alert('You have already signed up with a different auth provider for that email.');
          // If you are using multiple auth providers on your app you should handle linking
          // the user's accounts here.
        } else {
          console.error(error);
        }
        var status = '400';
        sendResponse({ error, status });
      });
    } else {
      var error = "unknown request: " + request.name;
      console.error("unknown request", request.name);
      var status = '400';
      sendResponse({ error, status });
    }

    return true;
  });

chrome.runtime.onMessage.addListenerは他(popup.ts)からのmessageを待ち受け、リクエストの種類(login)に応じて処理を行います。

loginリクエストに対しては、今回Googleのpopupベースのsign inスキームを利用しています。セットアップ方法は公式が参考になります。特にchrome-extensionは制約としてpopup operationのみのサポートとなっています。また利用時にはFirebase側でドメインの承認が必要です(参考

ログイン情報の管理はbackground.ts上でonAuthStateChangedを利用して以下のように設定できます。

var auth = firebase.auth();

function initApp() {
  // Listening for auth state changes.
  auth.onAuthStateChanged(function (user: firebase.User | null) {
    if (user) {
      // User is signed in.
      let displayName = user.displayName;
      let email = user.email;
      let uid = user.uid;
      chrome.storage.sync.set({ displayName, email, uid }, () => {
        console.log('User is signed in: ', displayName, email, uid);
      });
    } else {
      // User is signed out.
      let displayName = '';
      let email = '';
      let uid = '';
      chrome.storage.sync.set({ displayName, email, uid }, () => {
        console.log('User is signed out');
      });
    }
  });
}

initApp();

ここで現在のログイン情報はchrome.storage機能を使って保持し、後ほどpopup.tsからも見えるようにしています。

続いてsaveToDbリクエストに対する処理も同様にchrome.runtime.onMessage.addListenerへ加えていきます。

    } else if (request.name === "saveToDb") {
      // Save requested URL to DB.
      let url = request.url;

      // Request to Firebase functions.
      chrome.storage.sync.get("uid", ({ uid }) => {
        let saveToDb = functions.httpsCallable('saveToDb');
        saveToDb({ uid, url })
          .catch((error) => console.log('error saving url:', error));
      });

こちらではFirebase functions側のhttpsCallableを使った呼び出しにてfunctions側に対応する登録済saveToDbメソッドを呼び出していきます。functions側のメソッドは今回省略しますが、現時点では単純にFirestoreへデータ追加を行うのみとなります。

popup.ts

先述した通り、manifest.jsonの設定より、拡張機能のアイコンをクリックした際にこのスクリプトが呼び出されることになります。

popup.tsではReactを使ってloginsaveToDbに関わるViewを実装します。

const Popup = () => {
  const [uid, setUid] = useState<string>("");
  const [currentURL, setCurrentURL] = useState<string>();

  useEffect(() => {
    chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {
      setCurrentURL(tabs[0].url);
    });
  }, []);

  useEffect(() => {
    chrome.storage.sync.get("uid", ({ uid }) => {
      setUid(uid);
    });
  }, []);

  const Login = () => {
    chrome.runtime.sendMessage({ name: "login" }, response => {
      if (response.status !== "200") {
        console.error("Error on login request", response.error);
        alert(response.error);
        return;
      }
      console.log("Login succeeded:", response.user);
      setUid(response.user.uid);
    });
  }

  if (!uid) {
    // User is signed out.
    return (
      <>
        <ul>
          <li>Current URL: {currentURL}</li>
          <li>Current Time: {new Date().toLocaleTimeString()}</li>
          <li>User ID: Not logged in</li>
        </ul>
        <button
          onClick={() => Login()}
        >
          Log in
        </button>
      </>
    );
  }

  // User is signed in.
  let view: React.ReactElement;
  if (!isUrlSaved) {
    view = (
      <button
        onClick={() => {
          SendUrlToDb(currentURL);
          setIsUrlSaved(true);
        }}
      >
        Save button
      </button>
    );
  } else {
    view = (<p>"URL saved!"</p>);
  }

  return (
    <>
      <ul>
        <li>Current URL: {currentURL}</li>
        <li>Current Time: {new Date().toLocaleTimeString()}</li>
        <li>User ID: {uid}</li>
      </ul>
      {view}
    </>
  );
};

ReactDOM.render(
  <React.StrictMode>
    <Popup />
  </React.StrictMode>,
  document.getElementById("root")
);

Popupメソッドはユーザー情報に基づいてlogin button、あるいはsave buttonを表示させます。ユーザー情報は先程background.ts側でchrome.storageを用いて保持してあります。

続いてログイン後に表示されるsave buttonをクリックすると、以下のようなsendToDbメソッドが呼ばれます。

const SendToDb = (url: string | undefined) => {
  if (url === undefined) {
    console.error("Given URL is undefined.");
    alert("Given URL is undefined.");
    return;
  }
  console.log("Send URL to firebase DB", url);

  chrome.runtime.sendMessage({ name: "sendToDb", url }, response => {
    if (response.status !== "200") {
      console.error("Error on sending URL", response.error);
      alert(response.error);
      return;
    }
    console.log("Send URL successfully:", url);
  });
}

background.js側でmessage requestを受ける機能を実装済ですので、このメソッドではそれをurlと共に単純に呼び出しています。

デモ

Firebase側ではemulatorを使って実際に動かしてみました。

Webpackから出力したファイル群をchrome-extensionのパッケージ化されていない拡張機能を読み込むから読み込みます。

image.png

テスト用にexample.comへアクセスしアイコンをクリックしてみます

image.png

Log in buttonをクリックしてGoogleからログインします。するとポップアップ表示が以下に変わりました。

image.png

Save buttonを押してみた後、Firebase Firestoreを確認します。

image.png

以上のようにFireStore側でデータが追加されている事が確認できました。

補足

Firebase emulatorでauthenticationをemulateしようとしましたが、恐らく?既知のバグがあるようで今回は断念し、実際の環境で試しています。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?