😀

Flutter✖️Firebaseで認証メールを用いたアカウント作成機能を実装する方法

2024/09/29に公開

この記事では、「Flutter✖️Firebaseで認証メールを用いたアカウント作成機能を実装する方法」について解説したいと思います。

なお、Flutter(Dart)の基礎知識があり、FirebaseとFlutterプロジェクトを連携している前提で話を進めていきたいと思います。

Flutterを学んだことがない方は、以下の書籍がおすすめです。
動かして学ぶ!Flutter開発入門

ログインページのUIを作成

まずは、ログインページのUIを作成を作成します。

Flutterプロジェクトのlib/login.dartファイルを作成して、LoginPageクラスを作成して下さい。

login.dart
import 'package:flutter/material.dart';
import 'package:flutter/gestures.dart';
import 'create_account.dart'; // 後に作成

class LoginPage extends StatefulWidget {

  const LoginPage({super.key});

  
  LoginPageState createState() => LoginPageState();
}

class LoginPageState extends State<LoginPage> {
  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();
  bool _showVerificationMessage = false;

  
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color(0xFFF5F5F5),
      appBar: AppBar(
        title: const Text('ログイン', style: TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold)),
        backgroundColor: Colors.blue,
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: [
            TextField(
              controller: _emailController,
              decoration: const InputDecoration(
                labelText: 'メールアドレス',
                border: OutlineInputBorder(),
              ),
            ),
            const SizedBox(height: 16),
            TextField(
              controller: _passwordController,
              obscureText: true,
              decoration: const InputDecoration(
                labelText: 'パスワード',
                border: OutlineInputBorder(),
              ),
            ),
            const SizedBox(height: 16),
            ElevatedButton(
              onPressed: _login, // 後に作成
              style: ElevatedButton.styleFrom(
                backgroundColor: Colors.blue,
                minimumSize: const Size(double.infinity, 50),
              ),
              child: const Text('ログイン', style: TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold)),
            ),
            Text.rich(
              TextSpan(
                children: [
                  const TextSpan(text: 'アカウント未登録の方は'),
                  TextSpan(
                    text: 'こちら',
                    style: const TextStyle(
                      color: Colors.blue,
                      decoration: TextDecoration.underline,
                    ),
                    recognizer: TapGestureRecognizer()..onTap = () async {
                      final result = await Navigator.push(
                        context,
                        MaterialPageRoute(builder: (context) => const CreateAccountPage()),
                      );
                      if (result == true) {
                        setState(() {
                          _showVerificationMessage = true;
                        });
                      }
                    },
                  ),
                ],
              ),
            ),
            const SizedBox(height: 8),
            Text.rich(
              TextSpan(
                children: [
                  const TextSpan(text: 'パスワードを忘れた方は'),
                  TextSpan(
                    text: 'こちら',
                    style: const TextStyle(
                      color: Colors.blue,
                      decoration: TextDecoration.underline,
                    ),
                    recognizer: TapGestureRecognizer()..onTap = () {
                      Navigator.push(

                      );
                    },
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }

 
  void dispose() {
    _emailController.dispose();
    _passwordController.dispose();
    super.dispose();
  }
}

ログインページに必要なパーツを作成しました。ログインボタンを押した時に呼び出す_login関数やパスワードリセット機能は後に作成します。

続いて、ログインするにはアカウントを作成する必要があるので、'アカウント未登録の方はこちら'を押した時に遷移するアカウント作成ページを作成します。

アカウント作成ページのUIを作成

アカウントページのUIを作成します。

Flutterプロジェクトのlib/create_account.dartファイルを作成して、CreateAccountPageクラスを作成して下さい。

create_account.dart
import 'package:flutter/material.dart';

class CreateAccountPage extends StatefulWidget {
  const CreateAccountPage({super.key});

  
  CreateAccountPageState createState() => CreateAccountPageState();
}

class CreateAccountPageState extends State<CreateAccountPage> {

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('アカウント作成',
            style: TextStyle(
                color: Colors.white,
                fontSize: 16,
                fontWeight: FontWeight.bold)),
        backgroundColor: Colors.blue,
      ),
    );
  }

}

上記のコードでは、StatefulWidgetを継承してCreateAccountPageクラスを作成し、アップバーを設定しています。

画面上には以下の写真の様に表示されています。

続いて、アカウントを作成するために必要なパーツを作成します。Scaffoldウィジェットbodyパラメーターに以下のコードを作成して下さい。

create_account.dart
// 追加
import 'package:flutter/gestures.dart';
import 'package:url_launcher/url_launcher.dart';

// 省略

