Flutter×FirebaseAuthでSNS認証①

Flutter×supabaseのログイン調査してみたこんにちは、株式会社Pentagonでアプリ開発をしている石渡港です。
Flutter×FirebaseでSNS認証を実装したので簡単にまとめます。

目次

実装したコードとUI

ざっくりとコードとUIだけ記載します
signin_screen.dart

import 'dart:convert';
import 'dart:math';

import 'package:crypto/crypto.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart';
import 'package:flutter_facebook_login/flutter_facebook_login.dart';
import 'package:flutter_signin_button/flutter_signin_button.dart';
import 'package:google_sign_in/google_sign_in.dart';
import 'package:sign_in_with_apple/sign_in_with_apple.dart';
import 'package:twitter_login/twitter_login.dart';

final FirebaseAuth _auth = FirebaseAuth.instance;

class SignInScreen extends StatefulWidget {
 final String title = 'ログインとログアウト';

 @override
 State<StatefulWidget> createState() => _SignInScreenState();
}

class _SignInScreenState extends State<SignInScreen> {
 User? user;

 @override
 void initState() {
   _auth.userChanges().listen((event) => setState(() => user = event));
   super.initState();
 }

 @override
 Widget build(BuildContext context) {
   return Scaffold(
     appBar: AppBar(
       title: Text(widget.title),
       actions: [
         Builder(
           builder: (context) {
             return TextButton(
               onPressed: () async {
                 final user = _auth.currentUser;
                 if (user == null) {
                   ScaffoldMessenger.of(context).showSnackBar(
                     const SnackBar(
                       content: Text('サインインしていません'),
                     ),
                   );
                   return;
                 }
                 await _signOut();

                 final String uid = user.uid;
                 ScaffoldMessenger.of(context).showSnackBar(
                   SnackBar(
                     content: Text('$uid ログアウトしました'),
                   ),
                 );
               },
               child: Text(
                 'Sign out',
                 style: TextStyle(color: Theme.of(context).buttonColor),
               ),
             );
           },
         )
       ],
     ),
     body: SafeArea(
       child: Builder(
         builder: (BuildContext context) {
           return ListView(
             padding: const EdgeInsets.all(8),
             children: [
               _UserInfoCard(user),
               _EmailPasswordForm(),
               _EmailLinkSignInSection(),
               _AnonymouslySignInSection(),
               _PhoneSignInSection(),
               _OtherProvidersSignInSection(),
             ],
           );
         },
       ),
     ),
   );
 }

 // ログアウト
 Future<void> _signOut() async {
   await _auth.signOut();
 }
}

// ユーザ情報の表示用
class _UserInfoCard extends StatefulWidget {
 final User? user;

 const _UserInfoCard(this.user);

 @override
 _UserInfoCardState createState() => _UserInfoCardState();
}

