Flutter|Firebaseのエラーメッセージを表示する

Devlog
Devlog

認証のエラーメッセージをユーザーにわかりやすく表示したい

「エラーになるけど原因がわからない」という状態はユーザー体験としてよろしくないなと、日々利用するサービスの中で感じるので、きちんとエラー内容を表示したい。

認証機能で実装してみました。

まずは認証処理を実装

ユーザー認証はFirebaseのパッケージを利用。
https://pub.dev/packages/firebase_auth

Firebaseアカウントを作成し、Authenticationの画面を開きます。ログイン方法は、メール+パスワードと、Google認証、Appleアカウント認証を用意しています。

UI作成用に追加のパッケージをインストール
https://pub.dev/packages/google_sign_in
https://pub.dev/packages/sign_in_button

※参考 こちらを使用しているドキュメントや記事も見かけましたが、私は使用せずに実装できました。https://pub.dev/packages/google_sign_in_ios
https://pub.dev/packages/sign_in_with_apple

サインインの処理関数を用意します。セットでサインアウトとユーザー削除も用意。

※サインアップ=新規登録、サインイン=既存アカウントにログイン の意味で使っています。

Dart
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:google_sign_in/google_sign_in.dart';
import 'package:dayby/infrastructure/analytics/google_analytics_tags.dart';

class AuthService {
  // ------------------------------
  // メールアドレスとパスワード
  // ------------------------------
  Future<void> mailSignUp(String email, String password) async {
    await FirebaseAuth.instance.createUserWithEmailAndPassword(
      email: email,
      password: password,
    );
    await GATags().prcAUmailSignUp();
  }

  Future<void> mailSignIn(String email, String password) async {
    await FirebaseAuth.instance.signInWithEmailAndPassword(
      email: email,
      password: password,
    );
    await GATags().prcAUmailSignIn();
  }

  Future<void> passReset(String email) async {
    await FirebaseAuth.instance.sendPasswordResetEmail(
      email: email,
    );
    await GATags().prcAUpassReset();
  }

  // ------------------------------
  // Googleで新規登録&サインイン
  // ------------------------------
  Future<void> googleSignUpIn() async {
    // アプリが知りたい情報
    const scopes = ['openid', 'profile', 'email'];

    // サインインリクエスト
    final request = GoogleSignIn(scopes: scopes);
    final response = await request.signIn();

    // 戻り値からアクセストークンを取得
    final authn = await response?.authentication;
    final accessToken = authn?.accessToken;

    // アクセストークンがnull=サインイン途中に離脱 処理を抜ける
    if (accessToken == null) return;

    // Firebaseへアクセストークンを送る
    final oAuthCredential =
        GoogleAuthProvider.credential(accessToken: accessToken);
    await FirebaseAuth.instance.signInWithCredential(oAuthCredential);

    await GATags().prcAUgoogleSignUpIn();
  }

  // ------------------------------
  // AppleIDで新規登録&ログイン
  // ------------------------------
  Future<void> appleSignUpIn() async {
    final appleProvider = AppleAuthProvider();
    if (kIsWeb) {
      await FirebaseAuth.instance.signInWithPopup(appleProvider);
    } else {
      await FirebaseAuth.instance.signInWithProvider(appleProvider);
    }
    await GATags().prcAUappleSignUpIn();
  }

  // ------------------------------
  // サインアウト
  // ------------------------------
  Future<void> signOut() async {
    await FirebaseAuth.instance.signOut();
    await GATags().prcAUsignOut();
  }

  // ------------------------------
  // 退会
  // ------------------------------
  Future<void> deleteUser() async {
    await FirebaseAuth.instance.currentUser?.delete();
  }
}

処理ができたら画面を作成

それぞれのブロックを作成し、並べたサインインページを用意します。エラーがあった際の処理は次の章に書いています。

Dart
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:dayby/infrastructure/auth/auth.dart';
import 'package:dayby/infrastructure/auth/auth_error_message.dart';
import 'package:dayby/presentation/widgets/widget_styles.dart';
import 'package:dayby/presentation/widgets/staticwidgets.dart';

class MailAndPass extends HookWidget {
  const MailAndPass({super.key});

