見出し画像

Flutter×supabaseのログイン調査してみた

こんにちは、株式会社Pentagonでアプリ開発をしている石渡港です。

https://pentagon.tokyo

経緯
https://supabase.io/blog/2021/07/28/supabase-auth-passwordless-sms-login
FlutterでもSMSログインできるかな?🤔

今日は、Flutter×supabaseのログイン調査してみた について簡単にまとめたいと思います。

▼ 参考

https://supabase.io/docs/guides/with-flutter

▼ 手順

※ サクッと動きを確認したい方はこちら
superbaseを利用するにはGitHubアカウントが必要です。
1. superbaseのプロジェクトを作る
1.1. https://app.supabase.io/へアクセス
1.2. チームを作成

画像3


1.3. プロジェクトを作成

画像1

2. SQLを設定
2.1. プロジェクト詳細に移動
2.2. SQLへ移動

画像2


2.3. User Management Starterへ移動

画像4


2.4. Runを実行

画像5


3.  設定の確認
3.1. Settingsへ移動

画像6


3.2. サイドバーのAPIへ移動→こちらの情報を参照する

画像7


4. Flutterのプラグインの設定 ※ Flutter2.0で作成
4.1. 「supabase_flutter: ^0.0.6」を「pubspec.yaml」の「dependencies:」下に記載する ※ 0.1.0の場合はAPIが違うため注意
https://pub.dev/packages/supabase_flutter
4.2. 「pub get」を実行する
5. Authenticationの設定
5.1. Authenticationへ移動

画像8


5.2. サイドバーのSettingへ移動
5.3. Additional Redirect URLsに今回作成するFlutterプロジェクトのbundle identiferを利用したスキームURLを入力する、今回の場合、「tokyo.pentagon.supabase_quick_start://login-callback/」としました

画像9


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


7. 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(),
     },
   );
 }
}

8. 実行
するとメールアドレスを利用した登録とログインができました。※ メールアドレスを設定している実機で実行してもらうと良いと思います。

▼ 結果

今の所、Flutter×supabaseではSNSログインできないみたいです🤔
今後の進展に期待🙋

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