Skip to content

Commit

Permalink
Merge pull request #4 from SandroMaglione/4_supabase_auth
Browse files Browse the repository at this point in the history
Supabase authentication [4]
  • Loading branch information
SandroMaglione authored Sep 24, 2022
2 parents dbdd86e + 4638871 commit df52a44
Show file tree
Hide file tree
Showing 12 changed files with 317 additions and 37 deletions.
20 changes: 5 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,6 @@
# flutter_supabase_complete
# `Flutter + Supabase` app template
Setup for a production ready Flutter application using Supabase for authentication and database services.

A new Flutter project.

## Getting Started

This project is a starting point for a Flutter application.

A few resources to get you started if this is your first Flutter project:

- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)

For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.
## TODO (Functional Programming)
- [ ] Using `TaskEither` in `repository` as return type
- [ ] Convert less-specific types to more specific ones using guards (e.g. instead of `String` for `userId`, create a `UserId` type and check the validity using `Either`)
32 changes: 29 additions & 3 deletions lib/app.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,37 @@
import 'package:flutter/material.dart';
import 'package:flutter_supabase_complete/core/routes/app_router.dart';
import 'package:supabase_flutter/supabase_flutter.dart';

/// Entry widget of the app
class App extends StatelessWidget {
App({Key? key}) : super(key: key);
/// Entry widget of the app.
class App extends StatefulWidget {
const App({Key? key}) : super(key: key);

@override
State<App> createState() => _AppState();
}

class _AppState extends State<App> {
final _appRouter = AppRouter();

@override
void initState() {
super.initState();

/// Listen for authentication events and redirect to
/// correct page when key events are detected.
SupabaseAuth.instance.onAuthChange.listen((event) {
if (event == AuthChangeEvent.signedIn) {
_appRouter
..popUntilRoot()
..replace(const HomeRoute());
} else if (event == AuthChangeEvent.signedOut) {
_appRouter
..popUntilRoot()
..replace(const SignInRoute());
}
});
}

@override
Widget build(BuildContext context) {
return MaterialApp.router(
Expand Down
12 changes: 0 additions & 12 deletions lib/app/home_page.dart

This file was deleted.

8 changes: 8 additions & 0 deletions lib/app/modules/supabase_module.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import 'package:injectable/injectable.dart';
import 'package:supabase_flutter/supabase_flutter.dart';

@module
abstract class SupabaseModule {
@lazySingleton
Supabase supabase() => Supabase.instance;
}
33 changes: 33 additions & 0 deletions lib/app/pages/home_page.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import 'package:flutter/material.dart';
import 'package:flutter_supabase_complete/app/repository/auth_repository.dart';
import 'package:flutter_supabase_complete/injectable.dart';

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

@override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
body: Column(
children: [
ElevatedButton(
onPressed: () => _onClickSignOut(context),
child: const Text("Sign out"),
),
],
),
),
);
}

Future<void> _onClickSignOut(BuildContext context) async {
try {
await getIt<AuthRepository>().signOut();
} catch (e) {
// TODO: Show proper error to users
print("Error on sign out");
print(e);
}
}
}
69 changes: 69 additions & 0 deletions lib/app/pages/sign_in_page.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_supabase_complete/app/repository/auth_repository.dart';
import 'package:flutter_supabase_complete/core/routes/app_router.dart';
import 'package:flutter_supabase_complete/injectable.dart';

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

@override
State<SignInPage> createState() => _SignInPageState();
}

class _SignInPageState extends State<SignInPage> {
String email = "";
String password = "";

@override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
body: Column(
children: [
const Text("Sign in"),
TextField(
decoration: const InputDecoration(
hintText: "Email",
),
onChanged: (value) {
setState(() => email = value);
},
),
TextField(
decoration: const InputDecoration(
hintText: "Password",
),
obscureText: true,
onChanged: (value) {
setState(() => password = value);
},
),
ElevatedButton(
onPressed: () => _onClickSignIn(context),
child: const Text('Sign in'),
),
ElevatedButton(
onPressed: () => _onClickGoToSignUp(context),
child: const Text('Go to sign up'),
),
],
),
),
);
}

Future<void> _onClickSignIn(BuildContext context) async {
try {
await getIt<AuthRepository>().signInEmailAndPassword(email, password);
} catch (e) {
// TODO: Show proper error to users
print("Sign in error");
print(e);
}
}

void _onClickGoToSignUp(BuildContext context) {
context.router.push(const SignUpRoute());
}
}
68 changes: 68 additions & 0 deletions lib/app/pages/sign_up_page.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_supabase_complete/app/repository/auth_repository.dart';
import 'package:flutter_supabase_complete/injectable.dart';

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