class CreateAccountPageState extends State<CreateAccountPage> {
  // 追加
  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();
  final _confirmPasswordController = TextEditingController();
  final _usernameController = TextEditingController();
  bool _agreeToTerms = false;

  
  Widget build(BuildContext context) {
    return Scaffold(
    // 省略
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: [
            TextField(
              controller: _emailController,
              decoration: const InputDecoration(
                labelText: 'メールアドレス',
                border: OutlineInputBorder(),
              ),
            ),
            const SizedBox(height: 16),
            TextField(
              controller: _passwordController,
              decoration: const InputDecoration(
                labelText: 'パスワード(英数字6文字以上)',
                border: OutlineInputBorder(),
              ),
              obscureText: true,
            ),
            const SizedBox(height: 16),
            TextField(
              controller: _confirmPasswordController,
              decoration: const InputDecoration(
                labelText: 'パスワード(確認用)',
                border: OutlineInputBorder(),
              ),
              obscureText: true,
            ),
            const SizedBox(height: 16),
            TextField(
              controller: _usernameController,
              decoration: const InputDecoration(
                labelText: 'ユーザー名',
                border: OutlineInputBorder(),
              ),
            ),
            const SizedBox(height: 16),
            Container(
              decoration: BoxDecoration(
                border: Border.all(color: Colors.blue, width: 2),
                borderRadius: BorderRadius.circular(8),
              ),
              child: Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Checkbox(
                    value: _agreeToTerms,
                    onChanged: (value) {
                      setState(() {
                        _agreeToTerms = value!;
                      });
                    },
                  ),
                  Text.rich(
                    TextSpan(
                      children: [
                        TextSpan(
                          text: '利用規約',
                          style: const TextStyle(
                            color: Colors.blue,
                            decoration: TextDecoration.underline,
                          ),
                          recognizer: TapGestureRecognizer()
                            ..onTap = () async {
                              const url = '利用規約ページのURL';
                              if (await canLaunch(url)) {
                                await launch(url);
                              } else {
                                ScaffoldMessenger.of(context).showSnackBar(
                                  const SnackBar(content: Text('URLを開けませんでした')),
                                );
                              }
                            },
                        ),
                        const TextSpan(text: 'を確認の上、同意する'),
                      ],
                    ),
                  ),
                ],
              ),
            ),
            const SizedBox(height: 8),
            Text.rich(
              TextSpan(
                children: [
                  TextSpan(
                    text: 'プライバシーポリシー',
                    style: const TextStyle(
                      color: Colors.blue,
                      decoration: TextDecoration.underline,
                    ),
                    recognizer: TapGestureRecognizer()
                      ..onTap = () async {
                        const url = 'プライバシーポリシーページのURL';
                        if (await canLaunch(url)) {
                        await launch(url);
                      } else {
                        ScaffoldMessenger.of(context).showSnackBar(
                          const SnackBar(content: Text('URLを開けませんでした')),
                        );
                      }
                    },
                  ),
                  const TextSpan(text: 'を読む'),
                ],
              ),
            ),
            const SizedBox(height: 16),
            ElevatedButton(
              onPressed: _createAccount, // 後に作成
              style: ElevatedButton.styleFrom(
                backgroundColor: Colors.blue,
                minimumSize: const Size(double.infinity, 50),
              ),
              child: const Text('登録', style: TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold)),
            ),
          ],
        ),
      ),
    );
  }

 
  void dispose() {
    _emailController.dispose();
    _passwordController.dispose();
    _confirmPasswordController.dispose();
    _usernameController.dispose();
    super.dispose();
  }
}

メールアドレス、パスワード、パスワード確認用、ユーザー名のテキストフィールドを作成します。

また、利用規約確認用のチャックボックスとプライバシーポリシーのテキストリンクも設定しておくといいと思います。

自身が作成した利用規約、プライバシーポリシーページに遷移するためには、pubspec.yamlurl_launcherをインポートする必要があります。

アカウント作成機能

続いて、登録ボタンを押した時に呼び出す_createAccount関数を作成します。

create_account.dart
// 追加
import 'package:firebase_auth/firebase_auth.dart';
import 'package:cloud_firestore/cloud_firestore.dart';

// 省略

class CreateAccountPageState extends State<CreateAccountPage> {

 // 省略
  
 Future<void> _createAccount() async {
    if (_passwordController.text != _confirmPasswordController.text) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('パスワードが一致しません')),
      );
      return;
    }

    if (!_agreeToTerms) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('利用規約に同意してください')),
      );
      return;
    }

    try {
      UserCredential userCredential =
          await FirebaseAuth.instance.createUserWithEmailAndPassword(
        email: _emailController.text.trim(),
        password: _passwordController.text.trim(),
      );

      String uid = userCredential.user!.uid;
      String username = _usernameController.text.trim();

      await FirebaseFirestore.instance.collection('users').doc(uid).set({
        'username': username,
        'email': _emailController.text.trim(),
        'createdAt': FieldValue.serverTimestamp(),
        'profileImage': null,
      });

      await userCredential.user?.updateDisplayName(username);
      await userCredential.user?.sendEmailVerification();

      Navigator.pop(context, true);
    } on FirebaseAuthException catch (e) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('アカウント作成に失敗しました: ${e.message}')),
      );
    } catch (e) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('予期せぬエラーが発生しました: $e')),
      );
    }
  }
}

