テーマ
Design: ソーシャルログインモーダル
要件は requirements.md、ビジュアルは ui-design.md 参照。
モジュール構成
app/
├── api/auth/[...nextauth]/route.ts # NextAuth.js ハンドラー
└── providers.tsx # SessionProvider ラッパー
auth.config.ts # NextAuth プロバイダー設定
features/auth/
├── hooks/
│ ├── useAuth.ts # useSession の薄いラッパー
│ └── useLoginFlow.ts # ログインモーダル制御 + 認証後コールバック
└── types.ts
components/modals/
└── SocialLoginModal.tsx # 表示専用型定義
ts
// features/auth/types.ts
export type AuthProvider = 'facebook' | 'instagram';
export interface User {
id: string;
name: string;
email: string;
provider: AuthProvider;
}
export interface AuthState {
user: User | null;
isAuthenticated: boolean;
isLoading: boolean;
}認証基盤 (NextAuth.js)
セッション管理は NextAuth.js の SessionProvider に委譲する。アプリ独自の AuthContext は持たず、useAuth() は useSession() の薄いラッパーとして提供する。
NextAuth プロバイダー設定
ts
// auth.config.ts
import type { NextAuthConfig } from 'next-auth';
import Facebook from 'next-auth/providers/facebook';
import Instagram from 'next-auth/providers/instagram';
export const authConfig: NextAuthConfig = {
providers: [
Facebook({
clientId: process.env.FACEBOOK_CLIENT_ID!,
clientSecret: process.env.FACEBOOK_CLIENT_SECRET!,
}),
Instagram({
clientId: process.env.INSTAGRAM_CLIENT_ID!,
clientSecret: process.env.INSTAGRAM_CLIENT_SECRET!,
}),
],
};ts
// app/api/auth/[...nextauth]/route.ts
import NextAuth from 'next-auth';
import { authConfig } from '@/auth.config';
export const { handlers: { GET, POST } } = NextAuth(authConfig);SessionProvider ラッパー
tsx
// app/providers.tsx
'use client';
import { SessionProvider } from 'next-auth/react';
import type { ReactNode } from 'react';
export function Providers({ children }: { children: ReactNode }) {
return <SessionProvider>{children}</SessionProvider>;
}useAuth フック
ts
// features/auth/hooks/useAuth.ts
'use client';
import { signIn, signOut, useSession } from 'next-auth/react';
import type { AuthProvider } from '../types';
export function useAuth() {
const { data: session, status } = useSession();
return {
user: session?.user ?? null,
isAuthenticated: status === 'authenticated',
isLoading: status === 'loading',
login: (provider: AuthProvider) => signIn(provider),
logout: () => signOut(),
};
}カスタムフック
useLoginFlow()
ログインモーダルの開閉と認証後コールバックを管理する。
ts
type LoginCallback = () => void;
export function useLoginFlow() {
const { isAuthenticated, login } = useAuth();
const [isOpen, setIsOpen] = useState(false);
const [pendingCallback, setPendingCallback] = useState<LoginCallback | null>(null);
const open = (onSuccess?: LoginCallback) => {
if (isAuthenticated) {
onSuccess?.();
return;
}
setPendingCallback(() => onSuccess);
setIsOpen(true);
};
const close = () => {
setIsOpen(false);
setPendingCallback(null);
};
const handleProviderLogin = async (provider: AuthProvider) => {
await login(provider);
setIsOpen(false);
pendingCallback?.();
setPendingCallback(null);
};
return {
isOpen,
open,
close,
handleProviderLogin,
};
}コンポーネント (UI層)
tsx
// components/modals/SocialLoginModal.tsx
'use client';
interface SocialLoginModalProps {
isOpen: boolean;
isLoading?: boolean;
onClose: () => void;
onSelectProvider: (provider: 'facebook' | 'instagram') => void;
}
export function SocialLoginModal({ isOpen, isLoading, onClose, onSelectProvider }: SocialLoginModalProps) {
// 表示専用、ロジックは持たない
}重要: コンポーネントは useAuth() を 直接呼ばない。propsで onSelectProvider を受け取り、上位(containers/ または app/)で useLoginFlow().handleProviderLogin と紐付ける。
Props 詳細
| プロパティ | 型 | 必須 | 説明 |
|---|---|---|---|
isOpen | boolean | ✓ | モーダルの開閉状態 |
isLoading | boolean | – | ログイン処理中フラグ。trueの場合プロバイダーボタンを無効化 |
onClose | () => void | ✓ | モーダルを閉じる際に発火 |
onSelectProvider | (provider: 'facebook' | 'instagram') => void | ✓ | プロバイダー選択時に発火 |
アクセシビリティ実装
- Radix UI の
Dialogコンポーネントをベースにする:@radix-ui/react-dialogまたは shadcn/ui の Dialog を使用- フォーカストラップ(モーダル外にフォーカスが逃げない)が標準提供
- Escキーでの閉じる動作が標準提供
aria-modal,role="dialog"などの ARIA 属性が自動設定
- プロバイダーボタンに
aria-labelを付与: 「Facebookでログイン」「Instagramでログイン」など、視覚情報に依存せずスクリーンリーダーで判別可能にする - ローディング中は
aria-busy="true"をモーダル要素に付与する
統合例
tsx
// app/candidates/[id]/_components/CandidateDetailClient.tsx ('use client')
const loginFlow = useLoginFlow();
const voteFlow = useVoteFlow();
const handleFreeVote = (candidate: Candidate) => {
loginFlow.open(() => voteFlow.executeFreeVote(candidate));
};
return (
<>
<VoteActions candidate={candidate} onFreeVote={handleFreeVote} />
<SocialLoginModal
isOpen={loginFlow.isOpen}
onClose={loginFlow.close}
onSelectProvider={loginFlow.handleProviderLogin}
/>
</>
);エラーハンドリング
| シナリオ | 対応 |
|---|---|
| OAuth失敗 | sonner Toast「ログインに失敗しました」 |
| ユーザーがOAuth画面でキャンセル | モーダルを閉じ、pendingCallback 破棄 |
| ネットワークエラー | リトライ可能な状態でフォーム再表示 |
セキュリティ
サイト全体の共通セキュリティポリシーは steering/tech.md の「セキュリティポリシー(全画面共通)」を参照。 ソーシャルログインに固有の補足:
- NextAuth.js (Auth.js) を採用: PKCE / state検証 / HttpOnly Cookie / CSRF対策が標準提供される
- プロバイダー設定: Facebook / Instagram の OAuth クライアントID/シークレットは Vercel 環境変数で管理(
FACEBOOK_CLIENT_ID/SECRET,INSTAGRAM_CLIENT_ID/SECRET)
関連
requirements.md/ui-design.md../voting-flow/design.md— pendingVote との連携