見出し画像

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/overview

Googleログインの参考
https://firebase.google.com/docs/auth/ios/google-signin
https://firebase.google.com/docs/auth/android/google-signin
https://qiita.com/unsoluble_sugar/items/95b16c01b456be19f9ac

Sign in with Appleの参考(Androidは非対応)
https://firebase.google.com/docs/auth/ios/apple
https://medium.com/flutter-jp/sign-in-with-apple-d0d123cbbe17

Facebook周りの参考
https://firebase.google.com/docs/auth/ios/facebook-login
https://firebase.google.com/docs/auth/android/facebook-login
https://tech.jxpress.net/entry/2019/12/06/181705
https://blog.dalt.me/2200
https://blog.dalt.me/2197

Twitter周りの参考
https://firebase.google.com/docs/auth/ios/twitter-login
https://firebase.google.com/docs/auth/android/twitter-login
https://www.blog.danishi.net/2020/11/17/post-4146/
https://qiita.com/0maru/items/a46f5e5b1a9644bb58af
https://petercoding.com/firebase/2021/06/06/using-twitter-authentication-with-firebase-in-flutter/

今回詰まったところ

設定周りで困ったこと
1. projectIDをケバブケースにしてjsonが一致せず起動できなかった
2. FirebaseAuthの設定をせずに実行して、想定した動きにならなかった

設定周りの注意点
iOS
minSDKを10.0にすること
10.0以前のものを指定しているとpod install時にエラーが起きる

Android
minSdkVersion 16にすること

Sign in with Appleの実装注意点
1. 特に難しいところなし

Google認証の実装注意点
1. 特に難しいところなし

Facebook認証の実装注意点
1. カスタムスキームURLをFacebook用に用意する必要があり

Twitter認証の実装注意点
1. カスタムスキームURLをTwitter用に用意する必要があり
2. 似たようなプラグインがあるため注意する(今回はtwitter_loginを利用)

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

TBC...

この記事が気に入ったらサポートをしてみませんか?