diff --git a/apps/docs/content/guides/getting-started/tutorials/with-flutter.mdx b/apps/docs/content/guides/getting-started/tutorials/with-flutter.mdx index 34a270ecda7..a0a6003cb4f 100644 --- a/apps/docs/content/guides/getting-started/tutorials/with-flutter.mdx +++ b/apps/docs/content/guides/getting-started/tutorials/with-flutter.mdx @@ -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 createState() => _SplashPageState(); -} - -class _SplashPageState extends State { - @override - void initState() { - super.initState(); - _redirect(); - } - - Future _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 { 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 { @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 { 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 { 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 { }); 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 { }; 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 { 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 { 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 { ### 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 main() async { await Supabase.initialize( @@ -524,12 +469,22 @@ class MyApp extends StatelessWidget { ), ), ), - initialRoute: '/', - routes: { - '/': (_) => 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 { 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 { 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 { }; 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 { 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 _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 { 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')), + ], + ), ); } } diff --git a/examples/user-management/flutter-user-management/ios/Flutter/AppFrameworkInfo.plist b/examples/user-management/flutter-user-management/ios/Flutter/AppFrameworkInfo.plist index 9625e105df3..7c569640062 100644 --- a/examples/user-management/flutter-user-management/ios/Flutter/AppFrameworkInfo.plist +++ b/examples/user-management/flutter-user-management/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 11.0 + 12.0 diff --git a/examples/user-management/flutter-user-management/ios/Podfile b/examples/user-management/flutter-user-management/ios/Podfile index 313ea4a1533..2c068c404bd 100644 --- a/examples/user-management/flutter-user-management/ios/Podfile +++ b/examples/user-management/flutter-user-management/ios/Podfile @@ -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' diff --git a/examples/user-management/flutter-user-management/ios/Podfile.lock b/examples/user-management/flutter-user-management/ios/Podfile.lock index 638eb05dc9b..9f31d9b4fab 100644 --- a/examples/user-management/flutter-user-management/ios/Podfile.lock +++ b/examples/user-management/flutter-user-management/ios/Podfile.lock @@ -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 diff --git a/examples/user-management/flutter-user-management/ios/Runner.xcodeproj/project.pbxproj b/examples/user-management/flutter-user-management/ios/Runner.xcodeproj/project.pbxproj index 4b2b3b38be7..3acdb5a5a15 100644 --- a/examples/user-management/flutter-user-management/ios/Runner.xcodeproj/project.pbxproj +++ b/examples/user-management/flutter-user-management/ios/Runner.xcodeproj/project.pbxproj @@ -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; diff --git a/examples/user-management/flutter-user-management/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/examples/user-management/flutter-user-management/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index b52b2e698b7..e67b2808af0 100644 --- a/examples/user-management/flutter-user-management/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/examples/user-management/flutter-user-management/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ { 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); } } diff --git a/examples/user-management/flutter-user-management/lib/main.dart b/examples/user-management/flutter-user-management/lib/main.dart index 136c1145a21..34cbbbf2b79 100644 --- a/examples/user-management/flutter-user-management/lib/main.dart +++ b/examples/user-management/flutter-user-management/lib/main.dart @@ -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 main() async { await dotenv.load(); @@ -38,13 +37,22 @@ class MyApp extends StatelessWidget { ), ), ), - initialRoute: '/', - routes: { - // 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, + ), ); } } diff --git a/examples/user-management/flutter-user-management/lib/pages/account_page.dart b/examples/user-management/flutter-user-management/lib/pages/account_page.dart index 806231cd5f4..1864f480623 100644 --- a/examples/user-management/flutter-user-management/lib/pages/account_page.dart +++ b/examples/user-management/flutter-user-management/lib/pages/account_page.dart @@ -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 { _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 { }; 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 { 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 { ); } } 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 { 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')), + ], + ), ); } } diff --git a/examples/user-management/flutter-user-management/lib/pages/login_page.dart b/examples/user-management/flutter-user-management/lib/pages/login_page.dart index 1fe5d42e645..462b7c97d2e 100644 --- a/examples/user-management/flutter-user-management/lib/pages/login_page.dart +++ b/examples/user-management/flutter-user-management/lib/pages/login_page.dart @@ -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 { 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 { @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 { const SizedBox(height: 18), ElevatedButton( onPressed: _isLoading ? null : _signIn, - child: Text(_isLoading ? 'Loading' : 'Send Magic Link'), + child: Text(_isLoading ? 'Sending...' : 'Send Magic Link'), ), ], ),