Skip to main content

第20章:ミニ課題:ログイン必須ページ完成+チェックリスト✅🚪

ここは“総仕上げ回”だよ〜!😄 いままで作ってきた メールログイン+Googleログインを、「ログイン必須ページ(ガード付き)」として完成させて、さらに エラーがやさしい日本語で出るように整えて、最後に **AI(Firebase AI Logic / Gemini)**でUXを一段よくします🤖✨


0) この章のゴール(完成形イメージ)🏁🧭

完成したらこうなる👇😊

  • /login:メールログイン + Googleログイン(Popupメイン、Redirectも逃げ道)🌈
  • /signup:メール登録✍️
  • /mypage:ログイン必須(未ログインなら /login へ)🚧
  • エラー表示が「次に何をすればいいか」わかる言葉になってる😇
  • ログイン失敗時に「原因の説明💬」をGeminiに作ってもらえる(押すと出る)🤖✨(Firebase AI Logic)

1) “最終チェック用”の作るものリスト🧱📝

この章で揃える部品はこれ!

  1. ルート構成/login /signup /mypage)🧭
  2. Auth状態の一本化AuthProvider / useAuth)🦴
  3. ガード(RequireAuth) 🚧
  4. Googleログイン:Popup + Redirect(Popupがダメな環境の逃げ道)🌈
  5. getRedirectResultの回収(Redirectで戻ってきたときの結果取得)🔁
  6. エラー翻訳テーブル(Firebaseのエラーコード→やさしい表示)🗺️
  7. AIボタン(Geminiが“原因説明”を生成)🤖📝

2) ルーティング(React Router)を“完成形”にする🧭✨

Route Structure Map

まずはルートを3つに固定しよう👍 (すでに第16章でやってたら「最終形に整える」感じでOK!)

  • /login → ログインページ
  • /signup → 登録ページ
  • /mypage → ログイン必須ページ(ガード付き)

例(超ざっくり)👇

// App.tsx(例)
import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
import { AuthProvider } from "./auth/AuthProvider";
import { RequireAuth } from "./auth/RequireAuth";
import { LoginPage } from "./pages/LoginPage";
import { SignupPage } from "./pages/SignupPage";
import { MyPage } from "./pages/MyPage";

export default function App() {
return (
<BrowserRouter>
<AuthProvider>
<Routes>
<Route path="/" element={<Navigate to="/mypage" replace />} />
<Route path="/login" element={<LoginPage />} />
<Route path="/signup" element={<SignupPage />} />
<Route
path="/mypage"
element={
<RequireAuth>
<MyPage />
</RequireAuth>
}
/>
</Routes>
</AuthProvider>
</BrowserRouter>
);
}

3) RequireAuth(ガード)を“事故らない形”にする🚧🛡️

RequireAuth Logic Flow

ガードの鉄板はこれ👇

  • loading の間はスピナー(ここ超大事!)⏳
  • user == null なら /login に飛ばす(できれば“戻り先”も渡す)🔁
  • user != null なら子コンポーネント表示🙆‍♂️
// auth/RequireAuth.tsx(例)
import { Navigate, useLocation } from "react-router-dom";
import { useAuth } from "./useAuth";

export function RequireAuth({ children }: { children: React.ReactNode }) {
const { user, loading } = useAuth();
const location = useLocation();

if (loading) {
return <div style={{ padding: 24 }}>読み込み中…⏳</div>;
}

if (!user) {
return <Navigate to="/login" replace state={{ from: location.pathname }} />;
}

return <>{children}</>;
}

4) Googleログイン:Popupメイン+Redirectを“逃げ道”として用意🌈🚪

Google Login Fallback

なぜPopupだけじゃダメ?🤔

PopupはPCで体験が良いけど、環境によってはブロックされることがある😵‍💫 だから「PopupがダメならRedirectボタンを出す」が優しい✨

しかも Redirect は、近年のブラウザ事情で 追加の対策が必要になることがあるよ。Firebase公式も「本番で全ブラウザで意図どおり動かすには、案内されてる選択肢のどれかを必ず実装してね」と明言してる。さらに 2024-06-24以降、Chrome M115+ でも必須になったよ(Firefox/Safariはもっと前から必須)。(Firebase)

実装方針(おすすめ)👍

  • Googleログインボタン(Popup)
  • Popupがブロックされたっぽいエラーなら「Redirectでログイン」ボタンを表示
  • Redirectで戻ってきたら getRedirectResult() を回収する🔁(後でやる)

5) Loginページ(メール+Google+エラー表示)を完成させる🔑🌈😇

