---
# System prepended metadata

title: Firebase 筆記 (Google 登入 API、資料寫入 Firestore)

---

# Firebase 筆記 (Google 登入 API、資料寫入 Firestore)
此筆記是基於 zerotomastery.io 推出 React 課程 [Complete React Developer in 2022](https://www.udemy.com/course/complete-react-developer-zero-to-mastery/) 所撰寫。
## 88. Setting Up Firebase

1. firebase 官網新增新專案
2. `yarn add firebase`

## 89. Authentication Flow （Firebase 串 Google 登入驗證流程）

**腳色：**

1. 客戶端（我方網站）
2. Google Server
3. Firebase Server

**背後流程：**

1. 客戶端向 Google Server 發出一個登入 Request ⇒ 登入成功 Google Server 會回傳一個 auth_token 給客戶端。
2. 客戶端將 auth_token 發給 Firebase Server，Firebase Server 再向 Google Server 驗證這個 auth_token ⇒ 驗證成功 Google Server 會回傳一個 verification token 給 Firebase Server，此時 Firebase Server 會產生一個 access token 並回傳給客戶端。
3. 客戶端可以開始發 CRUD Requests（帶著 access token）到 Firebase Server

## 92. Authenticating With Firebase （串 Google 登入實作開始）

### 建置 Firebase 實例並 import 進 React 專案

1. 至 Firebase 新增一個網頁 App：

    ![](https://i.imgur.com/RWQLtVC.png)


    點選安卓圖案右邊的按鈕。

2.  輸入一個自訂 App 名稱並且註冊：

![](https://i.imgur.com/pbIioaa.png)


3.  註冊完成後，呈現以下畫面並把 SDK 段落全部複製：

```jsx
// Import the functions you need from the SDKs you need
import { initializeApp } from "firebase/app";
// TODO: Add SDKs for Firebase products that you want to use
// https://firebase.google.com/docs/web/setup#available-libraries

// Your web app's Firebase configuration
const firebaseConfig = {
  apiKey: "xxx",
  authDomain: "xxx",
  projectId: "xxx",
  storageBucket: "xxx",
  messagingSenderId: "xxx",
  appId: "xxx"
};

// Initialize Firebase
const app = initializeApp(firebaseConfig);
```

4.  新增檔案：`src/utils/firebase/firebase.utils.js` 貼上複製的 SDK

### 登入驗證實作

 1. import 登入、驗證相關 API from `'firebase/auth'`

```jsx
import {
	getAuth,
	signInWithRedirect,
	signInWithPopup, 
	GoogleAuthProvider
} from 'firebase/auth'
```

1. 建立 Google provider 實例，並呼叫 `setCustomParameters()`，此方法接受一個 configuration 物件作為參數，去定義這個 googleAuthProvider 要有什麼行為。這邊 configuration 物件填入 `{prompt: "select_account"}` ，意思是當每次有人接觸到我們的 provider，我們強制他一定要選擇一個帳號（若沒有加這個選項，使用者是在登入 Google 帳號狀態下會直接默認登入該帳號）：

```jsx
const provider = new GoogleAuthProvider();
provider.setCustomParameters({
	prompt: "select_account"
})
```

1. 呼叫 `getAuth()`、`signInWithPopup()` ，後者參數接收 `getAuth()` 的結果跟 `provider` ，最後 export 出去：

```jsx
export const auth = getAuth();
export const signInWithGooglePopup = () => signInWithPopup(auth, provider);
```

- 完整程式碼：
    
    ```jsx
    // firebase.utils.js
    import {
      getAuth,
      signInWithRedirect,
      signInWithPopup,
      googleAuthProvider,
    } from 'firebase/auth';
    import { initializeApp } from 'firebase/app';
    
    const firebaseConfig = {
      apiKey: 'AIzaSyCtIONL9cEIXntjElT-w7gGXK9JG_NzN8w',
      authDomain: 'crwn-clothing-db-2a895.firebaseapp.com',
      projectId: 'crwn-clothing-db-2a895',
      storageBucket: 'crwn-clothing-db-2a895.appspot.com',
      messagingSenderId: '758180544862',
      appId: '1:758180544862:web:eeb130b62cb76c1186cfcf',
    };
    
    // Initialize Firebase
    const firebaseApp = initializeApp(firebaseConfig);
    
    const provider = new googleAuthProvider();
    provider.setCustomParameters({
      prompt: 'select_account',
    });
    
    export const auth = getAuth();
    export const signInWithGooglePopup = () => signInWithPopup(auth, provider);
    ```
    
1. 回 Firebase 網站，點選昨方欄 Authentication，選擇 Google 接著照下圖指示：

    ![](https://i.imgur.com/eIyfux4.png)


1.  在 `sign-in.component.jsx` import utils 中的 `signInWithGooglePopup`，`signInWithGooglePopup()` 的結果會是一個 Promise 物件。

```jsx
import { signInWithGooglePopup } from '../../utils/firebase/firebase.utils';

const SignIn = () => {
  const logGoogleUser = async () => {
    const response = await signInWithGooglePopup();
    console.log(response);
  };
  return (
    <div>
      <h1>i am SignIn</h1>
      <button onClick={logGoogleUser}>Sign in with Google Popup</button>
    </div>
  );
};

export default SignIn;
```

本小節完成後，進度如下，可以看到 `await signInWithGooglePopup()` 的結果：

user 的資訊，如 accessToken 跟 uid 等等……

![](https://i.imgur.com/jqsLsnN.png)


**注意（重要）**

`signInWithGooglePopup()` 在做的事情是將透過 Google 登入的 user 通過 Firebase 的 Authentication 而已，並未寫入 Firestore 裡面：

![](https://i.imgur.com/Pb1TA2y.png)


## 93. Introducing Firestore Data Models （Firestore 資料結構）

Collection > Document > Data


## 94. Setting Up User Documents 開始使用 Firestore

**本小節情境：**

承 93. 小節，使用者透過 Google 登入後，我們要把使用者的資料存進 Firestore 裡面！

### 啟用 Firestore

1. 進入 Firebase 左方選擇 Firestore Database，選擇**以正式版模式啟動**
2. Cloud Firestore 位置任選

    ![](https://i.imgur.com/v9z0no1.png)


1. 進入**規則**，將 `allow read, write: if false` 改為 `allow read, write: if true` ，這是為了讓我們可以對這個資料庫的 document 進行修改。

    ![](https://i.imgur.com/5AH8KWa.png)


### 使用 Firestore API 將登入的使用者資料存入 Firestore

 1. 在 `src/utils/firebase/firebase.utils.js` import `'firebase/firestore'` 相關 API

```jsx
import {
  getFirestore, // 用來創造一個 firestore 實例
  doc, // 用來創造一個 document 實例
  getDoc, // 取得 document data
  setDoc // 設定 document data
} from 'firebase/firestore';
```

1. 建置 database （firestore 實例）

```jsx
export const db = getFirestore();
```

1. **建立一個 function 用來將登入的使用者資料存入 Firestore**
    - `doc()` 建立一個 document 實例，有兩種語法：
        1. 接收三個參數，第一個是 database、第二個是 collections（可想像為資料表名稱），第三個是一個字串作為此 doc 的 title，本範例使用 uid (Unique ID) 去識別該項目，uid 為當使用者通過 Google 登入後，Firebase 回傳的 uid。
        2. 接收兩個參數，第一個是 collection 實例，第二個是一個字串作為此 doc 的 title。
        
        語法1：`const userDocRef = doc(db, 'users', userAuth.uid);`
        
        語法2：`const docRef = doc(collectionRef, docTitle);`
        
        **補充**：此時我們才剛建立 firestore，資料庫中我們沒有 `userDocRef` 的參照，根本也沒有 users collection，但是在呼叫 `doc()` 之後，firestore 還是會建立一個物件指向一個獨立的位址，讓我們把資料 set 進去。（這個物件的 ID 就是我們傳入 `doc()` 裡的 `userAuth.uid` ）
        
    - `getDoc()` 用來把一個 document 實例的資料取出來，參數接收一個 document 實例，會回傳 Promise 物件。
        
        語法：`const userSnapshot = await getDoc(userDocRef);`
        
        - 將 `getDoc()` 的結果取出來後（用 `await` 或是 `.then()`）可直接呼叫 `exists()` 去判定在此 DB 中的該 collection 裡面到底存不存在這個值，這會回傳一個布林值。
    - `setDoc()` 用來設定一個 document 實例的資料，第一個參數接收一個 document 實例，第二個是一個要寫進資料庫的物件，會回傳 Promise 物件。
        
        語法：`await setDoc(userDocRef, {obj});`
        

```jsx
export const createUserDocumentFromAuth = async (userAuth) => {
	// 建立一個 document 實例
  const userDocRef = doc(db, 'users', userAuth.uid);
 
	// 將 document 實例的資料取出來
  const userSnapshot = await getDoc(userDocRef);
  console.log(userSnapshot);
  console.log(userSnapshot.exists());

	// 如果使用者不存在
  if (!userSnapshot.exists()) {
    const { displayName, email } = userAuth;
    const createdAt = new Date();
		// 就把資料寫進 Firestore
    try {
      await setDoc(userDocRef, {
        displayName,
        email,
        createdAt,
      });
    } catch (error) {
      console.log('建立使用者失敗' + error.message);
    }
  }

  // 如果使用者存在直接回傳 userDocRef
  return userDocRef;
};
```

## 97、98. Sign Up Form Pt.1 & Pt.2 + 99. Sign Up With Email + Password 信箱/密碼登入功能

**本三節重點：**

新增**信箱/密碼**的登入方式，藉由呼叫 `createUserWithEmailAndPassword()` 將透過使用**信箱/密碼**的 user ****通過 Firebase 的 Authentication。

- `createUserWithEmailAndPassword()` 跟 `signInWithGooglePopup()` 都是將 user 通過 Firebase 的 Authentication，並且會回傳 user 的一些資訊，如 uid、accessToken 等等。

### Firebase 設定

回到 Firebase 左方欄 Authentication，Sign-in method 新增**電子郵件/密碼**，如圖：

![](https://i.imgur.com/sBm7Ieg.png)


### 建置 sign-up-form 表單 ⇒ input 欄位掛 onChange 放進 useState

`src/components/sign-up-form/sign-up-form.component.jsx`

```jsx
import { useState } from 'react';

const defaultFormFields = {
  displayName: '',
  email: '',
  password: '',
  confirmPassword: '',
};

const SignUpForm = () => {
  const [formFields, setFormFields] = useState(defaultFormFields);
  const { displayName, email, password, confirmPassword } = formFields;
 
  const handleChange = (event) => {
    const { name, value } = event.target;
    setFormFields({ ...formFields, [name]: value });
  };

  return (
    <div>
      <h1>Sign up with your email and password</h1>
      <form>
        <label>Display Name</label>
        <input
          type="text"
          required
          onChange={handleChange}
          name="displayName"
          value={displayName}
        />

        <label>Email</label>
        <input
          type="email"
          required
          onChange={handleChange}
          name="email"
          value={email}
        />

        <label>Password</label>
        <input
          type="password"
          required
          onChange={handleChange}
          name="password"
          value={password}
        />

        <label>Confirm Password</label>
        <input
          type="password"
          required
          onChange={handleChange}
          name="confirmPassword"
          value={confirmPassword}
        />

        <button type="submit">Sign Up</button>
      </form>
    </div>
  );
};

export default SignUpForm;
```

### 表單 onSumbit 要做的事情

1. 在 `firebase.utils.js`
- **信箱/登入** sign-in method 是 Firebase 的 Native Provider 之一，所以只要 import 他的方法就好 ⇒ createUserWithEmailAndPassword。
- 補充 1：`createUserDocumentFromAuth` （在 Firestore 建立一個 user document）新增接收 additionalInformation 參數是因為採用**信箱/登入**（呼叫 `createUserWithEmailAndPassword(auth, email, password)` ）回傳的 user 資訊的 displayName 是 null，Firebase 應另有提供方法寫入 displayName 的值，但本課程講師決定當 user 以**信箱/登入，**displayName 就採用客戶端提供之 displayName input 值。additionalInformation 就是在 SignIn 時掛上 onChange 的 displayName 欄位的值。

```jsx
// in firebase.utils.js
import {
  getAuth,
  signInWithRedirect,
  signInWithPopup,
  GoogleAuthProvider,
  createUserWithEmailAndPassword, // import **信箱/登入** sign-in method
} from 'firebase/auth';

// 中間略...

export const createUserDocumentFromAuth = async (
  userAuth,
  additionalInformation = {} // 補充 1
) => {
  if (!userAuth) return;
  const userDocRef = doc(db, 'users', userAuth.uid);
  const userSnapshot = await getDoc(userDocRef);

  // 如果使用者不存在就將資料寫進 Firestore
  if (!userSnapshot.exists()) {
    const { displayName, email } = userAuth;
    const createdAt = new Date();

    try {
      await setDoc(userDocRef, {
        displayName,
        email,
        createdAt,
        ...additionalInformation, // 補充 1
      });
    } catch (error) {
      console.log('建立使用者失敗' + error.message);
    }
  }

  // 如果使用者存在直接回傳 userDocRef
  return userDocRef;
};

export const createAuthUserWithEmailAndPassword = async (email, password) => {
  if (!email || !password) return;

  return await createUserWithEmailAndPassword(auth, email, password);
};
```

2.  在 `sign-up-form.component.jsx`
    1.  `import { createAuthUserWithEmailAndPassword } from '../../utils/firebase/firebase.utils';`
    2. 表單送出時停止預設行為 ⇒ `event.preventDefault()`。
    3. 比對密碼與確認密碼欄位。
    4. `createAuthUserWithEmailAndPassword()` 傳入 email, password 參數後呼叫後會通過 Firebase 的 Authentication 並回傳一些 user 資訊，如 accessToken、uid 等等但透過**信箱/密碼**方式登入不會有 displayName。
    5. `createUserDocumentFromAuth()` 傳入呼叫 `createAuthUserWithEmailAndPassword()` 回傳的 user 資訊跟客戶端提供的 displayName ⇒ 在 Firestore 存入該使用者資料

```jsx
import { useState } from 'react';
import {
  createAuthUserWithEmailAndPassword,
  createUserDocumentFromAuth,
} from '../../utils/firebase/firebase.utils';

const defaultFormFields = {
  displayName: '',
  email: '',
  password: '',
  confirmPassword: '',
};

const SignUpForm = () => {
  const [formFields, setFormFields] = useState(defaultFormFields);
  const { displayName, email, password, confirmPassword } = formFields;

  const handleChange = (event) => {
    const { name, value } = event.target;
    setFormFields({ ...formFields, [name]: value });
  };

  const handleSubmit = async (event) => {
    event.preventDefault();
    if (password !== confirmPassword) {
      alert('passwords do not match');
      return;
    }
    try {
      const { user } = await createAuthUserWithEmailAndPassword(
        email,
        password
      );
      await createUserDocumentFromAuth(user, { displayName });
      setFormFields(defaultFormFields);
    } catch (error) {
      if (error.code === 'auth/email-already-in-use') {
        alert('Cannot create user, email already in use');
      }
      console.log('user creation encountered an error' + error);
    }
  };

  return (
    <div>
      <h1>Sign up with your email and password</h1>
      <form onSubmit={handleSubmit}>
	      // inputs 略...
        <button type="submit">Sign Up</button>
      </form>
    </div>
  );
};

export default SignUpForm;
```

## 100. Generalizing Form Input Component 美化&元件化 input

`src/components` 下新增 form-input 資料夾，底下再新增

- `form-input.component.jsx`
    - 這裡 `…otherProps` 運用到**剩餘屬性**！

```jsx
import './form-input.styles.scss';
const FormInput = ({ label, ...othersProps }) => {
  return (
    <div className="group">
      {label && (
        <label
          className={`${
            othersProps.value.length ? 'shink' : ''
          } form-input-label`}
        >
          {label}
        </label>
      )}

      <input className="form-input" {...othersProps} />
    </div>
  );
};

export default FormInput;
```

- `form-input.styles.scss`

## 101. Custom Button Component  按鈕元件化

**本小節重點：**

本專案中有三種相似 button，將 <button> 元件化並依據 class 名稱去顯示不同樣式。

- `src/components/button` 新增 `button.component.jsx`

```jsx
import './button.styles.scss';
const BUTTON_TYPE_CLASSES = {
  google: 'google-sign-in',
  inverted: 'inverted',
};

const Button = ({ children, buttonType, ...otherProps }) => {
  return (
    <button
      className={`button-container ${BUTTON_TYPE_CLASSES[buttonType]}`}
      {...otherProps} // 這裡的 otherProps 有 type="submit"
    >
      {children}
    </button>
  );
};

export default Button;
```

- [props.children 補充](https://b-l-u-e-b-e-r-r-y.github.io/post/ReactChildren/)

## 102. Sign In Form & 103. Finishing Authentication Page

**本兩小節重點：**

- 新增 `sign-in.component.jsx`
- 使用 Firebase API `signInWithEmailAndPassword` 進行登入
1. 在 `firebase.utils.js` `import {signInWithEmailAndPassword} from 'firebase/auth';`
2. 傳入 auth 參數後 export 出去

```jsx
export const signInAuthUserWithEmailAndPassword = async (email, password) => {
  if (!email || !password) return;

  return await signInWithEmailAndPassword(auth, email, password);
};
```

 3. 在 `sign-in-form.component.jsx` import `signInAuthUserWithEmailAndPassword`，onSubmit 事件如下：

```jsx
const handleSubmit = async (event) => {
    event.preventDefault();

    try {
      const response = await signInAuthUserWithEmailAndPassword(
        email,
        password
      );
      console.log(response);
      setFormFields(defaultFormFields);
    } catch (error) {
			// 錯誤處理
      switch (error.code) {
        case 'auth/wrong-password':
          alert('incorrect password for email');
          break;
        case 'auth/user-not-found':
          alert('no user associated with this email');
          break;
        default:
          console.log(error);
      }
    }
```

### [補充 return await Promise?](https://eslint.bootcss.com/docs/rules/no-return-await)