class _UserInfoCardState extends State<_UserInfoCard> {
 @override
 Widget build(BuildContext context) {
   return Card(
     child: Padding(
       padding: const EdgeInsets.all(16),
       child: Column(
         crossAxisAlignment: CrossAxisAlignment.start,
         children: [
           Container(
             padding: const EdgeInsets.only(bottom: 8),
             alignment: Alignment.center,
             child: const Text(
               'ユーザ情報',
               style: TextStyle(fontWeight: FontWeight.bold),
             ),
           ),
           if (widget.user != null)
             if (widget.user?.photoURL != null)
               Container(
                 alignment: Alignment.center,
                 margin: const EdgeInsets.only(bottom: 8),
                 child: Image.network(widget.user!.photoURL!),
               )
             else
               Align(
                 child: Container(
                   padding: const EdgeInsets.all(8),
                   margin: const EdgeInsets.only(bottom: 8),
                   color: Colors.black,
                   child: const Text(
                     '画像がありません',
                     textAlign: TextAlign.center,
                   ),
                 ),
               ),
           Text(widget.user == null
               ? 'ログインしていません'
               : '${widget.user!.isAnonymous ? '匿名ユーザ\n\n' : ''}'
                   'メールアドレス: ${widget.user!.email} (検証済みか否か: ${widget.user!.emailVerified})\n\n'
                   '電話番号: ${widget.user!.phoneNumber}\n\n'
                   '名前: ${widget.user!.displayName}\n\n\n'
                   'ID: ${widget.user!.uid}\n\n'
                   'Tenant ID: ${widget.user!.tenantId}\n\n'
                   'Refresh token: ${widget.user!.refreshToken}\n\n\n'
                   '作成日時: ${widget.user!.metadata.creationTime.toString()}\n\n'
                   '最終ログイン日時: ${widget.user!.metadata.lastSignInTime}\n\n'),
           if (widget.user != null)
             Column(
               crossAxisAlignment: CrossAxisAlignment.stretch,
               children: [
                 Text(
                   widget.user!.providerData.isEmpty ? 'プロバイダではない' : 'プロバイダ:',
                   style: const TextStyle(fontWeight: FontWeight.bold),
                   textAlign: TextAlign.center,
                 ),
                 for (var provider in widget.user!.providerData)
                   Dismissible(
                     key: Key(provider.uid!),
                     onDismissed: (action) =>
                         widget.user?.unlink(provider.providerId),
                     child: Card(
                       color: Colors.grey[700],
                       child: ListTile(
                         leading: provider.photoURL == null
                             ? IconButton(
                                 icon: const Icon(Icons.remove),
                                 onPressed: () =>
                                     widget.user?.unlink(provider.providerId),
                               )
                             : Image.network(provider.photoURL!),
                         title: Text(provider.providerId),
                         subtitle: Text(
                             '${provider.uid == null ? '' : 'ID: ${provider.uid}\n'}'
                             '${provider.email == null ? '' : 'メール検証: ${provider.email}\n'}'
                             '${provider.phoneNumber == null ? '' : '電話番号r: ${provider.phoneNumber}\n'}'
                             '${provider.displayName == null ? '' : '名前: ${provider.displayName}\n'}'),
                       ),
                     ),
                   ),
               ],
             ),
           Visibility(
             visible: widget.user != null,
             child: Container(
               margin: const EdgeInsets.only(top: 8),
               alignment: Alignment.center,
               child: Row(
                 mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                 children: [
                   IconButton(
                     onPressed: () => widget.user?.reload(),
                     icon: const Icon(Icons.refresh),
                   ),
                   IconButton(
                     onPressed: () => showDialog(
                       context: context,
                       builder: (context) => UpdateUserDialog(widget.user!),
                     ),
                     icon: const Icon(Icons.text_snippet),
                   ),
                   IconButton(
                     onPressed: () => widget.user?.delete(),
                     icon: const Icon(Icons.delete_forever),
                   ),
                 ],
               ),
             ),
           ),
         ],
       ),
     ),
   );
 }
}

// ダイアログ
class UpdateUserDialog extends StatefulWidget {
 final User user;

 const UpdateUserDialog(this.user);

 @override
 _UpdateUserDialogState createState() => _UpdateUserDialogState();
}

class _UpdateUserDialogState extends State<UpdateUserDialog> {
 late TextEditingController _nameController;
 late TextEditingController _urlController;

 @override
 void initState() {
   _nameController = TextEditingController(text: widget.user.displayName!);
   _urlController = TextEditingController(text: widget.user.photoURL!);
   super.initState();
 }

 @override
 Widget build(BuildContext context) {
   return AlertDialog(
     title: const Text('プロフィール'),
     content: SingleChildScrollView(
       child: ListBody(
         children: [
           TextFormField(
             controller: _nameController,
             autocorrect: false,
             decoration: const InputDecoration(labelText: '表示名'),
           ),
           TextFormField(
             controller: _urlController,
             decoration: const InputDecoration(labelText: '写真URL'),
             autovalidateMode: AutovalidateMode.onUserInteraction,
             autocorrect: false,
             validator: (value) {
               if (value?.isNotEmpty ?? false) {
                 var uri = Uri.parse(value!);
                 if (uri.isAbsolute) {
                   return null;
                 }
                 return '不正なURL!';
               }
               return null;
             },
           ),
         ],
       ),
     ),
     actions: [
       TextButton(
         onPressed: () {
           widget.user.updateDisplayName(_nameController.text);
           widget.user.updateDisplayName(_urlController.text);
           Navigator.of(context).pop();
         },
         child: const Text('更新する'),
       ),
     ],
   );
 }

 @override
 void dispose() {
   _nameController.dispose();
   _urlController.dispose();
   super.dispose();
 }
}

