mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
* Refactor login_page with a _showSnackBar function * Add an onError block to onAuthStateChange Errors get swallowed without this error handler, I noticed it when I had requested too many magic link emails, there was no feedback in the ui, nor in the logs in the console. * Fix SnackBars not being displayed and add a _showSnackBar function * Fix code samples for login_page.dart and account_page.dart to reflect fixes in #27104 * Fix isError not being passed for an Unexpected Error in login_page * Update flutter user management example to a more modern code * Update the guide to match the new content * run formatter * got rid of excess code and minor format fix --------- Co-authored-by: dshukertjr <dshukertjr@gmail.com>
This commit is contained in:
1 parent
fa25402482
commit
33c7e812c4
10 files changed
+226
-388
No files matched your search
@@ -125,9 +125,7 @@ There are no additional configurations.
|
||||
|
||||
### Main function
|
||||
|
||||
Now that we have deep links ready let's initialize the Supabase client inside our `main` function with the API credentials that you copied [earlier](#get-the-api-keys).
|
||||
These variables will be exposed on the app, and that's completely fine since we have
|
||||
[Row Level Security](/docs/guides/auth#row-level-security) enabled on our Database.
|
||||
Now that we have deep links ready let's initialize the Supabase client inside our `main` function with the API credentials that you copied [earlier](#get-the-api-keys). These variables will be exposed on the app, and that's completely fine since we have [Row Level Security](/docs/guides/auth#row-level-security) enabled on our Database.
|
||||
|
||||
```dart lib/main.dart
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -151,60 +149,28 @@ class MyApp extends StatelessWidget {
|
||||
return const MaterialApp(title: 'Supabase Flutter');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Set up splash screen
|
||||
|
||||
Let's create a splash screen that will be shown to users right after they open the app.
|
||||
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/main.dart';
|
||||
|
||||
class SplashPage extends StatefulWidget {
|
||||
const SplashPage({super.key});
|
||||
|
||||
@override
|
||||
State<SplashPage> createState() => _SplashPageState();
|
||||
}
|
||||
|
||||
class _SplashPageState extends State<SplashPage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_redirect();
|
||||
}
|
||||
|
||||
Future<void> _redirect() async {
|
||||
await Future.delayed(Duration.zero);
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
final session = supabase.auth.currentSession;
|
||||
if (session != null) {
|
||||
Navigator.of(context).pushReplacementNamed('/account');
|
||||
} else {
|
||||
Navigator.of(context).pushReplacementNamed('/login');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Scaffold(
|
||||
body: Center(child: CircularProgressIndicator()),
|
||||
extension ContextExtension on BuildContext {
|
||||
void showSnackBar(String message, {bool isError = false}) {
|
||||
ScaffoldMessenger.of(this).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(message),
|
||||
backgroundColor: isError
|
||||
? Theme.of(this).colorScheme.error
|
||||
: Theme.of(this).snackBarTheme.backgroundColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Notice that we have a `showSnackBar` extension method that we will use to show snack bars in the app. You could define this method in a separate file and import it where needed, but for simplicity, we will define it here.
|
||||
|
||||
### Set up a login page
|
||||
|
||||
Let's create a Flutter widget to manage logins and sign ups.
|
||||
We'll use Magic Links, so users can sign in with their email without using passwords.
|
||||
Notice that this page sets up a listener on the user's auth state using `onAuthStateChange`.
|
||||
A new event will fire when the user comes back to the app by clicking their magic link, which this page can catch and redirect the user accordingly.
|
||||
Let's create a Flutter widget to manage logins and sign ups. We will use Magic Links, so users can sign in with their email without using passwords.
|
||||
|
||||
Notice that this page sets up a listener on the user's auth state using `onAuthStateChange`. A new event will fire when the user comes back to the app by clicking their magic link, which this page can catch and redirect the user accordingly.
|
||||
|
||||
```dart lib/pages/login_page.dart
|
||||
import 'dart:async';
|
||||
@@ -213,6 +179,7 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import 'package:supabase_quickstart/main.dart';
|
||||
import 'package:supabase_quickstart/pages/account_page.dart';
|
||||
|
||||
class LoginPage extends StatefulWidget {
|
||||
const LoginPage({super.key});
|
||||
@@ -238,24 +205,15 @@ class _LoginPageState extends State<LoginPage> {
|
||||
kIsWeb ? null : 'io.supabase.flutterquickstart://login-callback/',
|
||||
);
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Check your email for a login link!')),
|
||||
);
|
||||
context.showSnackBar('Check your email for a login link!');
|
||||
|
||||
_emailController.clear();
|
||||
}
|
||||
} on AuthException catch (error) {
|
||||
if (mounted) {
|
||||
SnackBar(
|
||||
content: Text(error.message),
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
);
|
||||
}
|
||||
if (mounted) context.showSnackBar(error.message, isError: true);
|
||||
} catch (error) {
|
||||
if (mounted) {
|
||||
SnackBar(
|
||||
content: const Text('Unexpected error occurred'),
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
);
|
||||
context.showSnackBar('Unexpected error occurred', isError: true);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
@@ -268,14 +226,25 @@ class _LoginPageState extends State<LoginPage> {
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_authStateSubscription = supabase.auth.onAuthStateChange.listen((data) {
|
||||
if (_redirecting) return;
|
||||
final session = data.session;
|
||||
if (session != null) {
|
||||
_redirecting = true;
|
||||
Navigator.of(context).pushReplacementNamed('/account');
|
||||
}
|
||||
});
|
||||
_authStateSubscription = supabase.auth.onAuthStateChange.listen(
|
||||
(data) {
|
||||
if (_redirecting) return;
|
||||
final session = data.session;
|
||||
if (session != null) {
|
||||
_redirecting = true;
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(builder: (context) => const AccountPage()),
|
||||
);
|
||||
}
|
||||
},
|
||||
onError: (error) {
|
||||
if (error is AuthException) {
|
||||
context.showSnackBar(error.message, isError: true);
|
||||
} else {
|
||||
context.showSnackBar('Unexpected error occurred', isError: true);
|
||||
}
|
||||
},
|
||||
);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@@ -302,7 +271,7 @@ class _LoginPageState extends State<LoginPage> {
|
||||
const SizedBox(height: 18),
|
||||
ElevatedButton(
|
||||
onPressed: _isLoading ? null : _signIn,
|
||||
child: Text(_isLoading ? 'Loading' : 'Send Magic Link'),
|
||||
child: Text(_isLoading ? 'Sending...' : 'Send Magic Link'),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -320,6 +289,7 @@ Let's create a new widget called `account_page.dart` for that.
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import 'package:supabase_quickstart/main.dart';
|
||||
import 'package:supabase_quickstart/pages/login_page.dart';
|
||||
|
||||
class AccountPage extends StatefulWidget {
|
||||
const AccountPage({super.key});
|
||||
@@ -332,6 +302,7 @@ class _AccountPageState extends State<AccountPage> {
|
||||
final _usernameController = TextEditingController();
|
||||
final _websiteController = TextEditingController();
|
||||
|
||||
String? _avatarUrl;
|
||||
var _loading = true;
|
||||
|
||||
/// Called once a user id is received within `onAuthenticated()`
|
||||
@@ -341,24 +312,17 @@ class _AccountPageState extends State<AccountPage> {
|
||||
});
|
||||
|
||||
try {
|
||||
final userId = supabase.auth.currentUser!.id;
|
||||
final userId = supabase.auth.currentSession!.user.id;
|
||||
final data =
|
||||
await supabase.from('profiles').select().eq('id', userId).single();
|
||||
_usernameController.text = (data['username'] ?? '') as String;
|
||||
_websiteController.text = (data['website'] ?? '') as String;
|
||||
_avatarUrl = (data['avatar_url'] ?? '') as String;
|
||||
} on PostgrestException catch (error) {
|
||||
if (mounted) {
|
||||
SnackBar(
|
||||
content: Text(error.message),
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
);
|
||||
}
|
||||
if (mounted) context.showSnackBar(error.message, isError: true);
|
||||
} catch (error) {
|
||||
if (mounted) {
|
||||
SnackBar(
|
||||
content: const Text('Unexpected error occurred'),
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
);
|
||||
context.showSnackBar('Unexpected error occurred', isError: true);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
@@ -385,24 +349,12 @@ class _AccountPageState extends State<AccountPage> {
|
||||
};
|
||||
try {
|
||||
await supabase.from('profiles').upsert(updates);
|
||||
if (mounted) {
|
||||
const SnackBar(
|
||||
content: Text('Successfully updated profile!'),
|
||||
);
|
||||
}
|
||||
if (mounted) context.showSnackBar('Successfully updated profile!');
|
||||
} on PostgrestException catch (error) {
|
||||
if (mounted) {
|
||||
SnackBar(
|
||||
content: Text(error.message),
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
);
|
||||
}
|
||||
if (mounted) context.showSnackBar(error.message, isError: true);
|
||||
} catch (error) {
|
||||
if (mounted) {
|
||||
SnackBar(
|
||||
content: const Text('Unexpected error occurred'),
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
);
|
||||
context.showSnackBar('Unexpected error occurred', isError: true);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
@@ -417,22 +369,16 @@ class _AccountPageState extends State<AccountPage> {
|
||||
try {
|
||||
await supabase.auth.signOut();
|
||||
} on AuthException catch (error) {
|
||||
if (mounted) {
|
||||
SnackBar(
|
||||
content: Text(error.message),
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
);
|
||||
}
|
||||
if (mounted) context.showSnackBar(error.message, isError: true);
|
||||
} catch (error) {
|
||||
if (mounted) {
|
||||
SnackBar(
|
||||
content: const Text('Unexpected error occurred'),
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
);
|
||||
context.showSnackBar('Unexpected error occurred', isError: true);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
Navigator.of(context).pushReplacementNamed('/login');
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(builder: (_) => const LoginPage()),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -454,29 +400,27 @@ class _AccountPageState extends State<AccountPage> {
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Profile')),
|
||||
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')),
|
||||
],
|
||||
),
|
||||
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: _loading ? null : _updateProfile,
|
||||
child: Text(_loading ? 'Saving...' : 'Update'),
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
TextButton(onPressed: _signOut, child: const Text('Sign Out')),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -484,14 +428,15 @@ class _AccountPageState extends State<AccountPage> {
|
||||
|
||||
### Launch!
|
||||
|
||||
Now that we have all the components in place, let's update `lib/main.dart`:
|
||||
Now that we have all the components in place, let's update `lib/main.dart`.
|
||||
The `home` of the `MaterialApp`, meaning the initial page shown to the user, will be the `LoginPage` if the user is not authenticated, and the `AccountPage` if the user is authenticated.
|
||||
We also included some theming to make the app look a bit nicer.
|
||||
|
||||
```dart lib/main.dart
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import 'package:supabase_quickstart/pages/account_page.dart';
|
||||
import 'package:supabase_quickstart/pages/login_page.dart';
|
||||
import 'package:supabase_quickstart/pages/splash_page.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
await Supabase.initialize(
|
||||
@@ -524,12 +469,22 @@ class MyApp extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
initialRoute: '/',
|
||||
routes: <String, WidgetBuilder>{
|
||||
'/': (_) => const SplashPage(),
|
||||
'/login': (_) => const LoginPage(),
|
||||
'/account': (_) => const AccountPage(),
|
||||
},
|
||||
home: supabase.auth.currentSession == null
|
||||
? const LoginPage()
|
||||
: const AccountPage(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension ContextExtension on BuildContext {
|
||||
void showSnackBar(String message, {bool isError = false}) {
|
||||
ScaffoldMessenger.of(this).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(message),
|
||||
backgroundColor: isError
|
||||
? Theme.of(this).colorScheme.error
|
||||
: Theme.of(this).snackBarTheme.backgroundColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -661,21 +616,11 @@ class _AvatarState extends State<Avatar> {
|
||||
widget.onUpload(imageUrlResponse);
|
||||
} on StorageException catch (error) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(error.message),
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
);
|
||||
context.showSnackBar(error.message, isError: true);
|
||||
}
|
||||
} catch (error) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: const Text('Unexpected error occurred'),
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
);
|
||||
context.showSnackBar('Unexpected error occurred', isError: true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -691,8 +636,8 @@ And then we can add the widget to the Account page as well as some logic to upda
|
||||
```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/main.dart';
|
||||
import 'package:supabase_quickstart/pages/login_page.dart';
|
||||
|
||||
class AccountPage extends StatefulWidget {
|
||||
const AccountPage({super.key});
|
||||
@@ -716,27 +661,16 @@ class _AccountPageState extends State<AccountPage> {
|
||||
|
||||
try {
|
||||
final userId = supabase.auth.currentSession!.user.id;
|
||||
final data = await supabase
|
||||
.from('profiles')
|
||||
.select()
|
||||
.eq('id', userId)
|
||||
.single();
|
||||
final data =
|
||||
await supabase.from('profiles').select().eq('id', userId).single();
|
||||
_usernameController.text = (data['username'] ?? '') as String;
|
||||
_websiteController.text = (data['website'] ?? '') as String;
|
||||
_avatarUrl = (data['avatar_url'] ?? '') as String;
|
||||
} on PostgrestException catch (error) {
|
||||
if (mounted) {
|
||||
SnackBar(
|
||||
content: Text(error.message),
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
);
|
||||
}
|
||||
if (mounted) context.showSnackBar(error.message, isError: true);
|
||||
} catch (error) {
|
||||
if (mounted) {
|
||||
SnackBar(
|
||||
content: const Text('Unexpected error occurred'),
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
);
|
||||
context.showSnackBar('Unexpected error occurred', isError: true);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
@@ -763,24 +697,12 @@ class _AccountPageState extends State<AccountPage> {
|
||||
};
|
||||
try {
|
||||
await supabase.from('profiles').upsert(updates);
|
||||
if (mounted) {
|
||||
const SnackBar(
|
||||
content: Text('Successfully updated profile!'),
|
||||
);
|
||||
}
|
||||
if (mounted) context.showSnackBar('Successfully updated profile!');
|
||||
} on PostgrestException catch (error) {
|
||||
if (mounted) {
|
||||
SnackBar(
|
||||
content: Text(error.message),
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
);
|
||||
}
|
||||
if (mounted) context.showSnackBar(error.message, isError: true);
|
||||
} catch (error) {
|
||||
if (mounted) {
|
||||
SnackBar(
|
||||
content: const Text('Unexpected error occurred'),
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
);
|
||||
context.showSnackBar('Unexpected error occurred', isError: true);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
@@ -795,61 +717,18 @@ class _AccountPageState extends State<AccountPage> {
|
||||
try {
|
||||
await supabase.auth.signOut();
|
||||
} on AuthException catch (error) {
|
||||
if (mounted) {
|
||||
SnackBar(
|
||||
content: Text(error.message),
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
);
|
||||
}
|
||||
if (mounted) context.showSnackBar(error.message, isError: true);
|
||||
} catch (error) {
|
||||
if (mounted) {
|
||||
SnackBar(
|
||||
content: const Text('Unexpected error occurred'),
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
);
|
||||
context.showSnackBar('Unexpected error occurred', isError: true);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
Navigator.of(context).pushReplacementNamed('/login');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Called when image has been uploaded to Supabase storage from within Avatar widget
|
||||
Future<void> _onUpload(String imageUrl) async {
|
||||
try {
|
||||
final userId = supabase.auth.currentUser!.id;
|
||||
await supabase.from('profiles').upsert({
|
||||
'id': userId,
|
||||
'avatar_url': imageUrl,
|
||||
});
|
||||
if (mounted) {
|
||||
const SnackBar(
|
||||
content: Text('Updated your profile image!'),
|
||||
);
|
||||
}
|
||||
} on PostgrestException catch (error) {
|
||||
if (mounted) {
|
||||
SnackBar(
|
||||
content: Text(error.message),
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
if (mounted) {
|
||||
SnackBar(
|
||||
content: const Text('Unexpected error occurred'),
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(builder: (_) => const LoginPage()),
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_avatarUrl = imageUrl;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -869,34 +748,27 @@ class _AccountPageState extends State<AccountPage> {
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Profile')),
|
||||
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')),
|
||||
],
|
||||
),
|
||||
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: _loading ? null : _updateProfile,
|
||||
child: Text(_loading ? 'Saving...' : 'Update'),
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
TextButton(onPressed: _signOut, child: const Text('Sign Out')),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,6 @@
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
<key>MinimumOSVersion</key>
|
||||
<string>11.0</string>
|
||||
<string>12.0</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,5 +1,5 @@
|
||||
# Uncomment this line to define a global platform for your project
|
||||
platform :ios, '11.0'
|
||||
platform :ios, '12.0'
|
||||
|
||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||
|
||||
@@ -36,13 +36,13 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
app_links: 5ef33d0d295a89d9d16bb81b0e3b0d5f70d6c875
|
||||
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
|
||||
image_picker_ios: 4a8aadfbb6dc30ad5141a2ce3832af9214a705b5
|
||||
path_provider_foundation: eaf5b3e458fc0e5fbb9940fb09980e853fe058b8
|
||||
shared_preferences_foundation: e2dae3258e06f44cc55f49d42024fd8dd03c590c
|
||||
url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4
|
||||
app_links: e70ca16b4b0f88253b3b3660200d4a10b4ea9795
|
||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
|
||||
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
|
||||
|
||||
PODFILE CHECKSUM: 7368163408c647b7eb699d0d788ba6718e18fb8d
|
||||
PODFILE CHECKSUM: 4e8f8b2be68aeea4c0d5beb6ff1e79fface1d048
|
||||
|
||||
COCOAPODS: 1.11.3
|
||||
COCOAPODS: 1.14.3
|
||||
+4
-4
@@ -156,7 +156,7 @@
|
||||
97C146E61CF9000F007C117D /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastUpgradeCheck = 1430;
|
||||
LastUpgradeCheck = 1510;
|
||||
ORGANIZATIONNAME = "";
|
||||
TargetAttributes = {
|
||||
97C146ED1CF9000F007C117D = {
|
||||
@@ -343,7 +343,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
@@ -417,7 +417,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
@@ -466,7 +466,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1430"
|
||||
LastUpgradeVersion = "1510"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
||||
@@ -76,21 +76,11 @@ class _AvatarState extends State<Avatar> {
|
||||
widget.onUpload(imageUrlResponse);
|
||||
} on StorageException catch (error) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(error.message),
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
);
|
||||
context.showSnackBar(error.message, isError: true);
|
||||
}
|
||||
} catch (error) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: const Text('Unexpected error occurred'),
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
);
|
||||
context.showSnackBar('Unexpected error occurred', isError: true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import 'package:supabase_quickstart/pages/account_page.dart';
|
||||
import 'package:supabase_quickstart/pages/login_page.dart';
|
||||
import 'package:supabase_quickstart/pages/splash_page.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
await dotenv.load();
|
||||
@@ -38,13 +37,22 @@ class MyApp extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
initialRoute: '/',
|
||||
routes: <String, WidgetBuilder>{
|
||||
// Splash page is needed to ensure that authentication and page loading works correctly
|
||||
'/': (_) => const SplashPage(),
|
||||
'/login': (_) => const LoginPage(),
|
||||
'/account': (_) => const AccountPage(),
|
||||
},
|
||||
home: supabase.auth.currentSession == null
|
||||
? const LoginPage()
|
||||
: const AccountPage(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension ContextExtension on BuildContext {
|
||||
void showSnackBar(String message, {bool isError = false}) {
|
||||
ScaffoldMessenger.of(this).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(message),
|
||||
backgroundColor: isError
|
||||
? Theme.of(this).colorScheme.error
|
||||
: Theme.of(this).snackBarTheme.backgroundColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import 'package:supabase_quickstart/components/avatar.dart';
|
||||
import 'package:supabase_quickstart/main.dart';
|
||||
import 'package:supabase_quickstart/pages/login_page.dart';
|
||||
|
||||
class AccountPage extends StatefulWidget {
|
||||
const AccountPage({super.key});
|
||||
@@ -31,18 +32,10 @@ class _AccountPageState extends State<AccountPage> {
|
||||
_websiteController.text = (data['website'] ?? '') as String;
|
||||
_avatarUrl = (data['avatar_url'] ?? '') as String;
|
||||
} on PostgrestException catch (error) {
|
||||
if (mounted) {
|
||||
SnackBar(
|
||||
content: Text(error.message),
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
);
|
||||
}
|
||||
if (mounted) context.showSnackBar(error.message, isError: true);
|
||||
} catch (error) {
|
||||
if (mounted) {
|
||||
SnackBar(
|
||||
content: const Text('Unexpected error occurred'),
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
);
|
||||
context.showSnackBar('Unexpected error occurred', isError: true);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
@@ -69,24 +62,12 @@ class _AccountPageState extends State<AccountPage> {
|
||||
};
|
||||
try {
|
||||
await supabase.from('profiles').upsert(updates);
|
||||
if (mounted) {
|
||||
const SnackBar(
|
||||
content: Text('Successfully updated profile!'),
|
||||
);
|
||||
}
|
||||
if (mounted) context.showSnackBar('Successfully updated profile!');
|
||||
} on PostgrestException catch (error) {
|
||||
if (mounted) {
|
||||
SnackBar(
|
||||
content: Text(error.message),
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
);
|
||||
}
|
||||
if (mounted) context.showSnackBar(error.message, isError: true);
|
||||
} catch (error) {
|
||||
if (mounted) {
|
||||
SnackBar(
|
||||
content: const Text('Unexpected error occurred'),
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
);
|
||||
context.showSnackBar('Unexpected error occurred', isError: true);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
@@ -101,22 +82,16 @@ class _AccountPageState extends State<AccountPage> {
|
||||
try {
|
||||
await supabase.auth.signOut();
|
||||
} on AuthException catch (error) {
|
||||
if (mounted) {
|
||||
SnackBar(
|
||||
content: Text(error.message),
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
);
|
||||
}
|
||||
if (mounted) context.showSnackBar(error.message, isError: true);
|
||||
} catch (error) {
|
||||
if (mounted) {
|
||||
SnackBar(
|
||||
content: const Text('Unexpected error occurred'),
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
);
|
||||
context.showSnackBar('Unexpected error occurred', isError: true);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
Navigator.of(context).pushReplacementNamed('/login');
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(builder: (_) => const LoginPage()),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -135,18 +110,10 @@ class _AccountPageState extends State<AccountPage> {
|
||||
);
|
||||
}
|
||||
} on PostgrestException catch (error) {
|
||||
if (mounted) {
|
||||
SnackBar(
|
||||
content: Text(error.message),
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
);
|
||||
}
|
||||
if (mounted) context.showSnackBar(error.message, isError: true);
|
||||
} catch (error) {
|
||||
if (mounted) {
|
||||
SnackBar(
|
||||
content: const Text('Unexpected error occurred'),
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
);
|
||||
context.showSnackBar('Unexpected error occurred', isError: true);
|
||||
}
|
||||
}
|
||||
if (!mounted) {
|
||||
@@ -175,34 +142,32 @@ class _AccountPageState extends State<AccountPage> {
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Profile')),
|
||||
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')),
|
||||
],
|
||||
),
|
||||
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: _loading ? null : _updateProfile,
|
||||
child: Text(_loading ? 'Saving...' : 'Update'),
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
TextButton(onPressed: _signOut, child: const Text('Sign Out')),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import 'package:supabase_quickstart/main.dart';
|
||||
import 'package:supabase_quickstart/pages/account_page.dart';
|
||||
|
||||
class LoginPage extends StatefulWidget {
|
||||
const LoginPage({super.key});
|
||||
@@ -29,24 +30,15 @@ class _LoginPageState extends State<LoginPage> {
|
||||
kIsWeb ? null : 'io.supabase.flutterquickstart://login-callback/',
|
||||
);
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Check your email for a login link!')),
|
||||
);
|
||||
context.showSnackBar('Check your email for a login link!');
|
||||
|
||||
_emailController.clear();
|
||||
}
|
||||
} on AuthException catch (error) {
|
||||
if (mounted) {
|
||||
SnackBar(
|
||||
content: Text(error.message),
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
);
|
||||
}
|
||||
if (mounted) context.showSnackBar(error.message, isError: true);
|
||||
} catch (error) {
|
||||
if (mounted) {
|
||||
SnackBar(
|
||||
content: const Text('Unexpected error occurred'),
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
);
|
||||
context.showSnackBar('Unexpected error occurred', isError: true);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
@@ -59,14 +51,25 @@ class _LoginPageState extends State<LoginPage> {
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_authStateSubscription = supabase.auth.onAuthStateChange.listen((data) {
|
||||
if (_redirecting) return;
|
||||
final session = data.session;
|
||||
if (session != null) {
|
||||
_redirecting = true;
|
||||
Navigator.of(context).pushReplacementNamed('/account');
|
||||
}
|
||||
});
|
||||
_authStateSubscription = supabase.auth.onAuthStateChange.listen(
|
||||
(data) {
|
||||
if (_redirecting) return;
|
||||
final session = data.session;
|
||||
if (session != null) {
|
||||
_redirecting = true;
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(builder: (context) => const AccountPage()),
|
||||
);
|
||||
}
|
||||
},
|
||||
onError: (error) {
|
||||
if (error is AuthException) {
|
||||
context.showSnackBar(error.message, isError: true);
|
||||
} else {
|
||||
context.showSnackBar('Unexpected error occurred', isError: true);
|
||||
}
|
||||
},
|
||||
);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@@ -93,7 +96,7 @@ class _LoginPageState extends State<LoginPage> {
|
||||
const SizedBox(height: 18),
|
||||
ElevatedButton(
|
||||
onPressed: _isLoading ? null : _signIn,
|
||||
child: Text(_isLoading ? 'Loading' : 'Send Magic Link'),
|
||||
child: Text(_isLoading ? 'Sending...' : 'Send Magic Link'),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
Reference in new issue
Block a user