mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
guide: update flutter user management guide with new code
This commit is contained in:
1 parent
1bf0e5e969
commit
ee83945d53
2 files changed
+207
-146
No files matched your search
@@ -34,16 +34,20 @@ Then let's install the only additional dependency: [`supabase_flutter`](https://
|
||||
Copy and paste the following line in your pubspec.yaml to install the package:
|
||||
|
||||
```yaml
|
||||
supabase_flutter: ^1.0.0
|
||||
supabase_flutter: ^1.10.3
|
||||
```
|
||||
|
||||
Run `flutter pub get` to install the dependencies.
|
||||
|
||||
### Setup deep links
|
||||
|
||||
Now that we have the dependencies installed let's setup deep links so users who have logged in via magic link or OAuth can come back to the app.
|
||||
Now that we have the dependencies installed let's setup deep links.
|
||||
Setting up deep links is required to bring back the user to the app when they click on the magic link to sign in.
|
||||
We can setup deep links with just a minor tweak on our Flutter application.
|
||||
|
||||
Add `io.supabase.flutterquickstart://login-callback/` as a new [redirect URL](https://app.supabase.com/project/_/auth/url-configuration) in the Dashboard.
|
||||
We will use `io.supabase.flutterquickstart` as the scheme, and `login-callback` as the host for our deep link in this example, but you can change it to whatever you would like.
|
||||
|
||||
First, add `io.supabase.flutterquickstart://login-callback/` as a new [redirect URL](https://app.supabase.com/project/_/auth/url-configuration) in the Dashboard.
|
||||
|
||||

|
||||
|
||||
@@ -115,44 +119,19 @@ These variables will be exposed on the app, and that's completely fine since we
|
||||
|
||||
```dart lib/main.dart
|
||||
Future<void> main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
await Supabase.initialize(
|
||||
url: 'YOUR_SUPABASE_URL',
|
||||
anonKey: 'YOUR_SUPABASE_ANON_KEY',
|
||||
authFlowType: AuthFlowType.pkce,
|
||||
);
|
||||
runApp(MyApp());
|
||||
}
|
||||
```
|
||||
|
||||
### Setting up some constants and handy functions
|
||||
|
||||
Let's also create a constant file to make it easier to use Supabase client.
|
||||
We will also include an extension method declaration to call `showSnackBar` with one line of code.
|
||||
|
||||
```dart lib/constants.dart
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
|
||||
final supabase = Supabase.instance.client;
|
||||
|
||||
extension ShowSnackBar on BuildContext {
|
||||
void showSnackBar({
|
||||
required String message,
|
||||
Color backgroundColor = Colors.white,
|
||||
}) {
|
||||
ScaffoldMessenger.of(this).showSnackBar(SnackBar(
|
||||
content: Text(message),
|
||||
backgroundColor: backgroundColor,
|
||||
));
|
||||
}
|
||||
|
||||
void showErrorSnackBar({required String message}) {
|
||||
showSnackBar(message: message, backgroundColor: Colors.red);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`AuthFlowType.pkce` on `authFlowType` parameter indicates that we are using a secure [PKCE flow](https://supabase.com/blog/supabase-auth-sso-pkce#introducing-pkce) to perform our magic link login.
|
||||
|
||||
### Set up Splash Screen
|
||||
|
||||
Let's create a splash screen that will be shown to users right after they open the app.
|
||||
@@ -160,7 +139,7 @@ This screen retrieves the current session and redirects the user accordingly.
|
||||
|
||||
```dart lib/pages/splash_page.dart
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:supabase_quickstart/constants.dart';
|
||||
import 'package:supabase_quickstart/main.dart';
|
||||
|
||||
class SplashPage extends StatefulWidget {
|
||||
const SplashPage({super.key});
|
||||
@@ -199,7 +178,6 @@ class _SplashPageState extends State<SplashPage> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Set up a Login page
|
||||
@@ -215,8 +193,7 @@ import 'dart:async';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
|
||||
import 'package:supabase_quickstart/constants.dart';
|
||||
import 'package:supabase_quickstart/main.dart';
|
||||
|
||||
class LoginPage extends StatefulWidget {
|
||||
const LoginPage({super.key});
|
||||
@@ -228,37 +205,46 @@ class LoginPage extends StatefulWidget {
|
||||
class _LoginPageState extends State<LoginPage> {
|
||||
bool _isLoading = false;
|
||||
bool _redirecting = false;
|
||||
late final TextEditingController _emailController;
|
||||
late final TextEditingController _emailController = TextEditingController();
|
||||
late final StreamSubscription<AuthState> _authStateSubscription;
|
||||
|
||||
Future<void> _signIn() async {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
});
|
||||
try {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
});
|
||||
await supabase.auth.signInWithOtp(
|
||||
email: _emailController.text.trim(),
|
||||
emailRedirectTo:
|
||||
kIsWeb ? null : 'io.supabase.flutterquickstart://login-callback/',
|
||||
);
|
||||
if (mounted) {
|
||||
context.showSnackBar(message: 'Check your email for login link!');
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Check your email for a login link!')),
|
||||
);
|
||||
_emailController.clear();
|
||||
}
|
||||
} on AuthException catch (error) {
|
||||
context.showErrorSnackBar(message: error.message);
|
||||
SnackBar(
|
||||
content: Text(error.message),
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
);
|
||||
} catch (error) {
|
||||
context.showErrorSnackBar(message: 'Unexpected error occurred');
|
||||
SnackBar(
|
||||
content: const Text('Unexpected error occurred'),
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
);
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_emailController = TextEditingController();
|
||||
_authStateSubscription = supabase.auth.onAuthStateChange.listen((data) {
|
||||
if (_redirecting) return;
|
||||
final session = data.session;
|
||||
@@ -310,8 +296,7 @@ Let's create a new widget called `account_page.dart` for that.
|
||||
```dart lib/pages/account_page.dart"
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import 'package:supabase_quickstart/components/avatar.dart';
|
||||
import 'package:supabase_quickstart/constants.dart';
|
||||
import 'package:supabase_quickstart/main.dart';
|
||||
|
||||
class AccountPage extends StatefulWidget {
|
||||
const AccountPage({super.key});
|
||||
@@ -323,8 +308,8 @@ class AccountPage extends StatefulWidget {
|
||||
class _AccountPageState extends State<AccountPage> {
|
||||
final _usernameController = TextEditingController();
|
||||
final _websiteController = TextEditingController();
|
||||
String? _avatarUrl;
|
||||
var _loading = false;
|
||||
|
||||
var _loading = true;
|
||||
|
||||
/// Called once a user id is received within `onAuthenticated()`
|
||||
Future<void> _getProfile() async {
|
||||
@@ -336,21 +321,28 @@ class _AccountPageState extends State<AccountPage> {
|
||||
final userId = supabase.auth.currentUser!.id;
|
||||
final data = await supabase
|
||||
.from('profiles')
|
||||
.select()
|
||||
.select<Map<String, dynamic>>()
|
||||
.eq('id', userId)
|
||||
.single() as Map;
|
||||
.single();
|
||||
_usernameController.text = (data['username'] ?? '') as String;
|
||||
_websiteController.text = (data['website'] ?? '') as String;
|
||||
_avatarUrl = (data['avatar_url'] ?? '') as String;
|
||||
} on PostgrestException catch (error) {
|
||||
context.showErrorSnackBar(message: error.message);
|
||||
SnackBar(
|
||||
content: Text(error.message),
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
);
|
||||
} catch (error) {
|
||||
context.showErrorSnackBar(message: 'Unexpected exception occurred');
|
||||
SnackBar(
|
||||
content: const Text('Unexpected error occurred'),
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
);
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_loading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
/// Called when user taps `Update` button
|
||||
@@ -370,28 +362,46 @@ class _AccountPageState extends State<AccountPage> {
|
||||
try {
|
||||
await supabase.from('profiles').upsert(updates);
|
||||
if (mounted) {
|
||||
context.showSnackBar(message: 'Successfully updated profile!');
|
||||
const SnackBar(
|
||||
content: Text('Successfully updated profile!'),
|
||||
);
|
||||
}
|
||||
} on PostgrestException catch (error) {
|
||||
context.showErrorSnackBar(message: error.message);
|
||||
SnackBar(
|
||||
content: Text(error.message),
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
);
|
||||
} catch (error) {
|
||||
context.showErrorSnackBar(message: 'Unexpeted error occurred');
|
||||
SnackBar(
|
||||
content: const Text('Unexpected error occurred'),
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
);
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_loading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
setState(() {
|
||||
_loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _signOut() async {
|
||||
try {
|
||||
await supabase.auth.signOut();
|
||||
} on AuthException catch (error) {
|
||||
context.showErrorSnackBar(message: error.message);
|
||||
SnackBar(
|
||||
content: Text(error.message),
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
);
|
||||
} catch (error) {
|
||||
context.showErrorSnackBar(message: 'Unexpected error occurred');
|
||||
}
|
||||
if (mounted) {
|
||||
Navigator.of(context).pushReplacementNamed('/');
|
||||
SnackBar(
|
||||
content: const Text('Unexpected error occurred'),
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
);
|
||||
} finally {
|
||||
if (mounted) {
|
||||
Navigator.of(context).pushReplacementNamed('/login');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -412,27 +422,29 @@ class _AccountPageState extends State<AccountPage> {
|
||||
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),
|
||||
TextButton(onPressed: _signOut, child: const Text('Sign Out')),
|
||||
],
|
||||
),
|
||||
body: _loading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: 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: _loading ? null : _updateProfile,
|
||||
child: Text(_loading ? 'Saving...' : 'Update'),
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
TextButton(onPressed: _signOut, child: const Text('Sign Out')),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -451,13 +463,14 @@ import 'package:supabase_quickstart/pages/splash_page.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
await Supabase.initialize(
|
||||
// TODO: Replace credentials with your own
|
||||
url: 'YOUR_SUPABASE_URL',
|
||||
anonKey: 'YOUR_SUPABASE_ANON_KEY',
|
||||
);
|
||||
runApp(MyApp());
|
||||
}
|
||||
|
||||
final supabase = Supabase.instance.client;
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -539,7 +552,7 @@ We can start by creating a new component:
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import 'package:supabase_quickstart/constants.dart';
|
||||
import 'package:supabase_quickstart/main.dart';
|
||||
|
||||
class Avatar extends StatefulWidget {
|
||||
const Avatar({
|
||||
@@ -614,11 +627,21 @@ class _AvatarState extends State<Avatar> {
|
||||
widget.onUpload(imageUrlResponse);
|
||||
} on StorageException catch (error) {
|
||||
if (mounted) {
|
||||
context.showErrorSnackBar(message: error.message);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(error.message),
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
if (mounted) {
|
||||
context.showErrorSnackBar(message: 'Unexpected error occurred');
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: const Text('Unexpected error occurred'),
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -635,7 +658,7 @@ And then we can add the widget to the Account page as well as some logic to upda
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import 'package:supabase_quickstart/components/avatar.dart';
|
||||
import 'package:supabase_quickstart/constants.dart';
|
||||
import 'package:supabase_quickstart/main.dart';
|
||||
|
||||
class AccountPage extends StatefulWidget {
|
||||
const AccountPage({super.key});
|
||||
@@ -647,8 +670,9 @@ class AccountPage extends StatefulWidget {
|
||||
class _AccountPageState extends State<AccountPage> {
|
||||
final _usernameController = TextEditingController();
|
||||
final _websiteController = TextEditingController();
|
||||
|
||||
String? _avatarUrl;
|
||||
var _loading = false;
|
||||
var _loading = true;
|
||||
|
||||
/// Called once a user id is received within `onAuthenticated()`
|
||||
Future<void> _getProfile() async {
|
||||
@@ -660,21 +684,29 @@ class _AccountPageState extends State<AccountPage> {
|
||||
final userId = supabase.auth.currentUser!.id;
|
||||
final data = await supabase
|
||||
.from('profiles')
|
||||
.select()
|
||||
.select<Map<String, dynamic>>()
|
||||
.eq('id', userId)
|
||||
.single() as Map;
|
||||
.single();
|
||||
_usernameController.text = (data['username'] ?? '') as String;
|
||||
_websiteController.text = (data['website'] ?? '') as String;
|
||||
_avatarUrl = (data['avatar_url'] ?? '') as String;
|
||||
} on PostgrestException catch (error) {
|
||||
context.showErrorSnackBar(message: error.message);
|
||||
SnackBar(
|
||||
content: Text(error.message),
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
);
|
||||
} catch (error) {
|
||||
context.showErrorSnackBar(message: 'Unexpected exception occurred');
|
||||
SnackBar(
|
||||
content: const Text('Unexpected error occurred'),
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
);
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_loading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
/// Called when user taps `Update` button
|
||||
@@ -694,28 +726,46 @@ class _AccountPageState extends State<AccountPage> {
|
||||
try {
|
||||
await supabase.from('profiles').upsert(updates);
|
||||
if (mounted) {
|
||||
context.showSnackBar(message: 'Successfully updated profile!');
|
||||
const SnackBar(
|
||||
content: Text('Successfully updated profile!'),
|
||||
);
|
||||
}
|
||||
} on PostgrestException catch (error) {
|
||||
context.showErrorSnackBar(message: error.message);
|
||||
SnackBar(
|
||||
content: Text(error.message),
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
);
|
||||
} catch (error) {
|
||||
context.showErrorSnackBar(message: 'Unexpeted error occurred');
|
||||
SnackBar(
|
||||
content: const Text('Unexpected error occurred'),
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
);
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_loading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
setState(() {
|
||||
_loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _signOut() async {
|
||||
try {
|
||||
await supabase.auth.signOut();
|
||||
} on AuthException catch (error) {
|
||||
context.showErrorSnackBar(message: error.message);
|
||||
SnackBar(
|
||||
content: Text(error.message),
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
);
|
||||
} catch (error) {
|
||||
context.showErrorSnackBar(message: 'Unexpected error occurred');
|
||||
}
|
||||
if (mounted) {
|
||||
Navigator.of(context).pushReplacementNamed('/');
|
||||
SnackBar(
|
||||
content: const Text('Unexpected error occurred'),
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
);
|
||||
} finally {
|
||||
if (mounted) {
|
||||
Navigator.of(context).pushReplacementNamed('/login');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -728,12 +778,20 @@ class _AccountPageState extends State<AccountPage> {
|
||||
'avatar_url': imageUrl,
|
||||
});
|
||||
if (mounted) {
|
||||
context.showSnackBar(message: 'Updated your profile image!');
|
||||
const SnackBar(
|
||||
content: Text('Updated your profile image!'),
|
||||
);
|
||||
}
|
||||
} on PostgrestException catch (error) {
|
||||
context.showErrorSnackBar(message: error.message);
|
||||
SnackBar(
|
||||
content: Text(error.message),
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
);
|
||||
} catch (error) {
|
||||
context.showErrorSnackBar(message: 'Unexpected error has occurred');
|
||||
SnackBar(
|
||||
content: const Text('Unexpected error occurred'),
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
);
|
||||
}
|
||||
if (!mounted) {
|
||||
return;
|
||||
@@ -761,32 +819,34 @@ class _AccountPageState extends State<AccountPage> {
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Profile')),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 12),
|
||||
children: [
|
||||
Avatar(
|
||||
imageUrl: _avatarUrl,
|
||||
onUpload: _onUpload,
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
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),
|
||||
TextButton(onPressed: _signOut, child: const Text('Sign Out')),
|
||||
],
|
||||
),
|
||||
body: _loading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: ListView(
|
||||
padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 12),
|
||||
children: [
|
||||
Avatar(
|
||||
imageUrl: _avatarUrl,
|
||||
onUpload: _onUpload,
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
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: _loading ? null : _updateProfile,
|
||||
child: Text(_loading ? 'Saving...' : 'Update'),
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
TextButton(onPressed: _signOut, child: const Text('Sign Out')),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ Future<void> main() async {
|
||||
// TODO: Replace credentials with your own
|
||||
url: 'YOUR_SUPABASE_URL',
|
||||
anonKey: 'YOUR_SUPABASE_ANON_KEY',
|
||||
authFlowType: AuthFlowType.pkce,
|
||||
);
|
||||
runApp(MyApp());
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user