// メールパスワードフォーム
class _EmailPasswordForm extends StatefulWidget {
 @override
 State<StatefulWidget> createState() => _EmailPasswordFormState();
}

class _EmailPasswordFormState extends State<_EmailPasswordForm> {
 final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
 final TextEditingController _emailController = TextEditingController();
 final TextEditingController _passwordController = TextEditingController();

 @override
 Widget build(BuildContext context) {
   return Form(
     key: _formKey,
     child: Card(
       child: Padding(
         padding: const EdgeInsets.all(16),
         child: Column(
           crossAxisAlignment: CrossAxisAlignment.start,
           children: [
             Container(
               alignment: Alignment.center,
               child: const Text(
                 'メールアドレスとパスワードでサインイン',
                 style: TextStyle(fontWeight: FontWeight.bold),
               ),
             ),
             TextFormField(
               controller: _emailController,
               decoration: const InputDecoration(labelText: 'メールアドレス'),
               validator: (value) {
                 if (value?.isEmpty ?? false) return '入力してください';
                 return null;
               },
             ),
             TextFormField(
               controller: _passwordController,
               decoration: const InputDecoration(labelText: 'パスワード'),
               validator: (value) {
                 if (value?.isEmpty ?? false) return '入力してください';
                 return null;
               },
               obscureText: true,
             ),
             Container(
               padding: const EdgeInsets.only(top: 16),
               alignment: Alignment.center,
               child: SignInButton(
                 Buttons.Email,
                 text: 'ログイン',
                 onPressed: () async {
                   if (_formKey.currentState!.validate()) {
                     await _signInWithEmailAndPassword();
                   }
                 },
               ),
             ),
           ],
         ),
       ),
     ),
   );
 }

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

 Future<void> _signInWithEmailAndPassword() async {
   try {
     final user = (await _auth.signInWithEmailAndPassword(
       email: _emailController.text,
       password: _passwordController.text,
     ))
         .user;

     ScaffoldMessenger.of(context).showSnackBar(
       SnackBar(
         content: Text('${user?.email} でサインインしました'),
       ),
     );
   } catch (e) {
     ScaffoldMessenger.of(context).showSnackBar(
       const SnackBar(
         content: Text('メールアドレスとパスワードでサインインできませんでした'),
       ),
     );
   }
 }
}

class _EmailLinkSignInSection extends StatefulWidget {
 @override
 State<StatefulWidget> createState() => _EmailLinkSignInSectionState();
}

class _EmailLinkSignInSectionState extends State<_EmailLinkSignInSection> {
 final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
 final TextEditingController _emailController = TextEditingController();

 String _userEmail = '';

 @override
 Widget build(BuildContext context) {
   return Form(
     key: _formKey,
     child: Card(
       child: Padding(
         padding: const EdgeInsets.all(16),
         child: Column(
           crossAxisAlignment: CrossAxisAlignment.start,
           children: [
             Container(
               alignment: Alignment.center,
               child: const Text(
                 'メールのリンクでログインする',
                 style: TextStyle(fontWeight: FontWeight.bold),
               ),
             ),
             TextFormField(
               controller: _emailController,
               decoration: const InputDecoration(labelText: 'メールアドレス'),
               validator: (value) {
                 if (value?.isEmpty ?? false) return 'メールアドレスを入力してください';
                 return null;
               },
             ),
             Container(
               padding: const EdgeInsets.only(top: 16),
               alignment: Alignment.center,
               child: SignInButtonBuilder(
                 icon: Icons.insert_link,
                 text: 'ログイン',
                 backgroundColor: Colors.blueGrey[700]!,
                 onPressed: () async {
                   await _signInWithEmailAndLink();
                 },
               ),
             ),
           ],
         ),
       ),
     ),
   );
 }

 @override
 void dispose() {
   _emailController.dispose();
   super.dispose();
 }

 Future<void> _signInWithEmailAndLink() async {
   try {
     _userEmail = _emailController.text;

     await _auth.sendSignInLinkToEmail(
       email: _userEmail,
       actionCodeSettings: ActionCodeSettings(
           url:
               'https://react-native-firebase-testing.firebaseapp.com/emailSignin',
           handleCodeInApp: true,
           iOSBundleId: 'io.flutter.plugins.firebaseAuthExample',
           androidPackageName: 'io.flutter.plugins.firebaseauthexample'),
     );

     ScaffoldMessenger.of(context).showSnackBar(
       SnackBar(
         content: Text('$_userEmail にメールを送りました'),
       ),
     );
   } catch (e) {
     print(e);
     ScaffoldMessenger.of(context).showSnackBar(
       const SnackBar(
         content: Text('メール送信失敗しました'),
       ),
     );
   }
 }
}