Login Page Wireframe

ポイントはこの3つ!

  1. メールログイン(成功したら “元のページ”へ戻す)🔁
  2. Googleログイン(まずPopup)🌈
  3. エラーは“翻訳して表示”😇(AIボタンは次で追加)

例👇(必要なところだけ抜粋)

// pages/LoginPage.tsx(例)
import { useLocation, useNavigate, Link } from "react-router-dom";
import { useState } from "react";
import { useAuth } from "../auth/useAuth";
import { toFriendlyAuthMessage } from "../lib/authErrors";

export function LoginPage() {
const nav = useNavigate();
const loc = useLocation();
const from = (loc.state as any)?.from ?? "/mypage";

const { loginWithEmail, loginWithGooglePopup, loginWithGoogleRedirect } = useAuth();

const [email, setEmail] = useState("");
const [pw, setPw] = useState("");
const [err, setErr] = useState<string | null>(null);
const [showRedirect, setShowRedirect] = useState(false);
const [busy, setBusy] = useState(false);

async function onEmailLogin() {
setBusy(true); setErr(null);
try {
await loginWithEmail(email, pw);
nav(from, { replace: true });
} catch (e: any) {
setErr(toFriendlyAuthMessage(e));
} finally {
setBusy(false);
}
}

async function onGooglePopup() {
setBusy(true); setErr(null); setShowRedirect(false);
try {
await loginWithGooglePopup();
nav(from, { replace: true });
} catch (e: any) {
const msg = toFriendlyAuthMessage(e);
setErr(msg);

// Popup系の失敗なら「Redirectでやり直す」導線を出す(例)
const code = e?.code as string | undefined;
if (code?.includes("popup") || code === "auth/operation-not-supported-in-this-environment") {
setShowRedirect(true);
}
} finally {
setBusy(false);
}
}

async function onGoogleRedirect() {
setBusy(true); setErr(null);
try {
await loginWithGoogleRedirect();
// ここで画面が遷移する(戻ってきたら getRedirectResult で回収)
} catch (e: any) {
setErr(toFriendlyAuthMessage(e));
setBusy(false);
}
}

return (
<div style={{ padding: 24, maxWidth: 480 }}>
<h1>ログイン🔐</h1>

<div style={{ marginTop: 12 }}>
<input value={email} onChange={(e) => setEmail(e.target.value)} placeholder="メール" />
</div>
<div style={{ marginTop: 8 }}>
<input value={pw} onChange={(e) => setPw(e.target.value)} placeholder="パスワード" type="password" />
</div>

<button disabled={busy} onClick={onEmailLogin} style={{ marginTop: 12 }}>
メールでログイン🔑
</button>

<hr style={{ margin: "16px 0" }} />

<button disabled={busy} onClick={onGooglePopup}>
Googleでログイン🌈
</button>

{showRedirect && (
<div style={{ marginTop: 8 }}>
<button disabled={busy} onClick={onGoogleRedirect}>
Popupが無理そう → Redirectでログイン🚪
</button>
</div>
)}

{err && <div style={{ marginTop: 12, color: "crimson" }}>{err}</div>}

<div style={{ marginTop: 16 }}>
<Link to="/signup">新規登録はこちら✍️</Link>
</div>
</div>
);
}

6) Redirectで戻ってきた結果を“必ず回収”する🔁✅

Redirect Result Retrieval

Redirectログインは、戻ってきたあとに getRedirectResult() で結果を受け取るのがセットだよね😊 Firebase公式のベストプラクティスでも signInWithRedirect()getRedirectResult() の組み合わせが例示されてるよ。(Firebase)

これを AuthProvider の初期化で一回だけ実行して、エラーも表示できるようにしよう。

// auth/AuthProvider.tsx(例:要点だけ)
import { createContext, useContext, useEffect, useMemo, useState } from "react";
import { onAuthStateChanged, getRedirectResult, GoogleAuthProvider, signInWithRedirect, signInWithPopup, signInWithEmailAndPassword, signOut, User } from "firebase/auth";
import { auth } from "../lib/firebase";
import { toFriendlyAuthMessage } from "../lib/authErrors";

type AuthCtx = {
user: User | null;
loading: boolean;
lastAuthError: string | null;
loginWithEmail: (email: string, pw: string) => Promise<void>;
loginWithGooglePopup: () => Promise<void>;
loginWithGoogleRedirect: () => Promise<void>;
logout: () => Promise<void>;
};

const Ctx = createContext<AuthCtx | null>(null);

export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const [lastAuthError, setLastAuthError] = useState<string | null>(null);

