diff --git a/README.md b/README.md
index 6772c15..b86ee26 100644
--- a/README.md
+++ b/README.md
@@ -129,6 +129,80 @@ SupaSocialsAuth(
),
```
+## SupaAvatar
+
+A plug-and-play widget to show and edit a Supabase user's profile image.
+Supports both readonly and editable modes with full customization options.
+
+
+
+
+
+### Usage
+
+```dart
+import 'package:supabase_auth_ui/supabase_auth_ui.dart';
+
+class ProfileScreen extends StatelessWidget {
+ const ProfileScreen({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return Center(
+ child: SupaAvatar(
+ radius: 50,
+ isEditable: true,
+ ),
+ );
+ }
+}
+```
+When you tap on the avatar & `isEditable: true`, the avatar editor is opened:
+
+| dialog | modal |
+| -------- | ------- |
+|
|
|
+| `editorType: SupaAvatarEditorType.dialog,` | `editorType: SupaAvatarEditorType.modal,` |
+
+### Customization
+
+```dart
+SupaAvatar(
+ radius: 60,
+ isEditable: true,
+ supabaseStorageBucket: 'avatars',
+ supabaseStoragePath: 'profile_image', // stored as userId/profile_image
+ supabaseUserAttributeImageUrlKey: 'avatar_url',
+
+ fallbackIcon: Icon(Icons.person_2_rounded, size: 32),
+ cacheBuster: DateTime.now().millisecondsSinceEpoch.toString(),
+
+ // editor customization
+ editorType: SupaAvatarEditorType.dialog, // defaults to SupaAvatarEditorType.modal
+ editorShape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
+ ),
+ editorBackgroundColor: Colors.grey[900],
+
+ // Snackbar colors
+ snackBarBackgroundColor: Colors.green[600],
+ snackBarTextColor: Colors.white,
+ snackBarErrorBackgroundColor: Colors.red[600],
+ snackBarErrorTextColor: Colors.white,
+ snackBarDuration: Duration(seconds: 2),
+)
+```
+
+
+### Notes
+
+- The image is stored in `avatars/{userId}/{supabaseStoragePath}`.
+- The metadata field (`avatar_url` by default) is updated on upload/remove.
+- Uses `cacheBuster` to bypass CDN cache after upload.
+- Tapping on avatar opens a bottom sheet or dialog for edit options when `isEditable` is true.
+
+---
+
## Theming
This library uses bare Flutter components so that you can control the appearance of the components using your own theme.
diff --git a/example/.gitignore b/example/.gitignore
index 1f061d7..9f86c2c 100644
--- a/example/.gitignore
+++ b/example/.gitignore
@@ -6,9 +6,11 @@
*.swp
.DS_Store
.atom/
+.build/
.buildlog/
.history
.svn/
+.swiftpm/
migrate_working_dir/
# IntelliJ related
diff --git a/example/assets/supabase-logo-icon.png b/example/assets/supabase-logo-icon.png
new file mode 100644
index 0000000..a3b7b36
Binary files /dev/null and b/example/assets/supabase-logo-icon.png differ
diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist
index 9625e10..7c56964 100644
--- a/example/ios/Flutter/AppFrameworkInfo.plist
+++ b/example/ios/Flutter/AppFrameworkInfo.plist
@@ -21,6 +21,6 @@
CFBundleVersion
1.0
MinimumOSVersion
- 11.0
+ 12.0
diff --git a/example/ios/Podfile b/example/ios/Podfile
index 88359b2..279576f 100644
--- a/example/ios/Podfile
+++ b/example/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/example/ios/Podfile.lock b/example/ios/Podfile.lock
index c7b21ec..b8f16e2 100644
--- a/example/ios/Podfile.lock
+++ b/example/ios/Podfile.lock
@@ -1,42 +1,93 @@
PODS:
- - app_links (0.0.1):
+ - app_links (0.0.2):
- Flutter
+ - AppAuth (1.7.6):
+ - AppAuth/Core (= 1.7.6)
+ - AppAuth/ExternalUserAgent (= 1.7.6)
+ - AppAuth/Core (1.7.6)
+ - AppAuth/ExternalUserAgent (1.7.6):
+ - AppAuth/Core
- Flutter (1.0.0)
+ - google_sign_in_ios (0.0.1):
+ - AppAuth (>= 1.7.4)
+ - Flutter
+ - FlutterMacOS
+ - GoogleSignIn (~> 7.1)
+ - GTMSessionFetcher (>= 3.4.0)
+ - GoogleSignIn (7.1.0):
+ - AppAuth (< 2.0, >= 1.7.3)
+ - GTMAppAuth (< 5.0, >= 4.1.1)
+ - GTMSessionFetcher/Core (~> 3.3)
+ - GTMAppAuth (4.1.1):
+ - AppAuth/Core (~> 1.7)
+ - GTMSessionFetcher/Core (< 4.0, >= 3.3)
+ - GTMSessionFetcher (3.5.0):
+ - GTMSessionFetcher/Full (= 3.5.0)
+ - GTMSessionFetcher/Core (3.5.0)
+ - GTMSessionFetcher/Full (3.5.0):
+ - GTMSessionFetcher/Core
+ - image_picker_ios (0.0.1):
+ - Flutter
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
+ - sign_in_with_apple (0.0.1):
+ - Flutter
- url_launcher_ios (0.0.1):
- Flutter
DEPENDENCIES:
- app_links (from `.symlinks/plugins/app_links/ios`)
- Flutter (from `Flutter`)
+ - google_sign_in_ios (from `.symlinks/plugins/google_sign_in_ios/darwin`)
+ - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
+ - sign_in_with_apple (from `.symlinks/plugins/sign_in_with_apple/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
+SPEC REPOS:
+ trunk:
+ - AppAuth
+ - GoogleSignIn
+ - GTMAppAuth
+ - GTMSessionFetcher
+
EXTERNAL SOURCES:
app_links:
:path: ".symlinks/plugins/app_links/ios"
Flutter:
:path: Flutter
+ google_sign_in_ios:
+ :path: ".symlinks/plugins/google_sign_in_ios/darwin"
+ image_picker_ios:
+ :path: ".symlinks/plugins/image_picker_ios/ios"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
+ sign_in_with_apple:
+ :path: ".symlinks/plugins/sign_in_with_apple/ios"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
SPEC CHECKSUMS:
- app_links: ab4ba54d10a13d45825336bc9707b5eadee81191
- Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
- path_provider_foundation: eaf5b3e458fc0e5fbb9940fb09980e853fe058b8
- shared_preferences_foundation: e2dae3258e06f44cc55f49d42024fd8dd03c590c
- url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4
+ app_links: 76b66b60cc809390ca1ad69bfd66b998d2387ac7
+ AppAuth: d4f13a8fe0baf391b2108511793e4b479691fb73
+ Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
+ google_sign_in_ios: 19297361f2c51d7d8ac0201b866ef1fa5d1f94a8
+ GoogleSignIn: d4281ab6cf21542b1cfaff85c191f230b399d2db
+ GTMAppAuth: f69bd07d68cd3b766125f7e072c45d7340dea0de
+ GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
+ image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
+ path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
+ shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
+ sign_in_with_apple: c5dcc141574c8c54d5ac99dd2163c0c72ad22418
+ url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
-PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3
+PODFILE CHECKSUM: c4c93c5f6502fe2754f48404d3594bf779584011
-COCOAPODS: 1.14.3
+COCOAPODS: 1.16.2
diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj
index 578e7a0..3bd3034 100644
--- a/example/ios/Runner.xcodeproj/project.pbxproj
+++ b/example/ios/Runner.xcodeproj/project.pbxproj
@@ -140,6 +140,7 @@
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
5577070999A7873150D42AF2 /* [CP] Embed Pods Frameworks */,
+ 7507BAEEC32E71631CAEFDBF /* [CP] Copy Pods Resources */,
);
buildRules = (
);
@@ -156,7 +157,7 @@
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
- LastUpgradeCheck = 1430;
+ LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
97C146ED1CF9000F007C117D = {
@@ -231,6 +232,23 @@
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
+ 7507BAEEC32E71631CAEFDBF /* [CP] Copy Pods Resources */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
+ );
+ name = "[CP] Copy Pods Resources";
+ outputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
@@ -343,7 +361,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;
@@ -420,7 +438,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;
@@ -469,7 +487,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/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
index a6b826d..c53e2b3 100644
--- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
+++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -1,6 +1,6 @@
diff --git a/example/ios/Runner/AppDelegate.swift b/example/ios/Runner/AppDelegate.swift
index 70693e4..b636303 100644
--- a/example/ios/Runner/AppDelegate.swift
+++ b/example/ios/Runner/AppDelegate.swift
@@ -1,7 +1,7 @@
import UIKit
import Flutter
-@UIApplicationMain
+@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
diff --git a/example/lib/main.dart b/example/lib/main.dart
index ad28ebd..2cc8343 100644
--- a/example/lib/main.dart
+++ b/example/lib/main.dart
@@ -1,21 +1,14 @@
-import 'package:example/phone_sign_up.dart';
+import 'package:example/src/core/app_router.dart';
import 'package:flutter/material.dart';
import 'package:supabase_auth_ui/supabase_auth_ui.dart';
-import './home.dart';
-import './sign_in.dart';
-import './magic_link.dart';
-import './update_password.dart';
-import 'phone_sign_in.dart';
-import './verify_phone.dart';
+
void main() async {
WidgetsFlutterBinding.ensureInitialized();
-
- /// TODO: replace with your credentials
await Supabase.initialize(
- url: 'https://yoursupabaseurl.supabase.co',
- anonKey: 'your_anon_key',
+ url: 'https://hvjjvggueouofkqafawj.supabase.co',
+ anonKey: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imh2amp2Z2d1ZW91b2ZrcWFmYXdqIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDM5ODA2MTMsImV4cCI6MjA1OTU1NjYxM30.m0VSa8jdoNcJFmS18xMwtM0P9yblakd2QgEXkW5kAqU',
);
runApp(const MyApp());
}
@@ -26,40 +19,16 @@ class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
- return MaterialApp(
+ return MaterialApp.router(
debugShowCheckedModeBanner: false,
- title: 'Flutter Demo',
+ title: 'Supabase Auth UI Demo',
theme: ThemeData(
+ colorScheme: ColorScheme.fromSeed(seedColor: Colors.green),
inputDecorationTheme: const InputDecorationTheme(
border: OutlineInputBorder(),
),
),
- initialRoute: '/',
- routes: {
- '/': (context) => const SignUp(),
- '/magic_link': (context) => const MagicLink(),
- '/update_password': (context) => const UpdatePassword(),
- '/phone_sign_in': (context) => const PhoneSignIn(),
- '/phone_sign_up': (context) => const PhoneSignUp(),
- '/verify_phone': (context) => const VerifyPhone(),
- '/home': (context) => const Home(),
- },
- onUnknownRoute: (RouteSettings settings) {
- return MaterialPageRoute(
- settings: settings,
- builder: (BuildContext context) => const Scaffold(
- body: Center(
- child: Text(
- 'Not Found',
- style: TextStyle(
- fontSize: 42,
- fontWeight: FontWeight.bold,
- ),
- ),
- ),
- ),
- );
- },
+ routerConfig: appRouter,
);
}
}
diff --git a/example/lib/src/core/app_router.dart b/example/lib/src/core/app_router.dart
new file mode 100644
index 0000000..7b56c98
--- /dev/null
+++ b/example/lib/src/core/app_router.dart
@@ -0,0 +1,121 @@
+// initialRoute: '/',
+// routes: {
+// '/': (context) => const SignUp(),
+// '/magic_link': (context) => const MagicLink(),
+// '/update_password': (context) => const UpdatePassword(),
+// '/phone_sign_in': (context) => const PhoneSignIn(),
+// '/phone_sign_up': (context) => const PhoneSignUp(),
+// '/verify_phone': (context) => const VerifyPhone(),
+// '/home': (context) => const Home(),
+// },
+
+import 'dart:async';
+
+import 'package:example/src/core/constants.dart';
+import 'package:example/src/views/screens/home.dart';
+import 'package:example/src/views/screens/magic_link.dart';
+import 'package:example/src/views/screens/phone_sign_in.dart';
+import 'package:example/src/views/screens/sign_in_up.dart';
+import 'package:example/src/views/screens/splash.dart';
+import 'package:example/src/views/screens/update_password.dart';
+import 'package:example/src/views/screens/verify_phone.dart';
+import 'package:flutter/foundation.dart';
+import 'package:go_router/go_router.dart';
+
+// define the app routes enum with the path for each screen
+enum AppRoute {
+ signInUp('/sign_in_up'),
+ magicLink('/magic_link'),
+ updatePassword('/update_password'),
+ phoneSignIn('/phone_sign_in'),
+ phoneSignUp('/phone_sign_up'),
+ verifyPhone('/verify_phone'),
+ home('/'),
+ splash('/splash');
+
+ const AppRoute(this.path);
+ final String path;
+}
+
+// define the app router using GoRouter
+final appRouter = GoRouter(
+ initialLocation: AppRoute.splash.path,
+ debugLogDiagnostics: true,
+ redirect: (context, state) {
+ // Check if the user is authenticated
+ final user = supa.auth.currentUser;
+ final isAuthenticated = user != null;
+
+ // If the user is authenticated and trying to access the sign-in page, redirect to home
+ if (isAuthenticated && (state.matchedLocation == AppRoute.signInUp.path || state.matchedLocation == AppRoute.splash.path)) {
+ return AppRoute.home.path;
+ }
+
+ // If the user is not authenticated and trying to access the home page, redirect to sign-in
+ if (!isAuthenticated && (state.matchedLocation == AppRoute.home.path || state.matchedLocation == AppRoute.splash.path)) {
+ return AppRoute.signInUp.path;
+ }
+
+ return null;
+ },
+ refreshListenable: GoRouterRefreshStream(supa.auth.onAuthStateChange),
+ routes: [
+ // define the splash screen route
+ GoRoute(
+ path: AppRoute.splash.path,
+ builder: (context, state) => const Splash(),
+ ),
+
+ GoRoute(
+ name: AppRoute.signInUp.name,
+ path: AppRoute.signInUp.path,
+ builder: (context, state) => const SignInUp(),
+ ),
+ GoRoute(
+ name: AppRoute.magicLink.name,
+ path: AppRoute.magicLink.path,
+ builder: (context, state) => const MagicLink(),
+ ),
+ GoRoute(
+ name: AppRoute.updatePassword.name,
+ path: AppRoute.updatePassword.path,
+ builder: (context, state) => const UpdatePassword(),
+ ),
+ GoRoute(
+ name: AppRoute.phoneSignIn.name,
+ path: AppRoute.phoneSignIn.path,
+ builder: (context, state) => const PhoneSignIn(),
+ ),
+ GoRoute(
+ name: AppRoute.phoneSignUp.name,
+ path: AppRoute.phoneSignUp.path,
+ builder: (context, state) => const PhoneSignIn(),
+ ),
+ GoRoute(
+ name: AppRoute.verifyPhone.name,
+ path: AppRoute.verifyPhone.path,
+ builder: (context, state) => const VerifyPhone(),
+ ),
+ GoRoute(
+ name: AppRoute.home.name,
+ path: AppRoute.home.path,
+ builder: (context, state) => const Home(),
+ ),
+ ],
+);
+
+class GoRouterRefreshStream with ChangeNotifier {
+ GoRouterRefreshStream(Stream stream) {
+ _subscription = stream.listen((_) {
+ notifyListeners();
+ });
+ }
+
+ late final StreamSubscription _subscription;
+
+ @override
+ void dispose() {
+ _subscription.cancel();
+ super.dispose();
+ }
+}
diff --git a/example/lib/constants.dart b/example/lib/src/core/constants.dart
similarity index 77%
rename from example/lib/constants.dart
rename to example/lib/src/core/constants.dart
index 58060b7..64a1ede 100644
--- a/example/lib/constants.dart
+++ b/example/lib/src/core/constants.dart
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
+import 'package:supabase_auth_ui/supabase_auth_ui.dart';
AppBar appBar(String title) => AppBar(
title: Text(title),
@@ -17,3 +18,5 @@ const optionText = Text(
const spacer = SizedBox(
height: 12,
);
+
+final supa = Supabase.instance.client;
diff --git a/example/lib/home.dart b/example/lib/src/views/screens/home.dart
similarity index 78%
rename from example/lib/home.dart
rename to example/lib/src/views/screens/home.dart
index 0b5353d..e449e9e 100644
--- a/example/lib/home.dart
+++ b/example/lib/src/views/screens/home.dart
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:supabase_auth_ui/supabase_auth_ui.dart';
-import 'constants.dart';
+import '../../core/constants.dart';
class Home extends StatelessWidget {
const Home({Key? key}) : super(key: key);
@@ -18,10 +18,14 @@ class Home extends StatelessWidget {
'You are home',
style: TextStyle(fontSize: 42),
),
+ const SizedBox(height: 20),
+ const SupaAvatar(
+ isEditable: true,
+ // editorType: SupaAvatarEditorType.dialog, // defaults to modal sheet
+ ),
ElevatedButton(
onPressed: () {
Supabase.instance.client.auth.signOut();
- Navigator.of(context).pushReplacementNamed('/');
},
child: const Text(
'Log Out',
diff --git a/example/lib/magic_link.dart b/example/lib/src/views/screens/magic_link.dart
similarity index 76%
rename from example/lib/magic_link.dart
rename to example/lib/src/views/screens/magic_link.dart
index aebea2d..2b61d62 100644
--- a/example/lib/magic_link.dart
+++ b/example/lib/src/views/screens/magic_link.dart
@@ -1,8 +1,12 @@
+import 'dart:developer';
+
+import 'package:example/src/core/app_router.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
+import 'package:go_router/go_router.dart';
import 'package:supabase_auth_ui/supabase_auth_ui.dart';
-import 'constants.dart';
+import '../../core/constants.dart';
class MagicLink extends StatelessWidget {
const MagicLink({Key? key}) : super(key: key);
@@ -17,7 +21,8 @@ class MagicLink extends StatelessWidget {
children: [
SupaMagicAuth(
onSuccess: (response) {
- Navigator.of(context).pushReplacementNamed('/home');
+ log('Magic Link Sign In Success: ${response.toJson()}');
+
},
redirectUrl: kIsWeb
? null
@@ -29,7 +34,7 @@ class MagicLink extends StatelessWidget {
style: TextStyle(fontWeight: FontWeight.bold),
),
onPressed: () {
- Navigator.pushNamed(context, '/sign_in');
+ context.goNamed(AppRoute.signInUp.name);
},
),
],
diff --git a/example/lib/phone_sign_in.dart b/example/lib/src/views/screens/phone_sign_in.dart
similarity index 71%
rename from example/lib/phone_sign_in.dart
rename to example/lib/src/views/screens/phone_sign_in.dart
index 523f763..950faff 100644
--- a/example/lib/phone_sign_in.dart
+++ b/example/lib/src/views/screens/phone_sign_in.dart
@@ -1,7 +1,12 @@
+import 'dart:developer';
+
+import 'package:example/src/core/app_router.dart';
import 'package:flutter/material.dart';
+import 'package:go_router/go_router.dart';
import 'package:supabase_auth_ui/supabase_auth_ui.dart';
-import 'constants.dart';
+
+import '../../core/constants.dart';
class PhoneSignIn extends StatelessWidget {
const PhoneSignIn({Key? key}) : super(key: key);
@@ -16,8 +21,8 @@ class PhoneSignIn extends StatelessWidget {
children: [
SupaPhoneAuth(
authAction: SupaAuthAction.signIn,
- onSuccess: (response) {
- Navigator.of(context).pushReplacementNamed('/home');
+ onSuccess: (AuthResponse response) {
+ log('Phone Sign In Success: $response');
},
),
TextButton(
@@ -26,7 +31,7 @@ class PhoneSignIn extends StatelessWidget {
style: TextStyle(fontWeight: FontWeight.bold),
),
onPressed: () {
- Navigator.pushNamed(context, '/');
+ context.goNamed(AppRoute.signInUp.name);
},
),
],
diff --git a/example/lib/phone_sign_up.dart b/example/lib/src/views/screens/phone_sign_up.dart
similarity index 77%
rename from example/lib/phone_sign_up.dart
rename to example/lib/src/views/screens/phone_sign_up.dart
index a508051..33e7009 100644
--- a/example/lib/phone_sign_up.dart
+++ b/example/lib/src/views/screens/phone_sign_up.dart
@@ -1,7 +1,9 @@
+import 'package:example/src/core/app_router.dart';
import 'package:flutter/material.dart';
+import 'package:go_router/go_router.dart';
import 'package:supabase_auth_ui/supabase_auth_ui.dart';
-import 'constants.dart';
+import '../../core/constants.dart';
class PhoneSignUp extends StatelessWidget {
const PhoneSignUp({Key? key}) : super(key: key);
@@ -17,7 +19,7 @@ class PhoneSignUp extends StatelessWidget {
SupaPhoneAuth(
authAction: SupaAuthAction.signUp,
onSuccess: (response) {
- Navigator.of(context).pushReplacementNamed('/verify_phone');
+ context.goNamed(AppRoute.verifyPhone.name);
},
),
TextButton(
@@ -26,7 +28,7 @@ class PhoneSignUp extends StatelessWidget {
style: TextStyle(fontWeight: FontWeight.bold),
),
onPressed: () {
- Navigator.of(context).pushNamed('/sign_in');
+ context.goNamed(AppRoute.signInUp.name);
},
),
],
diff --git a/example/lib/sign_in.dart b/example/lib/src/views/screens/sign_in_up.dart
similarity index 94%
rename from example/lib/sign_in.dart
rename to example/lib/src/views/screens/sign_in_up.dart
index 6492132..37ebaea 100644
--- a/example/lib/sign_in.dart
+++ b/example/lib/src/views/screens/sign_in_up.dart
@@ -1,16 +1,20 @@
+import 'dart:developer';
+
+import 'package:example/src/core/app_router.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
+import 'package:go_router/go_router.dart';
import 'package:supabase_auth_ui/supabase_auth_ui.dart';
-import 'constants.dart';
+import '../../core/constants.dart';
-class SignUp extends StatelessWidget {
- const SignUp({Key? key}) : super(key: key);
+class SignInUp extends StatelessWidget {
+ const SignInUp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
void navigateHome(AuthResponse response) {
- Navigator.of(context).pushReplacementNamed('/home');
+ log('Sign in/up success: ${response.user?.email}');
}
final darkModeThemeData = ThemeData.dark().copyWith(
@@ -162,14 +166,14 @@ class SignUp extends StatelessWidget {
ElevatedButton.icon(
icon: const Icon(Icons.email),
onPressed: () {
- Navigator.popAndPushNamed(context, '/magic_link');
+ context.goNamed(AppRoute.magicLink.name);
},
label: const Text('Sign in with Magic Link'),
),
spacer,
ElevatedButton.icon(
onPressed: () {
- Navigator.popAndPushNamed(context, '/phone_sign_in');
+ context.goNamed(AppRoute.phoneSignIn.name);
},
icon: const Icon(Icons.phone),
label: const Text('Sign in with Phone'),
@@ -184,7 +188,7 @@ class SignUp extends StatelessWidget {
enableNativeAppleAuth: false,
socialProviders: OAuthProvider.values,
onSuccess: (session) {
- Navigator.of(context).pushReplacementNamed('/home');
+
},
),
],
diff --git a/example/lib/src/views/screens/splash.dart b/example/lib/src/views/screens/splash.dart
new file mode 100644
index 0000000..0e98f9a
--- /dev/null
+++ b/example/lib/src/views/screens/splash.dart
@@ -0,0 +1,30 @@
+import 'package:flutter/material.dart';
+
+class Splash extends StatelessWidget {
+ const Splash({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ body: Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Image.asset(
+ 'assets/supabase-logo-icon.png',
+ height: 100,
+ ),
+ const SizedBox(height: 20),
+ const Text(
+ 'Welcome to Supabase Auth UI',
+ textAlign: TextAlign.center,
+ style: TextStyle(fontSize: 42),
+ ),
+ const SizedBox(height: 20),
+ const CircularProgressIndicator(),
+ ],
+ ),
+ ),
+ );
+ }
+}
\ No newline at end of file
diff --git a/example/lib/update_password.dart b/example/lib/src/views/screens/update_password.dart
similarity index 78%
rename from example/lib/update_password.dart
rename to example/lib/src/views/screens/update_password.dart
index c8c06e7..8713d47 100644
--- a/example/lib/update_password.dart
+++ b/example/lib/src/views/screens/update_password.dart
@@ -1,7 +1,11 @@
+import 'dart:developer';
+
+import 'package:example/src/core/app_router.dart';
import 'package:flutter/material.dart';
+import 'package:go_router/go_router.dart';
import 'package:supabase_auth_ui/supabase_auth_ui.dart';
-import 'constants.dart';
+import '../../core/constants.dart';
class UpdatePassword extends StatelessWidget {
const UpdatePassword({Key? key}) : super(key: key);
@@ -18,7 +22,8 @@ class UpdatePassword extends StatelessWidget {
accessToken:
Supabase.instance.client.auth.currentSession!.accessToken,
onSuccess: (response) {
- Navigator.of(context).pushReplacementNamed('/home');
+ log('Update Password Success: $response');
+ context.goNamed(AppRoute.home.name);
},
),
TextButton(
diff --git a/example/lib/verify_phone.dart b/example/lib/src/views/screens/verify_phone.dart
similarity index 79%
rename from example/lib/verify_phone.dart
rename to example/lib/src/views/screens/verify_phone.dart
index 59fcd37..3fac3c0 100644
--- a/example/lib/verify_phone.dart
+++ b/example/lib/src/views/screens/verify_phone.dart
@@ -1,7 +1,11 @@
+import 'dart:developer';
+
+import 'package:example/src/core/app_router.dart';
import 'package:flutter/material.dart';
+import 'package:go_router/go_router.dart';
import 'package:supabase_auth_ui/supabase_auth_ui.dart';
-import 'constants.dart';
+import '../../core/constants.dart';
class VerifyPhone extends StatelessWidget {
const VerifyPhone({Key? key}) : super(key: key);
@@ -16,7 +20,8 @@ class VerifyPhone extends StatelessWidget {
children: [
SupaVerifyPhone(
onSuccess: (response) {
- Navigator.of(context).pushReplacementNamed('/home');
+ log("Verify Phone Success: $response");
+
},
),
TextButton(
@@ -25,7 +30,7 @@ class VerifyPhone extends StatelessWidget {
style: TextStyle(fontWeight: FontWeight.bold),
),
onPressed: () {
- Navigator.pushNamed(context, '/forgot_password');
+ context.goNamed(AppRoute.updatePassword.name);
},
),
TextButton(
diff --git a/example/macos/Flutter/GeneratedPluginRegistrant.swift b/example/macos/Flutter/GeneratedPluginRegistrant.swift
index 464810a..e7591d8 100644
--- a/example/macos/Flutter/GeneratedPluginRegistrant.swift
+++ b/example/macos/Flutter/GeneratedPluginRegistrant.swift
@@ -6,6 +6,7 @@ import FlutterMacOS
import Foundation
import app_links
+import file_selector_macos
import google_sign_in_ios
import path_provider_foundation
import shared_preferences_foundation
@@ -14,6 +15,7 @@ import url_launcher_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin"))
+ FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
FLTGoogleSignInPlugin.register(with: registry.registrar(forPlugin: "FLTGoogleSignInPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
diff --git a/example/pubspec.yaml b/example/pubspec.yaml
index 4e2f3c0..8b5a915 100644
--- a/example/pubspec.yaml
+++ b/example/pubspec.yaml
@@ -11,6 +11,7 @@ environment:
dependencies:
flutter:
sdk: flutter
+ go_router: ^14.8.1
supabase_auth_ui:
path: ../
@@ -22,3 +23,6 @@ dev_dependencies:
flutter:
uses-material-design: true
+
+ assets:
+ - assets/
diff --git a/example/windows/flutter/generated_plugin_registrant.cc b/example/windows/flutter/generated_plugin_registrant.cc
index 785a046..5bbd4c3 100644
--- a/example/windows/flutter/generated_plugin_registrant.cc
+++ b/example/windows/flutter/generated_plugin_registrant.cc
@@ -7,11 +7,14 @@
#include "generated_plugin_registrant.h"
#include
+#include
#include
void RegisterPlugins(flutter::PluginRegistry* registry) {
AppLinksPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("AppLinksPluginCApi"));
+ FileSelectorWindowsRegisterWithRegistrar(
+ registry->GetRegistrarForPlugin("FileSelectorWindows"));
UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
}
diff --git a/example/windows/flutter/generated_plugins.cmake b/example/windows/flutter/generated_plugins.cmake
index 8f8ee4f..79001bc 100644
--- a/example/windows/flutter/generated_plugins.cmake
+++ b/example/windows/flutter/generated_plugins.cmake
@@ -4,6 +4,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
app_links
+ file_selector_windows
url_launcher_windows
)
diff --git a/lib/src/components/supa_avatar/supa_avatar.dart b/lib/src/components/supa_avatar/supa_avatar.dart
new file mode 100644
index 0000000..848cce1
--- /dev/null
+++ b/lib/src/components/supa_avatar/supa_avatar.dart
@@ -0,0 +1,339 @@
+// 📁 supa_avatar.dart
+import 'dart:io';
+import 'package:flutter/material.dart';
+import 'package:mime/mime.dart';
+import 'package:supabase_auth_ui/src/components/supa_avatar/widgets/supa_avatar_editor.dart';
+import 'package:supabase_auth_ui/src/components/supa_avatar/widgets/supa_user_avatar.dart';
+import 'package:supabase_auth_ui/supabase_auth_ui.dart';
+
+enum SupaAvatarEditorType {
+ modal,
+ dialog,
+}
+
+class SupaAvatar extends StatefulWidget {
+ /// Creates a SupaAvatar widget.
+ ///
+ /// Displays the current user's avatar from Supabase user metadata or storage,
+ /// with optional editing functionality (upload, remove) via a modal bottom sheet.
+ ///
+ /// Set [isEditable] to true to allow avatar updates via gallery/camera.
+ const SupaAvatar({
+ super.key,
+
+ /// Radius of the avatar circle.
+ /// Defaults to `40`.
+ this.radius = 40,
+
+ /// Enables edit mode. If `true`, shows a modal to upload or remove avatar.
+ this.isEditable = false,
+
+ /// Optional cache buster string added to the image URL to bypass cached versions.
+ this.cacheBuster,
+
+ /// Custom shape for the editor (used in editable mode).
+ this.editorShape,
+
+ /// Custom background color for the editor.
+ this.editorBackgroundColor,
+
+ /// The Supabase storage bucket where avatars are stored.
+ /// Defaults to `'avatars'`.
+ this.supabaseStorageBucket = 'avatars',
+
+ /// The path under the user’s folder in the storage bucket where the avatar is saved.
+ /// Example: if `user.id` is `abc` and [supabaseStoragePath] is `profile`, the full path becomes `abc/profile`.
+ this.supabaseStoragePath = 'profile',
+
+ /// Widget to show if no avatar is available.
+ /// Defaults to [Icon(Icons.person)].
+ this.fallbackIcon = const Icon(Icons.person),
+
+ /// Background color of the snackbar shown on successful avatar upload or removal.
+ this.snackBarBackgroundColor,
+
+ /// Text color of the snackbar on success.
+ this.snackBarTextColor,
+
+ /// Background color of the snackbar shown on error.
+ this.snackBarErrorBackgroundColor,
+
+ /// Text color of the snackbar on error.
+ this.snackBarErrorTextColor,
+
+ /// Duration the snackbar is shown.
+ this.snackBarDuration,
+
+ /// The user metadata key used to retrieve the avatar URL.
+ /// Defaults to `'avatar_url'`.
+ this.supabaseUserAttributeImageUrlKey = 'avatar_url',
+
+ /// The type of editor to use for the avatar
+ ///
+ /// Defaults to [SupaAvatarEditorType.modal]
+ ///
+ /// If you want to use a dialog instead of a modal, set this to [SupaAvatarEditorType.dialog]
+ this.editorType = SupaAvatarEditorType.modal,
+ });
+
+ /// Radius of the avatar circle.
+ /// Defaults to `40`.
+ final double radius;
+
+ /// Enables edit mode. If `true`, shows a modal to upload or remove avatar.
+ final bool isEditable;
+
+ /// Optional cache buster string added to the image URL to bypass cached versions.
+ final String? cacheBuster;
+
+ /// Custom shape for the modal bottom sheet (used in editable mode).
+ final ShapeBorder? editorShape;
+
+ /// Custom background color for the modal bottom sheet.
+ final Color? editorBackgroundColor;
+
+ /// The Supabase storage bucket where avatars are stored.
+ /// Defaults to `'avatars'`.
+ final String supabaseStorageBucket;
+
+ /// The path under the user’s folder in the storage bucket where the avatar is saved.
+ /// Example: if `user.id` is `abc` and [supabaseStoragePath] is `profile`,
+ /// the full path becomes `abc/profile`.
+ final String supabaseStoragePath;
+
+ /// Widget to show if no avatar is available.
+ /// Defaults to [Icon(Icons.person)].
+ final Widget fallbackIcon;
+
+ /// Background color of the snackbar shown on successful avatar upload or removal.
+ final Color? snackBarBackgroundColor;
+
+ /// Text color of the snackbar on success.
+ final Color? snackBarTextColor;
+
+ /// Background color of the snackbar shown on error.
+ final Color? snackBarErrorBackgroundColor;
+
+ /// Text color of the snackbar on error.
+ final Color? snackBarErrorTextColor;
+
+ /// Duration the snackbar is shown.
+ final Duration? snackBarDuration;
+
+ /// The user metadata key used to retrieve the avatar URL.
+ /// Defaults to `'avatar_url'`.
+ final String supabaseUserAttributeImageUrlKey;
+
+ /// The type of editor to use for the avatar
+ ///
+ /// Defaults to [SupaAvatarEditorType.modal]
+ ///
+ /// If you want to use a dialog instead of a modal, set this to [SupaAvatarEditorType.dialog]
+ final SupaAvatarEditorType editorType;
+
+ @override
+ State createState() => _SupaAvatarEditorState();
+}
+
+class _SupaAvatarEditorState extends State {
+ late String _cacheBuster;
+ bool _isLoading = false;
+ File? _lastAvatarFile;
+
+ final user = Supabase.instance.client.auth.currentUser;
+
+ @override
+ void initState() {
+ super.initState();
+ _cacheBuster =
+ widget.cacheBuster ?? DateTime.now().millisecondsSinceEpoch.toString();
+ }
+
+ Future _handleAvatarEdit(BuildContext context) async {
+ Future