// 匿名ログイン
class _AnonymouslySignInSection extends StatefulWidget {
 @override
 State<StatefulWidget> createState() => _AnonymouslySignInSectionState();
}

class _AnonymouslySignInSectionState extends State<_AnonymouslySignInSection> {
 bool? _success;
 String? _userID;

 @override
 Widget build(BuildContext context) {
   return Card(
     child: Padding(
       padding: const EdgeInsets.all(16),
       child: Column(
         crossAxisAlignment: CrossAxisAlignment.start,
         children: [
           Container(
             alignment: Alignment.center,
             child: const Text(
               '匿名ログイン',
               style: TextStyle(fontWeight: FontWeight.bold),
             ),
           ),
           Container(
             padding: const EdgeInsets.only(top: 16),
             alignment: Alignment.center,
             child: SignInButtonBuilder(
               text: 'ログインする',
               icon: Icons.person_outline,
               backgroundColor: Colors.deepPurple,
               onPressed: _signInAnonymously,
             ),
           ),
           Visibility(
             visible: _success != null,
             child: Container(
               alignment: Alignment.center,
               padding: const EdgeInsets.symmetric(horizontal: 16),
               child: Text(
                 _success == null
                     ? ''
                     : (_success! ? 'ログインに成功しました: $_userID' : 'ログインに失敗しました'),
                 style: const TextStyle(color: Colors.red),
               ),
             ),
           ),
         ],
       ),
     ),
   );
 }

 Future<void> _signInAnonymously() async {
   try {
     final User? user = (await _auth.signInAnonymously()).user;

     ScaffoldMessenger.of(context).showSnackBar(
       SnackBar(
         content: Text('匿名ログインしました ${user?.uid}'),
       ),
     );
   } catch (e) {
     ScaffoldMessenger.of(context).showSnackBar(
       const SnackBar(
         content: Text('匿名ログインできませんでした'),
       ),
     );
   }
 }
}

// 電話番号認証
class _PhoneSignInSection extends StatefulWidget {
 @override
 State<StatefulWidget> createState() => _PhoneSignInSectionState();
}

class _PhoneSignInSectionState extends State<_PhoneSignInSection> {
 final TextEditingController _phoneNumberController = TextEditingController();
 final TextEditingController _smsController = TextEditingController();

 String? _message;
 String? _verificationId;
 ConfirmationResult? webConfirmationResult;

