🔏

Supabaseでログインを維持する

2023/02/24に公開

セッションを使う

Supabaseにはセッションの仕組みを使うとログイン状態を維持することができる機能があるようです。
こちらのコードを使うと実現できます。

公式のログインを維持するコードが書かれたサンプルコード

import 'package:supabase_flutter/supabase_flutter.dart';

final supabase = Supabase.instance.client;

class MyWidget extends StatefulWidget {
  const MyWidget({Key? key}) : super(key: key);

  
  State<MyWidget> createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
  late final StreamSubscription<AuthState> _authSubscription;
  User? _user;

  
  void initState() {
    _authSubscription = supabase.auth.onAuthStateChange.listen((data) {
      final AuthChangeEvent event = data.event;
      final Session? session = data.session;
      setState(() {
        _user = session?.user;
      });
    });
    super.initState();
  }

  
  void dispose() {
    _authSubscription.cancel();
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: () {
        supabase.auth.signInWithOtp(email: 'my_email@example.com');
      },
      child: const Text('Login'),
    );
  }
}

他にも設定が必要な項目がいくつかありますが、公式を参考にしながら設定していこうと思います。

参考にしたサイト

https://supabase.com/docs/guides/getting-started/tutorials/with-flutter
https://pub.dev/packages/supabase_flutter

完成品のソースコード

https://github.com/sakurakotubaki/SupabaseNotesApp/tree/future/auth

Redirect URLs

公式によると、ディープリンクなるものの設定が必要とのことです。
翻訳
依存関係のインストールが完了したので、マジックリンクまたは OAuth 経由でログインしたユーザーがアプリに戻れるように、ディープリンクを設定しましょう。

ダッシュボードに新しいリダイレクト URL として io.supabase.flutterquickstart://login-callback/ を追加します。
URLは何でもいいみたいです。公式のものをそのまま使わせてもらいました。こちらをコピーして、iOSとAndroidの設定で使用します。

このページでディープリンクを作成します。


iOSの設定をする

指定した場所にコードを追加してURLを設定

info.plist

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>CFBundleDevelopmentRegion</key>
	<string>$(DEVELOPMENT_LANGUAGE)</string>
	<key>CFBundleDisplayName</key>
	<string>Supabase Notes</string>
	<key>CFBundleExecutable</key>
	<string>$(EXECUTABLE_NAME)</string>
	<key>CFBundleIdentifier</key>
	<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
	<key>CFBundleInfoDictionaryVersion</key>
	<string>6.0</string>
	<key>CFBundleName</key>
	<string>supabase_notes</string>
	<key>CFBundlePackageType</key>
	<string>APPL</string>
	<key>CFBundleShortVersionString</key>
	<string>$(FLUTTER_BUILD_NAME)</string>
	<key>CFBundleSignature</key>
	<string>????</string>
	<key>CFBundleVersion</key>
	<string>$(FLUTTER_BUILD_NUMBER)</string>
	<key>LSRequiresIPhoneOS</key>
	<true/>
	<key>UILaunchStoryboardName</key>
	<string>LaunchScreen</string>
	<key>UIMainStoryboardFile</key>
	<string>Main</string>
	<key>UISupportedInterfaceOrientations</key>
	<array>
		<string>UIInterfaceOrientationPortrait</string>
		<string>UIInterfaceOrientationLandscapeLeft</string>
		<string>UIInterfaceOrientationLandscapeRight</string>
	</array>
	<key>UISupportedInterfaceOrientations~ipad</key>
	<array>
		<string>UIInterfaceOrientationPortrait</string>
		<string>UIInterfaceOrientationPortraitUpsideDown</string>
		<string>UIInterfaceOrientationLandscapeLeft</string>
		<string>UIInterfaceOrientationLandscapeRight</string>
	</array>
	<key>UIViewControllerBasedStatusBarAppearance</key>
	<false/>
	<key>CADisableMinimumFrameDurationOnPhone</key>
	<true/>
	<key>UIApplicationSupportsIndirectInputEvents</key>
	<true/>
	<!-- Add this array for Deep Links -->
  <key>CFBundleURLTypes</key>
  <array>
    <dict>
      <key>CFBundleTypeRole</key>
      <string>Editor</string>
      <key>CFBundleURLSchemes</key>
      <array>
        <string>io.supabase.flutterquickstart</string>
      </array>
    </dict>
  </array>
  <!-- ... other tags -->
