# 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)