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

こんにちは、株式会社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アカウントが必要です。

  1. superbaseのプロジェクトを作る
    1.1. https://app.supabase.io/ へアクセス
    1.2. チームを作成
    https://assets.st-note.com/production/uploads/images/58717088/picture_pc_39aa1b8ab29599c81fba172e12c65cde.png?width=800

    1.3. プロジェクトを作成
    https://assets.st-note.com/production/uploads/images/58716660/picture_pc_a42d551188a55351826a7a7706b10dda.png?width=800

  2. SQLを設定
    2.1. プロジェクト詳細に移動
    2.2. SQLへ移動
    https://assets.st-note.com/production/uploads/images/58717056/picture_pc_0edfcbb4ef12f1151ab0118582e07088.png?width=800

    2.3. User Management Starterへ移動
    https://assets.st-note.com/production/uploads/images/58717109/picture_pc_ddbcaae616a42a4c5fd28dd5b119a4b7.png?width=800

    2.4. Runを実行
    https://assets.st-note.com/production/uploads/images/58717116/picture_pc_a4d97ffa7050c5fe964195a4b78ef183.png?width=800

3.  設定の確認
3.1. Settingsへ移動
https://assets.st-note.com/production/uploads/images/58717142/picture_pc_5a31c919b252b085adf6bcc2c6314d0e.png?width=800

3.2. サイドバーのAPIへ移動→こちらの情報を参照する
https://assets.st-note.com/production/uploads/images/58717144/picture_pc_7b4d4de93773a76f22061e6b8c597a1f.png?width=800

  1. 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」を実行する

  2. Authenticationの設定
    5.1. Authenticationへ移動
    https://assets.st-note.com/production/uploads/images/58717166/picture_pc_ea8bf234372169521cffd1d1e03ca218.png?width=800

    5.2. サイドバーのSettingへ移動5.3. Additional Redirect URLsに今回作成するFlutterプロジェクトのbundle identiferを利用したスキームURLを入力する、今回の場合、「tokyo.pentagon.supabase_quick_start://login-callback/」としました
    https://assets.st-note.com/production/uploads/images/58717176/picture_pc_bcb9d038d571709098bc1b39a8d4a76b.png?width=800

  3. 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>
  1. 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(),
     },
   );
 }
}
  1. 実行するとメールアドレスを利用した登録とログインができました。※ メールアドレスを設定している実機で実行してもらうと良いと思います。

    ▼ 結果

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

採用情報はこちら
目次