 @override
 Widget build(BuildContext context) {
   if (kIsWeb) {
     return Card(
       child: Padding(
         padding: const EdgeInsets.all(16),
         child: Column(
           crossAxisAlignment: CrossAxisAlignment.start,
           children: [
             Container(
               padding: const EdgeInsets.only(bottom: 16),
               alignment: Alignment.center,
               child: const Text(
                 '電話番号ログイン',
                 style: TextStyle(fontWeight: FontWeight.bold),
               ),
             ),
             Container(
               padding: const EdgeInsets.only(bottom: 16),
               child: TextFormField(
                 controller: _phoneNumberController,
                 decoration: const InputDecoration(
                   labelText: '電話番号 (+x xxx-xxx-xxxx)',
                 ),
                 validator: (value) {
                   if (value?.isEmpty ?? false) {
                     return '電話番号 (+x xxx-xxx-xxxx)';
                   }
                   return null;
                 },
               ),
             ),
             Container(
               alignment: Alignment.center,
               child: SignInButtonBuilder(
                 padding: const EdgeInsets.only(top: 16),
                 icon: Icons.contact_phone,
                 backgroundColor: Colors.deepOrangeAccent[700]!,
                 text: '電話番号',
                 onPressed: _verifyWebPhoneNumber,
               ),
             ),
             TextField(
               controller: _smsController,
               decoration: const InputDecoration(labelText: '認証コード'),
             ),
             Container(
               padding: const EdgeInsets.only(top: 16),
               alignment: Alignment.center,
               child: SignInButtonBuilder(
                 icon: Icons.phone,
                 backgroundColor: Colors.deepOrangeAccent[400]!,
                 onPressed: () {
                   _confirmCodeWeb();
                 },
                 text: 'ログイン',
               ),
             ),
           ],
         ),
       ),
     );
   }

   return Card(
     child: Padding(
       padding: const EdgeInsets.all(16),
       child: Column(
         crossAxisAlignment: CrossAxisAlignment.start,
         children: [
           Container(
             alignment: Alignment.center,
             child: const Text(
               '電話番号ログイン',
               style: TextStyle(fontWeight: FontWeight.bold),
             ),
           ),
           TextFormField(
             controller: _phoneNumberController,
             decoration: const InputDecoration(
               labelText: '電話番号 (+x xxx-xxx-xxxx)',
             ),
             validator: (value) {
               if (value?.isEmpty ?? false) {
                 return '電話番号 (+x xxx-xxx-xxxx)';
               }
               return null;
             },
           ),
           Container(
             padding: const EdgeInsets.symmetric(vertical: 16),
             alignment: Alignment.center,
             child: SignInButtonBuilder(
               icon: Icons.contact_phone,
               backgroundColor: Colors.deepOrangeAccent[700]!,
               text: '電話番号',
               onPressed: _verifyPhoneNumber,
             ),
           ),
           TextField(
             controller: _smsController,
             decoration: const InputDecoration(labelText: '認証コード'),
           ),
           Container(
             padding: const EdgeInsets.only(top: 16),
             alignment: Alignment.center,
             child: SignInButtonBuilder(
               icon: Icons.phone,
               backgroundColor: Colors.deepOrangeAccent[400]!,
               onPressed: _signInWithPhoneNumber,
               text: 'ログイン',
             ),
           ),
           Visibility(
             visible: _message != null,
             child: Container(
               alignment: Alignment.center,
               padding: const EdgeInsets.symmetric(horizontal: 16),
               child: Text(
                 _message ?? '',
                 style: const TextStyle(color: Colors.red),
               ),
             ),
           )
         ],
       ),
     ),
   );
 }

 Future<void> _verifyWebPhoneNumber() async {
   ConfirmationResult confirmationResult =
       await _auth.signInWithPhoneNumber(_phoneNumberController.text);

   webConfirmationResult = confirmationResult;
 }

 Future<void> _confirmCodeWeb() async {
   if (webConfirmationResult != null) {
     try {
       await webConfirmationResult!.confirm(_smsController.text);
     } catch (e) {
       ScaffoldMessenger.of(context).showSnackBar(
         SnackBar(
           content: Text('ログイン失敗しました: ${e.toString()}'),
         ),
       );
     }
   } else {
     ScaffoldMessenger.of(context).showSnackBar(
       const SnackBar(
         content: Text('認証コードが間違っています'),
       ),
     );
   }
 }

 // Example code of how to verify phone number
 Future<void> _verifyPhoneNumber() async {
   setState(
     () {
       _message = '';
     },
   );

   PhoneVerificationCompleted verificationCompleted =
       (PhoneAuthCredential phoneAuthCredential) async {
     await _auth.signInWithCredential(phoneAuthCredential);
     ScaffoldMessenger.of(context).showSnackBar(
       SnackBar(
         content: Text('電話番号を自動認証したのでログインします: $phoneAuthCredential'),
       ),
     );
   };

   PhoneVerificationFailed verificationFailed =
       (FirebaseAuthException authException) {
     setState(
       () {
         _message =
             '認証失敗しました。コード: ${authException.code}メッセージ: ${authException.message}';
       },
     );
   };

   PhoneCodeSent codeSent = (verificationId, [forceResendingToken]) async {
     ScaffoldMessenger.of(context).showSnackBar(
       const SnackBar(
         content: Text('認証コードをご確認ください'),
       ),
     );
     _verificationId = verificationId;
   };

   PhoneCodeAutoRetrievalTimeout codeAutoRetrievalTimeout =
       (String verificationId) {
     _verificationId = verificationId;
   };

   try {
     await _auth.verifyPhoneNumber(
       phoneNumber: _phoneNumberController.text,
       timeout: const Duration(seconds: 5),
       verificationCompleted: verificationCompleted,
       verificationFailed: verificationFailed,
       codeSent: codeSent,
       codeAutoRetrievalTimeout: codeAutoRetrievalTimeout,
     );
   } catch (e) {
     ScaffoldMessenger.of(context).showSnackBar(
       SnackBar(
         content: Text('認証に失敗しました: $e'),
       ),
     );
   }
 }

