こんにちは、株式会社Pentagonでアプリ開発をしている石渡港です。
経緯https://supabase.io/blog/2021/07/28/supabase-auth-passwordless-sms-loginFlutter
でもSMSログインできるかな?🤔今日は、Flutter×supabaseのログイン調査してみた について簡単にまとめたいと思います。
▼ 参考
https://supabase.io/docs/guides/with-flutter
▼ 手順
※ サクッと動きを確認したい方はこちらsuperbaseを利用するにはGitHubアカウントが必要です。
-
superbaseのプロジェクトを作る
1.1. https://app.supabase.io/ へアクセス
1.2. チームを作成
1.3. プロジェクトを作成
-
SQLを設定
2.1. プロジェクト詳細に移動
2.2. SQLへ移動
2.3. User Management Starterへ移動
2.4. Runを実行
3. 設定の確認
3.1. Settingsへ移動
3.2. サイドバーのAPIへ移動→こちらの情報を参照する
-
Flutterのプラグインの設定 ※ Flutter2.0で作成
4.1. 「supabase_flutter: ^0.0.6」を「pubspec.yaml」の「dependencies:」下に記載する ※ 0.1.0の場合はAPIが違うため注意https://pub.dev/packages/supabase_flutter4.2. 「pub get」を実行する -
Authenticationの設定
5.1. Authenticationへ移動
5.2. サイドバーのSettingへ移動5.3. Additional Redirect URLsに今回作成するFlutterプロジェクトのbundle identiferを利用したスキームURLを入力する、今回の場合、「tokyo.pentagon.supabase_quick_start://login-callback/」としました
-
Flutterのディープリンクの設定
6.1. android/app/src/main/AndroidManifest.xmlを編集
<manifest ...>
<!-- ... other tags -->
<application ...>
<activity ...>
<!-- ... other tags -->
<!-- Add this intent-filter for Deep Links -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<!-- Accepts URIs that begin with YOUR_SCHEME://YOUR_HOST -->
<data
android:scheme="tokyo.pentagon.supabase_quick_start"
android:host="login-callback" />
</intent-filter>
</activity>
</application>
</manifest>
6.2. ios/Runner/Info.plistを編集
<!-- ... other tags -->
<plist>
<dict>
<!-- ... other tags -->
<!-- Add this array for Deep Links -->
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>tokyo.pentagon.supabase_quick_start</string>
</array>
</dict>
</array>
<!-- ... other tags -->
</dict>
</plist>
-
Flutterのsupabaseの設定
7.1 lib/main.dartへの書き込み
void main() {
WidgetsFlutterBinding.ensureInitialized();
Supabase.initialize(
url: '[YOUR_SUPABASE_URL]', // supabaseのSetting>APIを参照
anonKey: '[YOUR_SUPABASE_ANNON_KEY]', // supabaseのSetting>APIを参照
authCallbackUrlHostname: 'login-callback',
);
runApp(MyApp());
}
7.2. lib/components/auth_state.dartの作成 ※ 0.1.0だとこちらのメソッドの呼び出され方が違うようです🤔
import 'package:flutter/material.dart';
import 'package:supabase/supabase.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
class AuthState<T extends StatefulWidget> extends SupabaseAuthState<T> {
@override
void onUnauthenticated() {
Navigator.of(context).pushNamedAndRemoveUntil('/login', (route) => false);
}
@override
void onAuthenticated(Session session) {
Navigator.of(context).pushNamedAndRemoveUntil('/account', (route) => false);
}
@override
void onPasswordRecovery(Session session) {}
@override
void onErrorAuthenticating(String message) {
print('Error authenticating $message');
}
}
7.3. lib/components/auth_required_state.dartの作成
import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
class AuthRequiredState<T extends StatefulWidget>
extends SupabaseAuthRequiredState<T> {
@override
void onUnauthenticated() {
/// Users will be sent back to the LoginPage if they sign out.
Navigator.of(context).pushNamedAndRemoveUntil('/login', (route) => false);
}
}
7.4. lib/utils/constants.dartの作成
import 'package:supabase_flutter/supabase_flutter.dart';
final supabase = Supabase.instance.client;
7.5. lib/pages/splash_page.dartの作成
import 'package:flutter/material.dart';
import 'package:supabase_quick_start/components/auth_state.dart'; // 自プロジェクト内
class SplashPage extends StatefulWidget {
const SplashPage({Key? key}) : super(key: key);
@override
_SplashPageState createState() => _SplashPageState();
}
/// AuthStateが呼び出され、別のScreenへ遷移
class _SplashPageState extends AuthState<SplashPage> {
@override
void initState() {
recoverSupabaseSession();
super.initState();
}
@override
Widget build(BuildContext context) {
return const Scaffold(
body: Center(child: CircularProgressIndicator()),
);
}
}
7.6. lib/pages/login_page.dartの作成
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:supabase_quick_start/components/auth_state.dart'; // 自プロジェクト内
import 'package:supabase/supabase.dart';
import 'package:supabase_quick_start/utils/constants.dart'; // 自プロジェクト内
class LoginPage extends StatefulWidget {
const LoginPage({Key? key}) : super(key: key);
@override
_LoginPageState createState() => _LoginPageState();
}
class _LoginPageState extends AuthState<LoginPage> {
bool _isLoading = false;
late final TextEditingController _emailController;
Future<void> _signIn() async {
setState(() {
_isLoading = true;
});
final response = await supabase.auth.signIn(
email: _emailController.text,
options: AuthOptions(
redirectTo: kIsWeb
? null
: 'tokyo.pentagon.supabase_quick_start://login-callback/'));
if (response.error != null) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(response.error!.message),
backgroundColor: Colors.red,
));
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Check your email for login link!')));
}
setState(() {
_emailController.clear();
_isLoading = false;
});
}
@override
void initState() {
_emailController = TextEditingController();
super.initState();
}
@override
void dispose() {
_emailController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Sign In')),
body: ListView(
padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 12),
children: [
const Text('Sign in via the magic link with your email below'),
const SizedBox(height: 18),
TextFormField(
controller: _emailController,
decoration: const InputDecoration(labelText: 'Email'),
),
const SizedBox(height: 18),
ElevatedButton(
onPressed: _isLoading ? null : _signIn,
child: Text(_isLoading ? 'Loading' : 'Send Magic Link'),
),
],
),
);
}
}
7.7. lib/pages/account_page.dartの作成
import 'package:flutter/material.dart';
import 'package:supabase_quick_start/components/auth_required_state.dart'; // 自プロジェクト内
import 'package:supabase_quick_start/utils/constants.dart'; // 自プロジェクト内
import 'package:supabase/supabase.dart';
class AccountPage extends StatefulWidget {
const AccountPage({Key? key}) : super(key: key);
@override
_AccountPageState createState() => _AccountPageState();
}
class _AccountPageState extends AuthRequiredState<AccountPage> {
late final _usernameController = TextEditingController();
late final _websiteController = TextEditingController();
var _loading = false;
Future<void> _getProfile(String userId) async {
setState(() {
_loading = true;
});
final response = await supabase
.from('profiles')
.select()
.eq('id', userId)
.single()
.execute();
if (response.error != null && response.status != 406) {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text(response.error!.message)));
}
if (response.data != null) {
_usernameController.text = response.data!['username'] as String;
_websiteController.text = response.data!['website'] as String;
}
setState(() {
_loading = false;
});
}
Future<void> _updateProfile() async {
setState(() {
_loading = true;
});
final userName = _usernameController.text;
final website = _websiteController.text;
final user = supabase.auth.currentUser;
final updates = {
'id': user!.id,
'username': userName,
'website': website,
'updated_at': DateTime.now().toIso8601String(),
};
final response = await supabase.from('profiles').upsert(updates).execute();
if (response.error != null) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(response.error!.message),
backgroundColor: Colors.red,
));
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Successfully updated profile!')));
}
setState(() {
_loading = false;
});
}
Future<void> _signOut() async {
final response = await supabase.auth.signOut();
if (response.error != null) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(response.error!.message),
backgroundColor: Colors.red,
));
}
Navigator.of(context).pushReplacementNamed('/login');
}
@override
void onAuthenticated(Session session) {
final user = session.user;
if (user != null) {
_getProfile(user.id);
}
}
@override
void onUnauthenticated() {
Navigator.of(context).pushReplacementNamed('/login');
}
@override
void dispose() {
_usernameController.dispose();
_websiteController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Profile')),
body: ListView(
padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 12),
children: [
TextFormField(
controller: _usernameController,
decoration: const InputDecoration(labelText: 'User Name'),
),
const SizedBox(height: 18),
TextFormField(
controller: _websiteController,
decoration: const InputDecoration(labelText: 'Website'),
),
const SizedBox(height: 18),
ElevatedButton(
onPressed: _updateProfile,
child: Text(_loading ? 'Saving...' : 'Update')),
const SizedBox(height: 18),
ElevatedButton(onPressed: _signOut, child: const Text('Sign Out')),
],
),
);
}
}
7.8. lib/main.dartの追加編集
import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:supabase_quick_start/pages/account_page.dart'; // 自プロジェクト内
import 'package:supabase_quick_start/pages/login_page.dart'; // 自プロジェクト内
import 'package:supabase_quick_start/pages/splash_page.dart'; // 自プロジェクト内
void main() {
WidgetsFlutterBinding.ensureInitialized();
Supabase.initialize(
url: '[YOUR_SUPABASE_URL]',
anonKey: '[YOUR_SUPABASE_ANNON_KEY]',
);
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Supabase Flutter',
theme: ThemeData.dark().copyWith(
primaryColor: Colors.green,
accentColor: Colors.green,
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
onPrimary: Colors.white,
primary: Colors.green,
),
),
),
initialRoute: '/',
routes: <String, WidgetBuilder>{
'/': (_) => const SplashPage(),
'/login': (_) => const LoginPage(),
'/account': (_) => const AccountPage(),
},
);
}
}
- 実行するとメールアドレスを利用した登録とログインができました。※ メールアドレスを設定している実機で実行してもらうと良いと思います。
▼ 結果
今の所、Flutter×supabaseではSNSログインできないみたいです🤔今後の進展に期待🙋