useEffect(() => {
let alive = true;

async function boot() {
// ① Redirect結果の回収(戻ってきた直後だけ意味がある)
try {
await getRedirectResult(auth);
} catch (e: any) {
if (alive) setLastAuthError(toFriendlyAuthMessage(e));
}

// ② 通常のログイン状態監視
const unsub = onAuthStateChanged(auth, (u) => {
if (!alive) return;
setUser(u);
setLoading(false);
});

return () => unsub();
}

const cleanupPromise = boot();
return () => {
alive = false;
cleanupPromise.then((fn) => fn?.());
};
}, []);

const value = useMemo<AuthCtx>(() => ({
user,
loading,
lastAuthError,
loginWithEmail: async (email, pw) => { await signInWithEmailAndPassword(auth, email, pw); },
loginWithGooglePopup: async () => { await signInWithPopup(auth, new GoogleAuthProvider()); },
loginWithGoogleRedirect: async () => { await signInWithRedirect(auth, new GoogleAuthProvider()); },
logout: async () => { await signOut(auth); },
}), [user, loading, lastAuthError]);

return <Ctx.Provider value={value}>{children}</Ctx.Provider>;
}

export function useAuth() {
const v = useContext(Ctx);
if (!v) throw new Error("AuthProviderが必要です");
return v;
}

7) エラー翻訳(“人間の言葉”にする)😇🗺️

Firebase Authのエラーは、コードのままだと冷たい…🥶 だから「よくあるやつだけでも翻訳表」を作ると、体験が一気に良くなる✨

// lib/authErrors.ts(例)
export function toFriendlyAuthMessage(e: any): string {
const code = (e?.code as string | undefined) ?? "unknown";
switch (code) {
case "auth/invalid-email":
return "メールアドレスの形がちょっと変かも…📧💦 もう一回確認してね!";
case "auth/user-not-found":
return "そのメールのユーザーが見つからなかったよ👀 登録がまだなら新規登録へ!";
case "auth/wrong-password":
return "パスワードが違うみたい…🔑💦 入力ミスがないか見てみてね!";
case "auth/too-many-requests":
return "試行回数が多いので、少し時間をおいてから試してね⏳";
case "auth/popup-blocked":
return "Popupがブロックされたみたい😵 ブラウザ設定を確認するか、Redirectで試してね🚪";
case "auth/popup-closed-by-user":
return "Popupを閉じたみたい!もう一回やってみよう😊";
default:
return `ログインでエラーが起きたよ😢(${code}`;
}
}

8) 伸ばし(AI):失敗理由の説明をGeminiに作らせる💬🤖✨

AI Error Explanation Flow

ここからが“今っぽい強化”🔥 ログインに失敗したとき、ただエラーを出すだけじゃなくて、

  • 「何が起きたか」
  • 「ユーザーが次に何をすればいいか」

Geminiが短くやさしく説明してくれるボタンを付けよう😊

Firebase AI Logic(Web)の最小セット🧩

Firebase公式のWeb例では、firebase/ai から getAI, getGenerativeModel, GoogleAIBackend を使うよ。(Firebase) また、Gemini 2.0 Flash 系が 2026-03-31 に退役予定なので、今からなら gemini-2.5-... 系を選ぶのが安全だよ(例:gemini-2.5-flash / gemini-2.5-flash-lite)。(Firebase)

8-1) lib/ai.ts を作る(AIの窓口)🚪🤖

// lib/ai.ts(例)
import { firebaseApp } from "./firebase";
import { getAI, getGenerativeModel, GoogleAIBackend } from "firebase/ai";

const ai = getAI(firebaseApp, { backend: new GoogleAIBackend() });
const model = getGenerativeModel(ai, { model: "gemini-2.5-flash" });

export async function explainAuthErrorWithAI(params: {
errorCode: string;
situation: "login" | "signup";
}): Promise<string> {
// 個人情報は送らない(メール・パスワード・UIDなどは入れない)🙅‍♂️
const prompt =
`あなたはWebアプリのサポート担当です。` +
`ユーザーに向けて、次を日本語でやさしく説明して。` +
`\n- 起きたこと(1文)` +
`\n- どうすれば直るか(2〜3個の箇条書き)` +
`\n- 不安を煽らないトーン` +
`\n\n状況: ${params.situation}` +
`\nFirebase Auth errorCode: ${params.errorCode}`;

const result = await model.generateContent(prompt);
// SDKの返し方は環境で差があるので「とにかくテキストを返す」形に寄せる
const text = (result as any)?.response?.text?.() ?? (result as any)?.text ?? "";
return String(text || "うまく説明を作れなかった…ごめんね🥲");
}