 Future<void> _signInWithPhoneNumber() async {
   try {
     final PhoneAuthCredential credential = PhoneAuthProvider.credential(
       verificationId: _verificationId!,
       smsCode: _smsController.text,
     );
     final user = (await _auth.signInWithCredential(credential)).user;

     ScaffoldMessenger.of(context).showSnackBar(
       SnackBar(
         content: Text('ログインしました: ${user?.uid}'),
       ),
     );
   } catch (e) {
     print(e);
     ScaffoldMessenger.of(context).showSnackBar(
       const SnackBar(
         content: Text('ログイン失敗しました'),
       ),
     );
   }
 }
}

// SNSログイン
class _OtherProvidersSignInSection extends StatefulWidget {
 _OtherProvidersSignInSection();

 @override
 State<StatefulWidget> createState() => _OtherProvidersSignInSectionState();
}

class _OtherProvidersSignInSectionState
   extends State<_OtherProvidersSignInSection> {
 final TextEditingController _tokenController = TextEditingController();
 final TextEditingController _tokenSecretController = TextEditingController();

 int _selection = 0;
 bool _showAuthSecretTextField = false;
 bool _showProviderTokenField = true;
 String _provider = 'Apple';

 @override
 Widget build(BuildContext context) {
   return Card(
     child: Padding(
       padding: const EdgeInsets.all(16),
       child: Column(
         crossAxisAlignment: CrossAxisAlignment.start,
         children: [
           Container(
             alignment: Alignment.center,
             child: const Text(
               'SNSログイン',
               style: TextStyle(fontWeight: FontWeight.bold),
             ),
           ),
           Container(
             padding: const EdgeInsets.only(top: 16),
             alignment: Alignment.center,
             child: Column(
               mainAxisAlignment: MainAxisAlignment.center,
               children: [
                 ListTile(
                   title: const Text('Apple'),
                   leading: Radio(
                     value: 0,
                     groupValue: _selection,
                     onChanged: _handleRadioButtonSelected,
                   ),
                 ),
                 Visibility(
                   visible: !kIsWeb,
                   child: ListTile(
                     title: const Text('Facebook'),
                     leading: Radio(
                       value: 1,
                       groupValue: _selection,
                       onChanged: _handleRadioButtonSelected,
                     ),
                   ),
                 ),
                 ListTile(
                   title: const Text('Twitter'),
                   leading: Radio(
                     value: 2,
                     groupValue: _selection,
                     onChanged: _handleRadioButtonSelected,
                   ),
                 ),
                 ListTile(
                   title: const Text('Google'),
                   leading: Radio(
                     value: 3,
                     groupValue: _selection,
                     onChanged: _handleRadioButtonSelected,
                   ),
                 ),
               ],
             ),
           ),
           Visibility(
             visible: _showProviderTokenField && !kIsWeb,
             child: TextField(
               controller: _tokenController,
               decoration: const InputDecoration(labelText: 'token'),
             ),
           ),
           Visibility(
             visible: _showAuthSecretTextField && !kIsWeb,
             child: TextField(
               controller: _tokenSecretController,
               decoration: const InputDecoration(
                 labelText: 'authTokenSecret',
               ),
             ),
           ),
           Container(
             padding: const EdgeInsets.only(top: 16),
             alignment: Alignment.center,
             child: SignInButton(
               _provider == 'Apple'
                   ? Buttons.Apple
                   : (_provider == 'Facebook'
                       ? Buttons.Facebook
                       : (_provider == 'Twitter'
                           ? Buttons.Twitter
                           : Buttons.GoogleDark)),
               text: 'ログイン',
               onPressed: () async {
                 _signInWithOtherProvider();
               },
             ),
           ),
         ],
       ),
     ),
   );
 }

 void _handleRadioButtonSelected(int? value) {
   setState(
     () {
       _selection = value!;
       switch (_selection) {
         case 0:
           {
             _provider = 'Apple';
             _showAuthSecretTextField = false;
             _showProviderTokenField = false;
           }
           break;

         case 1:
           {
             _provider = 'Facebook';
             _showAuthSecretTextField = false;
             _showProviderTokenField = false;
           }
           break;

         case 2:
           {
             _provider = 'Twitter';
             _showAuthSecretTextField = false;
             _showProviderTokenField = false;
           }
           break;

         default:
           {
             _provider = 'Google';
             _showAuthSecretTextField = false;
             _showProviderTokenField = false;
           }
       }
     },
   );
 }

 void _signInWithOtherProvider() {
   switch (_selection) {
     case 0:
       _signInWithApple();
       break;
     case 1:
       _signInWithFacebook();
       break;
     case 2:
       _signInWithTwitter();
       break;
     default:
       _signInWithGoogle();
   }
 }

 // Appleログイン用(Android未対応)
 String generateNonce([int length = 32]) {
   final charset =
       '0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-._';
   final random = Random.secure();
   return List.generate(length, (_) => charset[random.nextInt(charset.length)])
       .join();
 }

 // Appleログイン用
 String sha256ofString(String input) {
   final bytes = utf8.encode(input);
   final digest = sha256.convert(bytes);
   return digest.toString();
 }

 // Appleログイン
 Future<void> _signInWithApple() async {
   try {
     UserCredential userCredential;
     final rawNonce = generateNonce();
     final nonce = sha256ofString(rawNonce);

     // Request credential for the currently signed in Apple account.
     final appleCredential = await SignInWithApple.getAppleIDCredential(
       scopes: [
         AppleIDAuthorizationScopes.email,
         AppleIDAuthorizationScopes.fullName,
       ],
       nonce: nonce,
     );

     if (kIsWeb) {
       userCredential =
           await _auth.signInWithPopup(OAuthProvider("apple.com"));
     } else {
       final oauthCredential = OAuthProvider("apple.com").credential(
         idToken: appleCredential.identityToken,
         rawNonce: rawNonce,
       );
       userCredential =
           await FirebaseAuth.instance.signInWithCredential(oauthCredential);
     }
     final user = userCredential.user;
     ScaffoldMessenger.of(context).showSnackBar(
       SnackBar(
         content: Text('Appleログイン ${user?.uid}'),
       ),
     );
   } catch (e) {
     print(e);
     ScaffoldMessenger.of(context).showSnackBar(
       SnackBar(
         content: Text('Appleでログイン失敗しました: $e'),
       ),
     );
   }
 }

 static final facebookLogin = FacebookLogin();

 // Facebookログイン
 Future<void> _signInWithFacebook() async {
   try {
     final result = await facebookLogin.logIn(['email']);

     switch (result.status) {
       case FacebookLoginStatus.loggedIn:
         final credential =
             FacebookAuthProvider.credential(result.accessToken.token);
         final authResult =
             await FirebaseAuth.instance.signInWithCredential(credential);
         final user = authResult.user;
         ScaffoldMessenger.of(context).showSnackBar(
           SnackBar(
             content: Text('Faceログイン ${user?.uid}'),
           ),
         );
         break;
       case FacebookLoginStatus.error:
         throw ('error, ${result.errorMessage}');
         break;
       case FacebookLoginStatus.cancelledByUser:
         throw ('cancelled');
         break;
     }
   } catch (e) {
     print(e);
     ScaffoldMessenger.of(context).showSnackBar(
       SnackBar(
         content: Text('Facebookでログイン失敗しました: $e'),
       ),
     );
   }
 }

 // Twitterログイン
 Future<void> _signInWithTwitter() async {
   try {
     UserCredential? userCredential;
     if (kIsWeb) {
       TwitterAuthProvider twitterProvider = TwitterAuthProvider();
       await _auth.signInWithPopup(twitterProvider);
     } else {
       final twitterLogin = TwitterLogin(
         apiKey: 'apiKey',
         apiSecretKey: 'apiSecretKey',
         redirectURI: 'tokyo.pentagon.flutterfiretest://',
       );
       final result = await twitterLogin.login();

       final AuthCredential credential = TwitterAuthProvider.credential(
         accessToken: result.authToken!,
         secret: result.authTokenSecret!,
       );
       userCredential = await _auth.signInWithCredential(credential);
     }
     final user = userCredential?.user;
     ScaffoldMessenger.of(context).showSnackBar(
       SnackBar(
         content: Text('Twitterログイン ${user?.uid}'),
       ),
     );
   } catch (e) {
     print(e);
     ScaffoldMessenger.of(context).showSnackBar(
       SnackBar(
         content: Text('Twitterでログイン失敗しました: $e'),
       ),
     );
   }
 }

 // Googleログイン
 Future<void> _signInWithGoogle() async {
   try {
     UserCredential userCredential;
     if (kIsWeb) {
       var googleProvider = GoogleAuthProvider();
       userCredential = await _auth.signInWithPopup(googleProvider);
     } else {
       final googleUser = await GoogleSignIn().signIn();
       final googleAuth = await googleUser?.authentication;
       final googleAuthCredential = GoogleAuthProvider.credential(
         accessToken: googleAuth?.accessToken,
         idToken: googleAuth?.idToken,
       );
       userCredential = await _auth.signInWithCredential(googleAuthCredential);
     }
     final user = userCredential.user;
     ScaffoldMessenger.of(context).showSnackBar(
       SnackBar(
         content: Text('Googleログイン ${user?.uid}'),
       ),
     );
   } catch (e) {
     print(e);
     ScaffoldMessenger.of(context).showSnackBar(
       SnackBar(
         content: Text('Googleでログイン失敗しました: $e'),
       ),
     );
   }
 }
}