</dict>
</plist>

Androidの設定をする

指定した場所にコードを追加してURLを設定

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.supabase_notes">
   <application
        android:label="supabase_notes"
        android:name="${applicationName}"
        android:icon="@mipmap/ic_launcher">
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:launchMode="singleTop"
            android:theme="@style/LaunchTheme"
            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
            android:hardwareAccelerated="true"
            android:windowSoftInputMode="adjustResize">
            <!-- Specifies an Android theme to apply to this Activity as soon as
                 the Android process has started. This theme is visible to the user
                 while the Flutter UI initializes. After that, this theme continues
                 to determine the Window background behind the Flutter UI. -->
            <meta-data
              android:name="io.flutter.embedding.android.NormalTheme"
              android:resource="@style/NormalTheme"
              />
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>

            <!-- 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="io.supabase.flutterquickstart"
          android:host="login-callback" />
      </intent-filter>
      
        </activity>
        <!-- Don't delete the meta-data below.
             This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
        <meta-data
            android:name="flutterEmbedding"
            android:value="2" />
    </application>
</manifest>

これでOSごとの設定は終わりです。といっても私も手探りで設定したので、人によってはうまくいかないかもしれません?

-----

アプリのコード

今回は、ログインと新規登録してHelloWorldするだけなので、物足りないかもしれないですが、なかなか情報がないので、少しはお役に立てる記事になっているかなと思われます。
.envの設定ですが、必要ない方は、しなくても大丈夫です。
公式の紹介通りに設定してください。

.envの設定方法

https://zenn.dev/flutteruniv_dev/articles/da265483f0073c

URLとANON_KEYを設定する。

import 'package:supabase_flutter/supabase_flutter.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await Supabase.initialize(
    url: SUPABASE_URL,
    anonKey: SUPABASE_ANON_KEY,
  );

  runApp(MyApp());
}

// It's handy to then extract the Supabase client in a variable for later uses
final supabase = Supabase.instance.client;

.envを使った設定

main.dart
import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:supabase_notes/tutorial/my_widget.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  // .envを読み込めるように設定.
  await dotenv.load(fileName: '.env');
  await Supabase.initialize(
    url: dotenv.get('VAR_URL'), // .envのURLを取得.
    anonKey: dotenv.get('VAR_ANONKEY'), // .envのanonキーを取得.
  );
  runApp(ProviderScope(child: MyApp()));
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyWidget(),
    );
  }
}

tutorialディレクトリにアプリのページを作成する。

ユーザーの新規登録とログインを行うページ。このページでログインをしているか判定をします。ログインしていれば、home_page.dartにアプリが起動したときに、リダイレクトします。ログインしていなかったらこちらのページが表示されます。

turorial/my_widget.dart
import 'dart:async';
import 'dart:developer';

import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:supabase_notes/tutorial/home_page.dart';
// Supabaseをインスタンス化する.
final supabase = Supabase.instance.client;

