Firebase Auth (Dart/Flutter)
認証ライブラリ
Firebase Auth (Dart/Flutter)
概要
Firebase Auth for Flutterは、Googleが提供するFirebaseプラットフォームのFlutter向け認証SDKです。2025年現在、FlutterFireエコシステムの中核を成し、iOS、Android、Web、デスクトップアプリケーション向けに統一された認証体験を提供しています。メール・パスワード認証、OAuth(Google、Facebook、Apple等)、匿名認証、電話番号認証など、豊富な認証方式をサポートし、Firebase Consoleとの連携により企業レベルのユーザー管理機能を提供します。
詳細
Firebase Auth for Flutterは、クロスプラットフォーム認証ソリューションの決定版です。主な特徴:
- マルチプラットフォーム対応: iOS、Android、Web、macOS、Windows、Linuxでの統一API
- 豊富な認証方式: メール・パスワード、OAuth、匿名、電話番号、カスタムトークン認証
- リアルタイム状態管理: authStateChanges、userChanges、idTokenChangesストリーム
- セキュリティ機能: 多要素認証、メール認証、パスワードリセット機能
- オフライン対応: 自動的な認証状態の永続化とオフライン機能
- Firebase統合: Firestore、Cloud Functions、Analytics等との完全統合
メリット・デメリット
メリット
- Googleが提供する企業グレードの認証基盤で信頼性が高い
- クロスプラットフォーム対応でコードの再利用性が抜群
- Firebase Consoleによる包括的なユーザー管理とアナリティクス
- リアルタイム認証状態管理とリアクティブなUI更新
- 豊富なOAuth プロバイダーとの統合が簡単
- 自動的なトークン管理とリフレッシュ機能
デメリット
- Firebaseエコシステムに依存するベンダーロックイン
- カスタム認証ロジックの実装に制約がある
- Firebaseの利用料金が発生(無料枠あり)
- 複雑な企業認証要件には対応が困難な場合がある
- Googleアカウントやインターネット接続が必要な機能がある
参考ページ
書き方の例
基本的なセットアップとインストール
# Firebase CLI のインストール
npm install -g firebase-tools
# Firebase にログイン
firebase login
# Flutter プロジェクトでFirebase を設定
flutter pub add firebase_core
flutter pub add firebase_auth
# FlutterFire CLI で設定
dart pub global activate flutterfire_cli
flutterfire configure
# プロジェクトの再ビルド
flutter run
Firebase初期化とメール・パスワード認証
// main.dart - Firebase初期化
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'firebase_options.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Firebase 初期化
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
// エミュレーター使用時(開発環境)
if (kDebugMode) {
await FirebaseAuth.instance.useAuthEmulator('localhost', 9099);
}
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Firebase Auth Demo',
home: AuthWrapper(),
);
}
}
// auth_wrapper.dart - 認証状態に基づく画面切り替え
class AuthWrapper extends StatelessWidget {
@override
Widget build(BuildContext context) {
return StreamBuilder<User?>(
stream: FirebaseAuth.instance.authStateChanges(),
builder: (context, snapshot) {
// 認証状態の確認
if (snapshot.connectionState == ConnectionState.waiting) {
return Scaffold(
body: Center(child: CircularProgressIndicator()),
);
} else if (snapshot.hasData && snapshot.data != null) {
// ログイン済み
return HomePage();
} else {
// 未ログイン
return LoginPage();
}
},
);
}
}
// login_page.dart - ログインページ
class LoginPage extends StatefulWidget {
@override
_LoginPageState createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
final _formKey = GlobalKey<FormState>();
bool _isLoading = false;
// メール・パスワードでのユーザー登録
Future<void> _registerWithEmailPassword() async {
if (!_formKey.currentState!.validate()) return;
setState(() => _isLoading = true);
try {
final credential = await FirebaseAuth.instance
.createUserWithEmailAndPassword(
email: _emailController.text.trim(),
password: _passwordController.text,
);
// 登録後にメール認証を送信
await credential.user?.sendEmailVerification();
_showMessage('登録成功!認証メールを確認してください。');
} on FirebaseAuthException catch (e) {
String message;
switch (e.code) {
case 'weak-password':
message = 'パスワードが弱すぎます。';
break;
case 'email-already-in-use':
message = 'このメールアドレスは既に使用されています。';
break;
case 'invalid-email':
message = '無効なメールアドレスです。';
break;
default:
message = '登録に失敗しました: ${e.message}';
}
_showMessage(message);
} catch (e) {
_showMessage('予期しないエラーが発生しました: $e');
} finally {
setState(() => _isLoading = false);
}
}
// メール・パスワードでのログイン
Future<void> _signInWithEmailPassword() async {
if (!_formKey.currentState!.validate()) return;
setState(() => _isLoading = true);
try {
await FirebaseAuth.instance.signInWithEmailAndPassword(
email: _emailController.text.trim(),
password: _passwordController.text,
);
} on FirebaseAuthException catch (e) {
String message;
switch (e.code) {
case 'user-not-found':
message = 'ユーザーが見つかりません。';
break;
case 'wrong-password':
message = 'パスワードが間違っています。';
break;
case 'invalid-email':
message = '無効なメールアドレスです。';
break;
case 'user-disabled':
message = 'このアカウントは無効化されています。';
break;
default:
message = 'ログインに失敗しました: ${e.message}';
}
_showMessage(message);
} finally {
setState(() => _isLoading = false);
}
}
void _showMessage(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message)),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('ログイン')),
body: Padding(
padding: EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextFormField(
controller: _emailController,
decoration: InputDecoration(
labelText: 'メールアドレス',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.emailAddress,
validator: (value) {
if (value == null || value.isEmpty) {
return 'メールアドレスを入力してください';
}
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$')
.hasMatch(value)) {
return '有効なメールアドレスを入力してください';
}
return null;
},
),
SizedBox(height: 16),
TextFormField(
controller: _passwordController,
decoration: InputDecoration(
labelText: 'パスワード',
border: OutlineInputBorder(),
),
obscureText: true,
validator: (value) {
if (value == null || value.isEmpty) {
return 'パスワードを入力してください';
}
if (value.length < 6) {
return 'パスワードは6文字以上で入力してください';
}
return null;
},
),
SizedBox(height: 24),
if (_isLoading)
CircularProgressIndicator()
else ...[
ElevatedButton(
onPressed: _signInWithEmailPassword,
style: ElevatedButton.styleFrom(
minimumSize: Size(double.infinity, 50),
),
child: Text('ログイン'),
),
SizedBox(height: 16),
TextButton(
onPressed: _registerWithEmailPassword,
child: Text('新規登録'),
),
TextButton(
onPressed: _resetPassword,
child: Text('パスワードを忘れた場合'),
),
],
],
),
),
),
);
}
// パスワードリセット
Future<void> _resetPassword() async {
final email = _emailController.text.trim();
if (email.isEmpty) {
_showMessage('メールアドレスを入力してください');
return;
}
try {
await FirebaseAuth.instance.sendPasswordResetEmail(email: email);
_showMessage('パスワードリセットメールを送信しました');
} on FirebaseAuthException catch (e) {
_showMessage('エラー: ${e.message}');
}
}
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
}
OAuth認証(Google、Apple等)の実装
// oauth_service.dart - OAuth認証サービス
import 'package:google_sign_in/google_sign_in.dart';
import 'package:flutter_facebook_auth/flutter_facebook_auth.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/foundation.dart';
class OAuthService {
// Google サインイン
static Future<UserCredential?> signInWithGoogle() async {
try {
if (kIsWeb) {
// Web版Google認証
GoogleAuthProvider googleProvider = GoogleAuthProvider();
googleProvider.addScope('https://www.googleapis.com/auth/contacts.readonly');
googleProvider.setCustomParameters({
'login_hint': '[email protected]'
});
return await FirebaseAuth.instance.signInWithPopup(googleProvider);
} else {
// ネイティブ版Google認証
final GoogleSignInAccount? googleUser = await GoogleSignIn().signIn();
if (googleUser == null) return null;
final GoogleSignInAuthentication googleAuth =
await googleUser.authentication;
final credential = GoogleAuthProvider.credential(
accessToken: googleAuth.accessToken,
idToken: googleAuth.idToken,
);
return await FirebaseAuth.instance.signInWithCredential(credential);
}
} catch (e) {
print('Google サインインエラー: $e');
return null;
}
}
// Facebook サインイン
static Future<UserCredential?> signInWithFacebook() async {
try {
if (kIsWeb) {
// Web版Facebook認証
FacebookAuthProvider facebookProvider = FacebookAuthProvider();
facebookProvider.addScope('email');
facebookProvider.setCustomParameters({'display': 'popup'});
return await FirebaseAuth.instance.signInWithPopup(facebookProvider);
} else {
// ネイティブ版Facebook認証
final LoginResult loginResult = await FacebookAuth.instance.login();
if (loginResult.status != LoginStatus.success) return null;
final OAuthCredential facebookAuthCredential =
FacebookAuthProvider.credential(loginResult.accessToken!.token);
return await FirebaseAuth.instance
.signInWithCredential(facebookAuthCredential);
}
} catch (e) {
print('Facebook サインインエラー: $e');
return null;
}
}
// Apple サインイン
static Future<UserCredential?> signInWithApple() async {
try {
final appleProvider = AppleAuthProvider();
appleProvider.addScope('email');
appleProvider.addScope('name');
if (kIsWeb) {
return await FirebaseAuth.instance.signInWithPopup(appleProvider);
} else {
return await FirebaseAuth.instance.signInWithProvider(appleProvider);
}
} catch (e) {
print('Apple サインインエラー: $e');
return null;
}
}
// 匿名サインイン
static Future<UserCredential?> signInAnonymously() async {
try {
return await FirebaseAuth.instance.signInAnonymously();
} on FirebaseAuthException catch (e) {
switch (e.code) {
case "operation-not-allowed":
print("匿名認証が有効化されていません");
break;
default:
print("匿名認証エラー: ${e.message}");
}
return null;
}
}
// サインアウト
static Future<void> signOut() async {
await GoogleSignIn().signOut();
await FacebookAuth.instance.logOut();
await FirebaseAuth.instance.signOut();
}
}
// oauth_buttons.dart - OAuth認証ボタンウィジェット
class OAuthButtons extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: [
// Google サインインボタン
ElevatedButton.icon(
onPressed: () async {
final result = await OAuthService.signInWithGoogle();
if (result != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Googleでログインしました')),
);
}
},
icon: Icon(Icons.login),
label: Text('Googleでサインイン'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
minimumSize: Size(double.infinity, 50),
),
),
SizedBox(height: 12),
// Facebook サインインボタン
ElevatedButton.icon(
onPressed: () async {
final result = await OAuthService.signInWithFacebook();
if (result != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Facebookでログインしました')),
);
}
},
icon: Icon(Icons.facebook),
label: Text('Facebookでサインイン'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue[800],
foregroundColor: Colors.white,
minimumSize: Size(double.infinity, 50),
),
),
SizedBox(height: 12),
// Apple サインインボタン(iOS/Web)
if (Theme.of(context).platform == TargetPlatform.iOS || kIsWeb)
ElevatedButton.icon(
onPressed: () async {
final result = await OAuthService.signInWithApple();
if (result != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Appleでログインしました')),
);
}
},
icon: Icon(Icons.apple),
label: Text('Appleでサインイン'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.black,
foregroundColor: Colors.white,
minimumSize: Size(double.infinity, 50),
),
),
SizedBox(height: 12),
// 匿名サインインボタン
TextButton.icon(
onPressed: () async {
final result = await OAuthService.signInAnonymously();
if (result != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('匿名でログインしました')),
);
}
},
icon: Icon(Icons.person_outline),
label: Text('匿名でサインイン'),
),
],
);
}
}
ユーザー情報管理とプロファイル更新
// user_profile_page.dart - ユーザープロファイル管理
class UserProfilePage extends StatefulWidget {
@override
_UserProfilePageState createState() => _UserProfilePageState();
}
class _UserProfilePageState extends State<UserProfilePage> {
final _displayNameController = TextEditingController();
final _currentPasswordController = TextEditingController();
final _newPasswordController = TextEditingController();
bool _isLoading = false;
@override
void initState() {
super.initState();
_loadUserData();
}
void _loadUserData() {
final user = FirebaseAuth.instance.currentUser;
if (user != null) {
_displayNameController.text = user.displayName ?? '';
}
}
// プロフィール更新
Future<void> _updateProfile() async {
final user = FirebaseAuth.instance.currentUser;
if (user == null) return;
setState(() => _isLoading = true);
try {
await user.updateDisplayName(_displayNameController.text);
await user.reload();
_showMessage('プロフィールを更新しました');
} catch (e) {
_showMessage('更新に失敗しました: $e');
} finally {
setState(() => _isLoading = false);
}
}
// パスワード変更
Future<void> _changePassword() async {
final user = FirebaseAuth.instance.currentUser;
if (user == null) return;
if (_currentPasswordController.text.isEmpty ||
_newPasswordController.text.isEmpty) {
_showMessage('現在のパスワードと新しいパスワードを入力してください');
return;
}
setState(() => _isLoading = true);
try {
// 再認証
final credential = EmailAuthProvider.credential(
email: user.email!,
password: _currentPasswordController.text,
);
await user.reauthenticateWithCredential(credential);
// パスワード更新
await user.updatePassword(_newPasswordController.text);
_currentPasswordController.clear();
_newPasswordController.clear();
_showMessage('パスワードを変更しました');
} on FirebaseAuthException catch (e) {
String message;
switch (e.code) {
case 'wrong-password':
message = '現在のパスワードが間違っています';
break;
case 'weak-password':
message = '新しいパスワードが弱すぎます';
break;
default:
message = 'パスワード変更に失敗しました: ${e.message}';
}
_showMessage(message);
} finally {
setState(() => _isLoading = false);
}
}
// メール認証送信
Future<void> _sendEmailVerification() async {
final user = FirebaseAuth.instance.currentUser;
if (user == null) return;
try {
await user.sendEmailVerification();
_showMessage('認証メールを送信しました');
} catch (e) {
_showMessage('送信に失敗しました: $e');
}
}
// アカウント削除
Future<void> _deleteAccount() async {
final confirmed = await _showConfirmDialog(
'アカウント削除',
'アカウントを削除すると、すべてのデータが失われます。\n本当に削除しますか?',
);
if (!confirmed) return;
final user = FirebaseAuth.instance.currentUser;
if (user == null) return;
try {
await user.delete();
_showMessage('アカウントを削除しました');
} on FirebaseAuthException catch (e) {
if (e.code == 'requires-recent-login') {
_showMessage('セキュリティのため、再ログインしてからもう一度お試しください');
} else {
_showMessage('削除に失敗しました: ${e.message}');
}
}
}
Future<bool> _showConfirmDialog(String title, String message) async {
return await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text(title),
content: Text(message),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: Text('キャンセル'),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
child: Text('削除'),
style: TextButton.styleFrom(foregroundColor: Colors.red),
),
],
),
) ?? false;
}
void _showMessage(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message)),
);
}
@override
Widget build(BuildContext context) {
final user = FirebaseAuth.instance.currentUser;
return Scaffold(
appBar: AppBar(
title: Text('プロフィール'),
actions: [
IconButton(
onPressed: () async {
await OAuthService.signOut();
},
icon: Icon(Icons.logout),
),
],
),
body: user == null
? Center(child: Text('ユーザーが見つかりません'))
: SingleChildScrollView(
padding: EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// ユーザー情報表示
Card(
child: Padding(
padding: EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('基本情報', style: Theme.of(context).textTheme.headlineSmall),
SizedBox(height: 16),
if (user.photoURL != null)
CircleAvatar(
radius: 40,
backgroundImage: NetworkImage(user.photoURL!),
),
SizedBox(height: 16),
Text('メールアドレス: ${user.email ?? "未設定"}'),
SizedBox(height: 8),
Text('UID: ${user.uid}'),
SizedBox(height: 8),
Row(
children: [
Text('メール認証: '),
Icon(
user.emailVerified ? Icons.check_circle : Icons.cancel,
color: user.emailVerified ? Colors.green : Colors.red,
),
if (!user.emailVerified) ...[
SizedBox(width: 8),
TextButton(
onPressed: _sendEmailVerification,
child: Text('認証メール送信'),
),
],
],
),
SizedBox(height: 16),
TextField(
controller: _displayNameController,
decoration: InputDecoration(
labelText: '表示名',
border: OutlineInputBorder(),
),
),
SizedBox(height: 16),
ElevatedButton(
onPressed: _isLoading ? null : _updateProfile,
child: _isLoading
? CircularProgressIndicator()
: Text('プロフィール更新'),
),
],
),
),
),
SizedBox(height: 16),
// パスワード変更セクション
if (user.providerData.any((info) => info.providerId == 'password'))
Card(
child: Padding(
padding: EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('パスワード変更',
style: Theme.of(context).textTheme.headlineSmall),
SizedBox(height: 16),
TextField(
controller: _currentPasswordController,
decoration: InputDecoration(
labelText: '現在のパスワード',
border: OutlineInputBorder(),
),
obscureText: true,
),
SizedBox(height: 16),
TextField(
controller: _newPasswordController,
decoration: InputDecoration(
labelText: '新しいパスワード',
border: OutlineInputBorder(),
),
obscureText: true,
),
SizedBox(height: 16),
ElevatedButton(
onPressed: _isLoading ? null : _changePassword,
child: Text('パスワード変更'),
),
],
),
),
),
SizedBox(height: 16),
// アカウント削除セクション
Card(
child: Padding(
padding: EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('危険な操作',
style: Theme.of(context).textTheme.headlineSmall),
SizedBox(height: 16),
ElevatedButton(
onPressed: _deleteAccount,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
),
child: Text('アカウント削除'),
),
],
),
),
),
],
),
),
);
}
@override
void dispose() {
_displayNameController.dispose();
_currentPasswordController.dispose();
_newPasswordController.dispose();
super.dispose();
}
}
認証状態管理と高度な機能
// auth_state_service.dart - 認証状態管理サービス
class AuthStateService {
static final FirebaseAuth _auth = FirebaseAuth.instance;
// 現在のユーザーを取得
static User? get currentUser => _auth.currentUser;
// 認証状態変更ストリーム
static Stream<User?> get authStateChanges => _auth.authStateChanges();
// ユーザー変更ストリーム(プロフィール変更も含む)
static Stream<User?> get userChanges => _auth.userChanges();
// IDトークン変更ストリーム
static Stream<User?> get idTokenChanges => _auth.idTokenChanges();
// 認証状態の永続化設定(Web専用)
static Future<void> setPersistence(Persistence persistence) async {
if (kIsWeb) {
await _auth.setPersistence(persistence);
}
}
// 現在のユーザーの詳細情報を取得
static Future<User?> reloadCurrentUser() async {
final user = _auth.currentUser;
if (user != null) {
await user.reload();
return _auth.currentUser;
}
return null;
}
// IDトークンを取得
static Future<String?> getIdToken({bool forceRefresh = false}) async {
final user = _auth.currentUser;
if (user != null) {
return await user.getIdToken(forceRefresh);
}
return null;
}
// カスタムクレームを取得
static Future<Map<String, dynamic>?> getCustomClaims() async {
final user = _auth.currentUser;
if (user != null) {
final idTokenResult = await user.getIdTokenResult();
return idTokenResult.claims;
}
return null;
}
}
// auth_guard.dart - 認証ガードウィジェット
class AuthGuard extends StatelessWidget {
final Widget child;
final Widget? loginPage;
final bool requireEmailVerification;
const AuthGuard({
Key? key,
required this.child,
this.loginPage,
this.requireEmailVerification = false,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return StreamBuilder<User?>(
stream: AuthStateService.authStateChanges,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Scaffold(
body: Center(child: CircularProgressIndicator()),
);
}
final user = snapshot.data;
// ユーザーが存在しない場合
if (user == null) {
return loginPage ?? LoginPage();
}
// メール認証が必要な場合
if (requireEmailVerification && !user.emailVerified) {
return EmailVerificationPage();
}
// 認証済みユーザー
return child;
},
);
}
}
// email_verification_page.dart - メール認証確認ページ
class EmailVerificationPage extends StatefulWidget {
@override
_EmailVerificationPageState createState() => _EmailVerificationPageState();
}
class _EmailVerificationPageState extends State<EmailVerificationPage> {
bool _isLoading = false;
Timer? _timer;
@override
void initState() {
super.initState();
_checkEmailVerification();
}
void _checkEmailVerification() {
_timer = Timer.periodic(Duration(seconds: 3), (timer) async {
await FirebaseAuth.instance.currentUser?.reload();
final user = FirebaseAuth.instance.currentUser;
if (user?.emailVerified == true) {
timer.cancel();
// 認証確認後にページを更新するため、setState を呼び出す
if (mounted) {
setState(() {});
}
}
});
}
Future<void> _resendVerificationEmail() async {
setState(() => _isLoading = true);
try {
await FirebaseAuth.instance.currentUser?.sendEmailVerification();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('認証メールを再送信しました')),
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('送信に失敗しました: $e')),
);
} finally {
setState(() => _isLoading = false);
}
}
@override
Widget build(BuildContext context) {
final user = FirebaseAuth.instance.currentUser;
return Scaffold(
appBar: AppBar(
title: Text('メール認証'),
actions: [
IconButton(
onPressed: () => FirebaseAuth.instance.signOut(),
icon: Icon(Icons.logout),
),
],
),
body: Padding(
padding: EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.mark_email_unread,
size: 100,
color: Colors.orange,
),
SizedBox(height: 24),
Text(
'メール認証が必要です',
style: Theme.of(context).textTheme.headlineSmall,
textAlign: TextAlign.center,
),
SizedBox(height: 16),
Text(
'${user?.email} に送信された認証メールのリンクをクリックしてください。',
style: Theme.of(context).textTheme.bodyLarge,
textAlign: TextAlign.center,
),
SizedBox(height: 32),
ElevatedButton(
onPressed: _isLoading ? null : _resendVerificationEmail,
child: _isLoading
? CircularProgressIndicator()
: Text('認証メールを再送信'),
),
SizedBox(height: 16),
TextButton(
onPressed: () async {
await FirebaseAuth.instance.currentUser?.reload();
setState(() {});
},
child: Text('認証状態を確認'),
),
],
),
),
);
}
@override
void dispose() {
_timer?.cancel();
super.dispose();
}
}