pubspec.yaml(一部抜粋)

dependencies:
 flutter:
   sdk: flutter

 # The following adds the Cupertino Icons font to your application.
 # Use with the CupertinoIcons class for iOS style icons.
 cupertino_icons: ^1.0.2
 firebase_core: ^1.4.0
 firebase_auth: ^3.0.1
 firebase_analytics: ^8.2.0
 cloud_firestore: ^2.4.0
 flutter_signin_button: ^2.0.0
 google_sign_in: ^5.0.7
 sign_in_with_apple: ^3.0.0
 crypto: ^3.0.1
 convert: ^3.0.1
 flutter_facebook_login: ^3.0.0
 twitter_login: ^4.0.1

参考

メインのコードhttps://github.com/FirebaseExtended/flutterfire/tree/master/packages/firebase_auth/firebase_auth/example
設定周りの参考https://firebase.flutter.dev/docs/auth/overviewGoogleログインの参考https://firebase.google.com/docs/auth/ios/google-signinhttps://firebase.google.com/docs/auth/android/google-signinhttps://qiita.com/unsoluble_sugar/items/95b16c01b456be19f9acSign in with Appleの参考(Androidは非対応)https://firebase.google.com/docs/auth/ios/applehttps://medium.com/flutter-jp/sign-in-with-apple-d0d123cbbe17Facebook周りの参考https://firebase.google.com/docs/auth/ios/facebook-loginhttps://firebase.google.com/docs/auth/android/facebook-loginhttps://tech.jxpress.net/entry/2019/12/06/181705https://blog.dalt.me/2200https://blog.dalt.me/2197Twitter周りの参考https://firebase.google.com/docs/auth/ios/twitter-loginhttps://firebase.google.com/docs/auth/android/twitter-loginhttps://www.blog.danishi.net/2020/11/17/post-4146/https://qiita.com/0maru/items/a46f5e5b1a9644bb58afhttps://petercoding.com/firebase/2021/06/06/using-twitter-authentication-with-firebase-in-flutter/

今回詰まったところ

設定周りで困ったこと1. projectIDをケバブケースにしてjsonが一致せず起動できなかった2. FirebaseAuthの設定をせずに実行して、想定した動きにならなかった
設定周りの注意点iOSminSDKを10.0にすること10.0以前のものを指定しているとpod install時にエラーが起きるAndroidminSdkVersion 16にすること
Sign in with Appleの実装注意点1. 特に難しいところなし
Google認証の実装注意点1. 特に難しいところなし
Facebook認証の実装注意点1. カスタムスキームURLをFacebook用に用意する必要があり
Twitter認証の実装注意点1. カスタムスキームURLをTwitter用に用意する必要があり2. 似たようなプラグインがあるため注意する(今回はtwitter_loginを利用)

細かいところに関しては次回以降SNSごとに記述します

TBC...

採用情報はこちら
目次