  @override
  Widget build(BuildContext context) {
    // アドレス入力
    final TextEditingController emailController = useTextEditingController();
    final mailField = SizedBox(
      width: fieldWidth(context) * 0.7,
      height: 45,
      child: TextFormField(
        controller: emailController,
        decoration: InputDecoration(
          labelText: 'メールアドレス',
        ),
        onChanged: (String value) {
          emailController.text = value;
        },
      ),
    );

    // パスワード入力
    final TextEditingController passwordController = useTextEditingController();
    final isObscure = useState<bool>(true);
    final passwordField = SizedBox(
      width: fieldWidth(context) * 0.7,
      height: 45,
      child: TextFormField(
        controller: passwordController,
        decoration: InputDecoration(
          labelText: 'パスワード',
          suffix: Padding(
            padding: EdgeInsets.symmetric(horizontal: 8 * sz),
            child: InkWell(
              child: isObscure.value ? MyIcon().obscureT : MyIcon().obscureF,
              onTap: () => isObscure.value = !isObscure.value,
            ),
          ),
        ),
        obscureText: isObscure.value,
        onChanged: (String value) {
          passwordController.text = value;
        },
      ),
    );

    // ユーザー登録ボタン
    final signUpButton = SaveButton(
      onPressed: () async {
        try {
          await AuthService().mailSignUp(
            emailController.text,
            passwordController.text,
          );
        } on FirebaseAuthException catch (e) {
          if (context.mounted) authError(context, e);
        }
      },
      title: 'ユーザー登録',
      width: fieldWidth(context) * 0.7,
    );

    final signInButton = SaveButton(
      onPressed: () async {
        try {
          await AuthService().mailSignIn(
            emailController.text,
            passwordController.text,
          );
        } on FirebaseAuthException catch (e) {
          if (context.mounted) authError(context, e);
        }
      },
      title: 'ログイン',
      width: fieldWidth(context) * 0.7,
    );

    return signinContainer(
      [
        mailField,
        passwordField,
        signUpButton,
        signInButton,
      ],
    );
  }
}
Dart
import 'package:flutter/material.dart';
import 'package:sign_in_button/sign_in_button.dart';
import 'package:dayby/infrastructure/auth/auth.dart';
import 'package:dayby/presentation/widgets/widget_styles.dart';

class Social extends StatelessWidget {
  const Social({super.key});

  @override
  Widget build(BuildContext context) {
    // google ///////////////////////////
    final googleSignInButton = SizedBox(
      width: fieldWidth(context) * 0.7,
      height: 40,
      child: SignInButton(
        Buttons.google,
        text: (fieldWidth(context) < 300) ? 'Googleサインイン' : 'Googleアカウントを使う',
        onPressed: () async {
          await AuthService().googleSignUpIn();
        },
      ),
    );

    // apple ////////////////////////////
    final appleSignInButton = SizedBox(
      width: fieldWidth(context) * 0.7,
      height: 40,
      child: SignInButton(
        Buttons.apple,
        text: (fieldWidth(context) < 300) ? 'Appleサインイン' : 'Appleアカウントを使う',
        onPressed: () async {
          await AuthService().appleSignUpIn();
        },
      ),
    );

    return signinContainer(
      [
        Text('外部アカウントで登録・ログイン'),
        googleSignInButton,
        appleSignInButton,
      ],
    );
  }
}
Dart
import 'package:flutter/material.dart';
import 'package:dayby/infrastructure/auth/auth.dart';
import 'package:dayby/infrastructure/firestore/firestore.dart';
import 'package:dayby/presentation/pages/_boot/signup_and_signin_statement.dart';
import 'package:dayby/presentation/pages/_boot/signup_and_signin_mailpass.dart';
import 'package:dayby/presentation/pages/_boot/signup_and_signin_social.dart';
import 'package:dayby/presentation/widgets/logo.dart';

class SignUpAndIn extends StatelessWidget {
  const SignUpAndIn({super.key});

  @override
  Widget build(BuildContext context) {

    return Scaffold(
      body: SingleChildScrollView(
        child: Container(
          alignment: Alignment.center,
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            crossAxisAlignment: CrossAxisAlignment.center,
            children: [
              // ロゴ
              MyLogo(),
              // メールアドレス・パスワード
              MailAndPass(),
              // 外部アカウント
              Social(),
            ],
          ),
        ),
      ),
    );
  }
}

エラーメッセージを変換

エラーメッセージはエラーコードで出力されるので、ユーザーへ提示する文章は独自で定義します。主要なエラーコードを指定し、そのほかは「予期しないエラー」としてまとめています。

主なエラーコードはこちら
https://firebase.google.com/docs/auth/admin/errors?hl=ja

Dart
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:dayby/infrastructure/analytics/google_analytics_tags.dart';

// エラーコードを独自定義に変換
String authErrorMsg(String code) {
  switch (code) {
    case 'user-not-found':
      return 'ユーザーが見つかりません';
    case 'user-disabled':
      return 'ユーザーが無効化されています';
    case 'email-already-in-use':
      return 'メースアドレスは既に利用されています';
    case 'invalid-email':
      return '入力したメールアドレスが無効です';
    case 'weak-password':
      return 'パスワードには6文字以上の文字列を指定してください';
    case 'invalid-credential':
      return 'メールアドレスまたはパスワードが間違っています';
    case 'too-many-requests':
      return 'アクセスが集中しています。少し時間を置いて再度お試しください';
    default:
      return '予期しないエラーが発生しました';
  }
}

SnackBarがエラーっぽいかなと思ったので、表示する関数を作成し、「ユーザー登録」「ログイン」ボタンの押下時にエラーをキャッチしたら呼び出すようにします。

Dart
Future<void> authError(BuildContext context, FirebaseAuthException e) async {
  ScaffoldMessenger.of(context).showSnackBar(
    SnackBar(
      content: Text(authErrorMsg(e.code)),
    ),
  );
  await GATags().errAuth(e.code);
}

コメント

タイトルとURLをコピーしました