認証のエラーメッセージをユーザーにわかりやすく表示したい
「エラーになるけど原因がわからない」という状態はユーザー体験としてよろしくないなと、日々利用するサービスの中で感じるので、きちんとエラー内容を表示したい。
認証機能で実装してみました。
まずは認証処理を実装
ユーザー認証は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
サインインの処理関数を用意します。セットでサインアウトとユーザー削除も用意。
※サインアップ=新規登録、サインイン=既存アカウントにログイン の意味で使っています。
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();
}
}
処理ができたら画面を作成
それぞれのブロックを作成し、並べたサインインページを用意します。エラーがあった際の処理は次の章に書いています。
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,
],
);
}
}
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,
],
);
}
}
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
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がエラーっぽいかなと思ったので、表示する関数を作成し、「ユーザー登録」「ログイン」ボタンの押下時にエラーをキャッチしたら呼び出すようにします。
Future<void> authError(BuildContext context, FirebaseAuthException e) async {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(authErrorMsg(e.code)),
),
);
await GATags().errAuth(e.code);
}
コメント