class MyWidget extends StatefulWidget {
  const MyWidget({Key? key}) : super(key: key);

  
  State<MyWidget> createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {

  // ログインと新規登録に使用するTextEditingController.
  TextEditingController _email = TextEditingController();
  TextEditingController _password = TextEditingController();

  //ログイン判定をする変数.
  bool _redirecting = false;

  //ユーザーの新規登録をするメソッド.
  Future<void> signUpUser(String _email, String _password) async {
    final AuthResponse res =
        await supabase.auth.signUp(email: _email, password: _password);
    log(res.toString());
  }

  //ログインをするメソッド.
  Future<void> signInUser(String _email, String _password) async {
    final AuthResponse res = await supabase.auth
        .signInWithPassword(email: _email, password: _password);
    log(res.toString());
  }

  late final StreamSubscription<AuthState> _authSubscription;
  //セッションを使うための変数.
  User? _user;

  // ログインした状態を維持するためのロジック.
  
  void initState() {
    _authSubscription = supabase.auth.onAuthStateChange.listen((data) {
      final AuthChangeEvent event = data.event;
      final Session? session = data.session;
      // ユーザーがログインしているかをifで判定する.
      if (_redirecting) return;
      if (session != null) {
        _redirecting = true;
        //ユーザーがログインしていたら、アプリのページへリダイレクトする.
        Navigator.of(context).pushReplacement(
            MaterialPageRoute(builder: (context) => HomePage()));
      }
      setState(() {
        _user = session?.user;
      });
    });
    super.initState();
  }

  
  void dispose() {
    _authSubscription.cancel();
    _email.dispose();
    _password.dispose();
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('LoginPage'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            TextField(
              controller: _email,
              decoration: InputDecoration(hintText: 'メールアドレスを入力'),
            ),
            TextField(
              controller: _password,
              decoration: InputDecoration(hintText: 'パスワードを入力'),
            ),
            ElevatedButton(
              onPressed: () async {
                // ログイン用ボタン
                signInUser(_email.text, _password.text);
              },
              child: const Text('SignIn'),
            ),
            ElevatedButton(
              // 新規登録用のボタン.
              onPressed: () async {
                signUpUser(_email.text, _password.text);
              },
              child: const Text('SignUp'),
            ),
          ],
        ),
      ),
    );
  }
}

ログイン後のページ

tutorial/home_page.dart
import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:supabase_notes/tutorial/my_widget.dart';

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        actions: [
          IconButton(
              onPressed: () async {
                // ログアウトするボタン.
                await Supabase.instance.client.auth.signOut();
                Navigator.of(context).pushReplacement(
                    MaterialPageRoute(builder: (context) => MyWidget()));
              },
              icon: Icon(Icons.logout)),
        ],
        title: Text('home'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              'Wolcome!',
              style: TextStyle(fontSize: 50),
            )
          ],
        ),
      ),
    );
  }
}



新規登録をするとこのようなログが表示されます。

