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