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...