flutter: {"currentSession":{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjoxNjc3MjQyNTU1LCJzdWIiOiJlOTg0YmRjMS1hNmUwLTQwOWMtOGVhMC1hYmJlMWNmMjgwMjEiLCJlbWFpbCI6ImV4YW1wbGVAY28uanAiLCJwaG9uZSI6IiIsImFwcF9tZXRhZGF0YSI6eyJwcm92aWRlciI6ImVtYWlsIiwicHJvdmlkZXJzIjpbImVtYWlsIl19LCJ1c2VyX21ldGFkYXRhIjp7fSwicm9sZSI6ImF1dGhlbnRpY2F0ZWQiLCJhYWwiOiJhYWwxIiwiYW1yIjpbeyJtZXRob2QiOiJwYXNzd29yZCIsInRpbWVzdGFtcCI6MTY3NzIzODk1NX1dLCJzZXNzaW9uX2lkIjoiZWFjYjQ4YTgtZDEyNi00YzMyLWIzMjktNjM3OGJkYWQyMWZhIn0.CsXYoTlR8ZecMOE34O3dlZD5VEk0eMGuAic5AmfjbUE","expires_in":3600,"refresh_token":"qFHDEJAmbLmvFB1YE8_MWA","token_type":"bearer","provider_token":null,"provider_refresh_token":null,"user":{"id":"e984bdc1-a6e0-409c-8ea0-abbe1cf28021","app_metadata":{"provider":"email","providers":["email"]},"user_metadata":{},"aud":"authenticated","email":"example@co.jp","phone":"","created_at":"2023-02-24T11:42:35.120857Z","confirmed_at":null,"email_confirmed_at":"2023-02-24T11:42:35.1345691<…>
Reloaded 1 of 1235 libraries in 409ms (compile: 45 ms, reload: 230 ms, reassemble: 115 ms).
Reloaded 1 of 1235 libraries in 392ms (compile: 33 ms, reload: 235 ms, reassemble: 105 ms).
Reloaded 1 of 1235 libraries in 348ms (compile: 39 ms, reload: 192 ms, reassemble: 100 ms).

一度アプリを停止して、再起動させてみましょう。Welcomeと表示されていればログインの状態を維持しています。

アプリが起動するとこのようなログが表示されます。

flutter: {"currentSession":{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjoxNjc3MjQyNTU1LCJzdWIiOiJlOTg0YmRjMS1hNmUwLTQwOWMtOGVhMC1hYmJlMWNmMjgwMjEiLCJlbWFpbCI6ImV4YW1wbGVAY28uanAiLCJwaG9uZSI6IiIsImFwcF9tZXRhZGF0YSI6eyJwcm92aWRlciI6ImVtYWlsIiwicHJvdmlkZXJzIjpbImVtYWlsIl19LCJ1c2VyX21ldGFkYXRhIjp7fSwicm9sZSI6ImF1dGhlbnRpY2F0ZWQiLCJhYWwiOiJhYWwxIiwiYW1yIjpbeyJtZXRob2QiOiJwYXNzd29yZCIsInRpbWVzdGFtcCI6MTY3NzIzODk1NX1dLCJzZXNzaW9uX2lkIjoiZWFjYjQ4YTgtZDEyNi00YzMyLWIzMjktNjM3OGJkYWQyMWZhIn0.CsXYoTlR8ZecMOE34O3dlZD5VEk0eMGuAic5AmfjbUE","expires_in":3600,"refresh_token":"qFHDEJAmbLmvFB1YE8_MWA","token_type":"bearer","provider_token":null,"provider_refresh_token":null,"user":{"id":"e984bdc1-a6e0-409c-8ea0-abbe1cf28021","app_metadata":{"provider":"email","providers":["email"]},"user_metadata":{},"aud":"authenticated","email":"example@co.jp","phone":"","created_at":"2023-02-24T11:42:35.120857Z","confirmed_at":null,"email_confirmed_at":"2023-02-24T11:42:35.1345691<…>
flutter: ***** SupabaseDeepLinkingMixin startAuthObserver

ログアウトするとこのようなログが表示されます。
logout

flutter: ***** SupabaseDeepLinkingMixin startAuthObserver
flutter: **** onAuthStateChange: AuthChangeEvent.signedOut

ログインするとこのようなログが表示されます。
login

flutter: {"currentSession":{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjoxNjc3MjQzMDQ0LCJzdWIiOiJlOTg0YmRjMS1hNmUwLTQwOWMtOGVhMC1hYmJlMWNmMjgwMjEiLCJlbWFpbCI6ImV4YW1wbGVAY28uanAiLCJwaG9uZSI6IiIsImFwcF9tZXRhZGF0YSI6eyJwcm92aWRlciI6ImVtYWlsIiwicHJvdmlkZXJzIjpbImVtYWlsIl19LCJ1c2VyX21ldGFkYXRhIjp7fSwicm9sZSI6ImF1dGhlbnRpY2F0ZWQiLCJhYWwiOiJhYWwxIiwiYW1yIjpbeyJtZXRob2QiOiJwYXNzd29yZCIsInRpbWVzdGFtcCI6MTY3NzIzOTQ0NH1dLCJzZXNzaW9uX2lkIjoiNTgwZDgwNWQtM2Q3ZS00ZTRlLWE2ODYtMWM4NTg3NjE5OTg3In0.a658VnrX1OLWLrXZ83_4_WoaLUFkOfcVZcwH9VKj_xs","expires_in":3600,"refresh_token":"KpK1CdpU82VT7o_U4lbBcg","token_type":"bearer","provider_token":null,"provider_refresh_token":null,"user":{"id":"e984bdc1-a6e0-409c-8ea0-abbe1cf28021","app_metadata":{"provider":"email","providers":["email"]},"user_metadata":{},"aud":"authenticated","email":"example@co.jp","phone":"","created_at":"2023-02-24T11:42:35.120857Z","confirmed_at":"2023-02-24T11:42:35.134569Z","email_confirmed_at":"20<…>

最後に

あまり情報がないので、思いつきで作ってたまたまログインを維持する機能を実装できました。Supabaseは出たばかりの技術ですが、人気が出ていて今後伸びていく技術ではないかと思います。
認証機能を自分で作らなくても使えて、ログイン状態を維持できるので、これほど便利なサービスはないですね。
今年の3月21日の火曜日にFlutter大学のイベントで私、登壇する予定なのですが、SupabaseをテーマにしたFlutterの発表を行う予定です。
https://flutteruniv.connpass.com/event/275584/

Flutter大学

Discussion