@override
State<SignUpPage> createState() => _SignUpPageState();
}

class _SignUpPageState extends State<SignUpPage> {
String email = "";
String password = "";

@override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
body: Column(
children: [
const Text("Sign up"),
TextField(
decoration: const InputDecoration(
hintText: "Email",
),
onChanged: (value) {
setState(() => email = value);
},
),
TextField(
decoration: const InputDecoration(
hintText: "Password",
),
obscureText: true,
onChanged: (value) {
setState(() => password = value);
},
),
ElevatedButton(
onPressed: () => _onClickSignUp(context),
child: const Text('Sign up'),
),
ElevatedButton(
onPressed: () => _onClickGoToSignIn(context),
child: const Text('Go to sign in'),
),
],
),
),
);
}

Future<void> _onClickSignUp(BuildContext context) async {
try {
await getIt<AuthRepository>().signUpEmailAndPassword(email, password);
} catch (e) {
// TODO: Show proper error to users
print("Sign up error");
print(e);
}
}

void _onClickGoToSignIn(BuildContext context) {
context.router.pop();
}
}
58 changes: 58 additions & 0 deletions lib/app/pages/splash_screen_page.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_supabase_complete/core/routes/app_router.dart';
import 'package:supabase_flutter/supabase_flutter.dart';

/// Initial loading route of the app.
///
/// Used to load required information before starting the app (auth).
class SplashScreenPage extends StatefulWidget {
const SplashScreenPage({Key? key}) : super(key: key);

@override
State<SplashScreenPage> createState() => _SplashScreenPageState();
}

class _SplashScreenPageState extends State<SplashScreenPage> {
@override
void initState() {
super.initState();

/// Load auth session.
///
/// Wait a minium `delayed` time in any case
/// to avoid flashing screen.
Future.wait([
SupabaseAuth.instance.initialSession,
Future.delayed(
const Duration(milliseconds: 2000),
),
]).then((responseList) {
final session = responseList.first as Session?;

/// Redirect to either home or sign in routes based on current session.
context.router.replace(
session != null ? const HomeRoute() : const SignInRoute(),
);
}).catchError((_) {
context.router.replace(const SignInRoute());
});
}

@override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
body: Center(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: const [
CircularProgressIndicator(),
],
),
),
),
);
}
}
5 changes: 4 additions & 1 deletion lib/app/repository/auth_repository.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
abstract class AuthRepository {
Future<String> signIn();
Future<String> signInEmailAndPassword(String email, String password);
Future<String> signUpEmailAndPassword(String email, String password);

Future<void> signOut();
}
37 changes: 34 additions & 3 deletions lib/app/services/supabase_auth_repository.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,42 @@
import 'package:flutter_supabase_complete/app/repository/auth_repository.dart';
import 'package:injectable/injectable.dart';
import 'package:supabase_flutter/supabase_flutter.dart';

@Injectable(as: AuthRepository)
class SupabaseAuthRepository implements AuthRepository {
final Supabase _supabase;
const SupabaseAuthRepository(this._supabase);

@override
Future<String> signInEmailAndPassword(String email, String password) async {
final response = await _supabase.client.auth.signIn(
email: email,
password: password,
);

final userId = response.user?.id;
if (userId == null) {
throw UnimplementedError();
}

return userId;
}

@override
Future<String> signUpEmailAndPassword(String email, String password) async {
final response = await _supabase.client.auth.signUp(email, password);

final userId = response.user?.id;
if (userId == null) {
throw UnimplementedError();
}

return userId;
}

@override
Future<String> signIn() {
// TODO: implement signIn
throw UnimplementedError();
Future<void> signOut() async {
await _supabase.client.auth.signOut();
return;
}
}
10 changes: 8 additions & 2 deletions lib/core/routes/app_router.dart
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
/// Make sure to import `auto_route` and `material` (required)
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_supabase_complete/app/home_page.dart';
import 'package:flutter_supabase_complete/app/pages/home_page.dart';
import 'package:flutter_supabase_complete/app/pages/sign_in_page.dart';
import 'package:flutter_supabase_complete/app/pages/sign_up_page.dart';
import 'package:flutter_supabase_complete/app/pages/splash_screen_page.dart';

part 'app_router.gr.dart';

@MaterialAutoRouter(
replaceInRouteName: 'Page,Route',
routes: <AutoRoute>[
AutoRoute(page: HomePage, initial: true),
AutoRoute(page: SplashScreenPage, initial: true),
AutoRoute(page: SignUpPage),
AutoRoute(page: SignInPage),
AutoRoute(page: HomePage),
],
)
class AppRouter extends _$AppRouter {}
Loading

0 comments on commit df52a44

Please sign in to comment.