// 省略

パスワードが一致しており、利用規約がチェックされていれば、Firebaseにユーザーの情報を保存します。そして、設定したメールアドレス認証メールを送って、ログインページに戻ります。

ログイン機能を作成

login.dartでログイン機能を作成します。

login.dart
import 'create_account.dart'; // 追加

class LoginPageState extends State<LoginPage> {

  // 追加
  Future<void> _login() async {
    try {
      UserCredential userCredential = await FirebaseAuth.instance.signInWithEmailAndPassword(
        email: _emailController.text.trim(),
        password: _passwordController.text.trim(),
      );
      
      if (userCredential.user != null && !userCredential.user!.emailVerified) {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text('メールアドレスの認証が完了していません。認証メールのリンクをクリックしてください。')),
        );
        await FirebaseAuth.instance.signOut();
        return;
      }
      
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('ログインに成功しました')),
      );
      Navigator.of(context).pushReplacement(
        // ログイン完了時に遷移するページを設定
      );
    } on FirebaseAuthException catch (e) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('ログインに失敗しました: ${e.message}')),
      );
    }
  }


 
  Widget build(BuildContext context) {
    return Scaffold(
    // 省略
    ElevatedButton(
              onPressed: _login,
              style: ElevatedButton.styleFrom(
                backgroundColor: Colors.blue,
                minimumSize: const Size(double.infinity, 50),
              ),
              child: const Text('ログイン', style: TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold)),
            ),
            if (_showVerificationMessage)
              Container(
                margin: const EdgeInsets.only(top: 16),
                padding: const EdgeInsets.all(12),
                decoration: BoxDecoration(
                  color: Colors.red[100],
                  border: Border.all(color: Colors.red),
                  borderRadius: BorderRadius.circular(4),
                ),
                child: const Text(
                  '認証メールを送信しています。メール内に記載されたURLをクリックし、認証を完了してください。\n※ 「迷惑メールボックス」に入っている場合もあります。',
                  style: TextStyle(color: Colors.red),
                ),
              ),
  );
}

アカウント作成後、認証メールを送ったことを伝える文をログインボタンの下に表示します。

_login関数は、ユーザーが認証メールのリンクをクリックしたらログインできる様な機能になっています。

ログイン後に遷移するページは各自で設定して下さい。

パスワードリセット機能を作成

最後に、パスワードリセット機能を作成します。passward_reset.dartを作成して、以下のコードを実装して下さい。

password_reset.dart
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';

class PasswordResetPage extends StatefulWidget {
  const PasswordResetPage({Key? key}) : super(key: key);

  
  _PasswordResetPageState createState() => _PasswordResetPageState();
}

class _PasswordResetPageState extends State<PasswordResetPage> {
  final TextEditingController _emailController = TextEditingController();

  Future<void> _resetPassword() async {
    try {
      await FirebaseAuth.instance.sendPasswordResetEmail(
        email: _emailController.text.trim(),
      );
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('パスワードリセットメールを送信しました')),
      );
    } on FirebaseAuthException catch (e) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('エラー: ${e.message}')),
      );
    }
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('パスワードリセット', style: TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold)),
        backgroundColor: Colors.blue,
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            TextField(
              controller: _emailController,
              decoration: const InputDecoration(
                labelText: '登録メールアドレス',
                border: OutlineInputBorder(),
              ),
              keyboardType: TextInputType.emailAddress,
            ),
            const SizedBox(height: 16),
            ElevatedButton(
              onPressed: _resetPassword,
              style: ElevatedButton.styleFrom(
                backgroundColor: Colors.blue,
                padding: const EdgeInsets.symmetric(vertical: 16),
              ),
              child: const Text('パスワードをリセット', style: TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold)),
            ),
            const SizedBox(height: 20),
            const Text(
              '登録されたメールアドレスにメールが届きます',
              textAlign: TextAlign.center,
              style: TextStyle(color: Colors.grey, fontWeight: FontWeight.bold),
            ),
          ],
        ),
      ),
    );
  }
}

登録したメールアドレスにリセットメールが送られて、パスワードを再設定できます。

login.dartでpassword_reset.dartに遷移する機能を実装すれば終わりです。

login.dart
import 'password_reset.dart'; // 追加

class LoginPageState extends State<LoginPage> {

  
  Widget build(BuildContext context) {
    return Scaffold(
    // 省略
    Text.rich(
              TextSpan(
                children: [
                  const TextSpan(text: 'パスワードを忘れた方は'),
                  TextSpan(
                    text: 'こちら',
                    style: const TextStyle(
                      color: Colors.blue,
                      decoration: TextDecoration.underline,
                    ),
                    recognizer: TapGestureRecognizer()..onTap = () {
                      Navigator.push(
                        context,
                        MaterialPageRoute(builder: (context) => const PasswordResetPage()),
                      );
                    },
                  ),
                ],
              ),
            ),
  );
}

Discussion