Firebase AI Logic の導入フローでは、コンソール側でプロバイダ(Gemini Developer API 推奨)を選んで、必要APIやキーを準備する流れになってるよ。キーをアプリに直書きしない注意も書かれてる。(Firebase) それと、開発が本気になってきたら App Checkを早めに入れるのが推奨だよ🛡️(Firebase)

8-2) Login画面に「原因を説明して💬」ボタンを付ける✨

// LoginPage.tsx のどこか(例)
import { explainAuthErrorWithAI } from "../lib/ai";

const [aiHelp, setAiHelp] = useState<string | null>(null);
const [aiBusy, setAiBusy] = useState(false);
const [lastErrorCode, setLastErrorCode] = useState<string | null>(null);

// catch(e) の中で
// setLastErrorCode(e?.code ?? "unknown");

async function onAskAI() {
if (!lastErrorCode) return;
setAiBusy(true);
try {
const text = await explainAuthErrorWithAI({ errorCode: lastErrorCode, situation: "login" });
setAiHelp(text);
} finally {
setAiBusy(false);
}
}

表示部分👇

{lastErrorCode && (
<div style={{ marginTop: 8 }}>
<button disabled={aiBusy} onClick={onAskAI}>
原因をやさしく説明して💬🤖
</button>
</div>
)}

{aiHelp && (
<div style={{ marginTop: 8, whiteSpace: "pre-wrap", background: "#f6f6f6", padding: 12 }}>
{aiHelp}
</div>
)}

9) Antigravity / Gemini CLI で“仕上げの品質チェック”をやる🔎🤖🛠️

Antigravity(エージェント)に投げるミッション例🛰️

Antigravityは「複数エージェントをミッションコントロールで動かす」系の開発体験を狙ったものだよ。(Google Codelabs) この章の相性、めっちゃ良い🙂

  • ミッションA:RequireAuth の分岐漏れ(loading/user null)チェック🚧
  • ミッションB:Authエラーコード一覧の“不足”を洗い出して候補追加🗺️
  • ミッションC:getRedirectResult の呼び出し位置が安全かレビュー🔁
  • ミッションD:UI文言(説明、ボタン、補足文)を統一して整える🧼✨

Gemini CLIで「抜け・漏れ」点検🧪

Gemini CLI はターミナルで使えるオープンソースのAIエージェントで、ReActループやMCPなども触れられる設計になってるよ。(Google Cloud Documentation)

やること例👇(イメージ)

  • 「未ログインで /mypage 直打ちしたときの挙動」をレビュー
  • 「Popupが失敗したときの導線」が自然かレビュー
  • 「エラーメッセージが責めてないか」レビュー😇

10) 最終チェックリスト(この章の合格ライン)✅✅✅

Chapter Completion Checklist

ここ、チェックが全部つけば勝ち!🎉

  • /mypage を未ログインで開く → /login に飛ぶ🚧
  • ログイン成功 → 元のページ(from)に戻る🔁
  • リロードしてもログイン状態が方針どおり維持される(local/sessionなど)🔄
  • ログアウト → ログイン前UIに戻る🚪
  • Googleログイン:Popupが成功する🌈
  • Popupが失敗する環境でも Redirect導線で詰まらない🚪
  • Redirectで戻ってきたあと getRedirectResult() が回収されてる🔁
  • エラー文が「次に何すればいいか」になってる😇
  • AIボタンが押せて、説明文が出る💬🤖

Redirectの本番安定化は、Firebase公式の “Option 1〜5” のどれを採るかが超重要だよ(ホスティング形態で分岐するやつ)。(Firebase)


11) ミニ問題(理解チェック)📝🙂

  1. RequireAuthloading 中に即リダイレクトしちゃうと何が起きる?⏳
  2. Redirectログインで “戻ってきた結果” を受け取る関数はどれ?🔁
  3. Popupがブロックされたとき、ユーザーに用意すべき導線は?🚪
  4. AIに送っていい情報・ダメな情報の例を1つずつ言える?🙅‍♂️✅

次に進むなら…🔜🔥

この“認証の背骨”ができたら、次の章(Firestore)で users/{uid} を中心にデータを持つ設計が一気に気持ちよくなるよ🦴➡️📚

「今のコード構成(ファイル一覧)を貼る」か、「今どこまで動いてるか(Popup/Redirect/AI)」を教えてくれたら、あなたの状態に合わせて“最短で合格”に寄せる調整案も出せるよ😄✨