Skip to content

Commit bad84bd

Browse files
authored
feat(auth): complete Google OAuth integration with Deep Link and Provider (#18)
* feat(core): add auth layout template, custom textfield and colors * feat(auth): implement viewmodels for auth flow (MVVM) * feat(auth): build complete auth UI screens (Login, Register, OTP, Passwords) * chore(main): set LoginView as initial route * refactor(auth) : delete .gitkeep * chore: update dependencies and pubspec.lock * refactor(auth): optimize registration logic, timezone handling, and form validation * feat(auth): update UI for login, registration, and forgot password screens * feat(tasks): update task management UI and statistics screen * chore: update main entry point and fix widget tests * chore: ignore devtools_options.yaml * chore: ignore devtools_options.yaml * style(login) : rewrite title for login view * feat(auth): configure android deep link for supabase oauth * refactor(ui): add social login callbacks to auth layout template * feat(auth): update oauth methods with redirect url and signout * feat(auth): implement AuthGate using StreamBuilder for session tracking * feat(viewmodel): add oauth logic and improve provider lifecycle * refactor(ui): migrate LoginView to Provider pattern * chore(main): set AuthGate as initial route and setup provider
1 parent 4b84337 commit bad84bd

7 files changed

Lines changed: 127 additions & 3 deletions

File tree

src/android/app/src/main/AndroidManifest.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,14 @@
1616
the Android process has started. This theme is visible to the user
1717
while the Flutter UI initializes. After that, this theme continues
1818
to determine the Window background behind the Flutter UI. -->
19+
<intent-filter>
20+
<action android:name="android.intent.action.VIEW" />
21+
<category android:name="android.intent.category.DEFAULT" />
22+
<category android:name="android.intent.category.BROWSABLE" />
23+
<data
24+
android:scheme="taskapp"
25+
android:host="login-callback" />
26+
</intent-filter>
1927
<meta-data
2028
android:name="io.flutter.embedding.android.NormalTheme"
2129
android:resource="@style/NormalTheme"

src/lib/core/theme/auth_layout_template.dart

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ class AuthLayoutTemplate extends StatelessWidget {
1414
final bool useCard;
1515
final Widget? customHeaderIcon;
1616
final Widget? footerContent;
17+
final VoidCallback? onGoogleTap; // Login with Google
18+
final VoidCallback? onFacebookTap; // Login with Facebook
1719

1820
const AuthLayoutTemplate({
1921
super.key,
@@ -27,6 +29,8 @@ class AuthLayoutTemplate extends StatelessWidget {
2729
this.useCard = true,
2830
this.customHeaderIcon,
2931
this.footerContent,
32+
this.onGoogleTap,
33+
this.onFacebookTap,
3034
});
3135

3236
@override
@@ -200,7 +204,7 @@ class AuthLayoutTemplate extends StatelessWidget {
200204
children: [
201205
Expanded(
202206
child: OutlinedButton.icon(
203-
onPressed: () {},
207+
onPressed: onGoogleTap,
204208
icon: const Icon(
205209
Icons.g_mobiledata,
206210
color: Colors.red,
@@ -212,7 +216,7 @@ class AuthLayoutTemplate extends StatelessWidget {
212216
const SizedBox(width: 16),
213217
Expanded(
214218
child: OutlinedButton.icon(
215-
onPressed: () {},
219+
onPressed: onFacebookTap,
216220
icon: const Icon(Icons.facebook, color: Color(0xFF1877F2)),
217221
label: const Text('Facebook'),
218222
),

src/lib/features/auth/data/auth_helper.dart

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,30 @@ class AuthHelper {
4040
}
4141
}
4242

43+
Future<bool> loginWithGoogle() async {
44+
try {
45+
return await supabase.auth.signInWithOAuth(
46+
OAuthProvider.google,
47+
redirectTo: 'taskapp://login-callback', // back to app
48+
);
49+
} on AuthException catch (e) {
50+
print('Lỗi đăng nhập Google: ${e.message}');
51+
return false;
52+
}
53+
}
54+
55+
Future<bool> loginWithFacebook() async {
56+
try {
57+
return await supabase.auth.signInWithOAuth(
58+
OAuthProvider.facebook,
59+
redirectTo: 'taskapp://login-callback', // back to app
60+
);
61+
} on AuthException catch (e) {
62+
print('Lỗi đăng nhập Facebook: ${e.message}');
63+
return false;
64+
}
65+
}
66+
4367
Future<UserModel?> register(String email, String password, String username) async {
4468
try {
4569
final timezoneObj = await FlutterTimezone.getLocalTimezone();
@@ -106,4 +130,14 @@ class AuthHelper {
106130
rethrow;
107131
}
108132
}
133+
}
134+
135+
// SignOut session
136+
Future<void> signOut() async {
137+
try {
138+
await supabase.auth.signOut();
139+
} catch (e) {
140+
print('Lỗi đăng xuất: $e');
141+
rethrow;
142+
}
109143
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:supabase_flutter/supabase_flutter.dart';
3+
import 'login_view.dart';
4+
import '../../../main/view/screens/main_screen.dart';
5+
6+
class AuthGate extends StatelessWidget {
7+
const AuthGate({super.key});
8+
9+
@override
10+
Widget build(BuildContext context) {
11+
// StreamBuilder continously checks the auth state
12+
return StreamBuilder<AuthState>(
13+
stream: Supabase.instance.client.auth.onAuthStateChange,
14+
builder: (context, snapshot) {
15+
// Wait response from Supabase
16+
if (snapshot.connectionState == ConnectionState.waiting) {
17+
return const Scaffold(
18+
body: Center(child: CircularProgressIndicator()),
19+
);
20+
}
21+
22+
// Check if there is an active session ( user logged in )
23+
final session = snapshot.data?.session;
24+
25+
// if session exists -> Navigate to MainScreen
26+
if (session != null) {
27+
return const MainScreen();
28+
}
29+
30+
// if session not exists -> Navigate to LoginView
31+
return const LoginView();
32+
},
33+
);
34+
}
35+
}

src/lib/features/auth/presentation/view/login_view.dart

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,18 @@ class _LoginViewState extends State<LoginView> {
2525
submitText: 'Đăng nhập',
2626
isLoading: _vm.isLoading,
2727
showSocial: true,
28+
onGoogleTap: () async {
29+
final error = await _vm.loginWithGoogle();
30+
if (error != null && context.mounted) {
31+
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(error), backgroundColor: AppColors.error));
32+
}
33+
},
34+
onFacebookTap: () async {
35+
final error = await _vm.loginWithFacebook();
36+
if (error != null && context.mounted) {
37+
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(error), backgroundColor: AppColors.error));
38+
}
39+
},
2840
onSubmit: () async {
2941
FocusScope.of(context).unfocus();
3042
final errorMessage = await _vm.login();

src/lib/features/auth/presentation/viewmodels/auth_viewmodels.dart

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,37 @@ class LoginViewModel extends BaseViewModel {
9595
setLoading(false);
9696
}
9797
}
98+
// 1.1 LOGIN WITH GOOGLE
99+
Future<String?> loginWithGoogle() async {
100+
setLoading(true);
101+
try {
102+
final success = await _authHelper.loginWithGoogle();
103+
if (success) return null; // Thành công (thường Supabase sẽ tự văng ra web browser)
104+
return 'Lỗi khi mở cổng đăng nhập Google!';
105+
} catch (e) {
106+
return handleError(e);
107+
} finally {
108+
setLoading(false);
109+
}
110+
}
111+
112+
// 1.2 LOGIN WITH FACEBOOK
113+
114+
Future<String?> loginWithFacebook() async {
115+
setLoading(true);
116+
try {
117+
final success = await _authHelper.loginWithFacebook();
118+
if (success) return null;
119+
return 'Lỗi khi mở cổng đăng nhập Facebook!';
120+
} catch (e) {
121+
return handleError(e);
122+
} finally {
123+
setLoading(false);
124+
}
125+
}
98126
}
99127

128+
100129
// ==========================================
101130
// 2. REGISTER VIEWMODEL
102131
// ==========================================

src/lib/main.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ import 'package:flutter/material.dart';
22
import 'package:flutter/services.dart';
33
import 'core/theme/app_colors.dart';
44
import 'features/auth/presentation/view/login_view.dart';
5+
import 'features/auth/presentation/view/auth_gate.dart';
56
import 'package:flutter_dotenv/flutter_dotenv.dart';
67
import 'package:supabase_flutter/supabase_flutter.dart';
78

9+
810
Future<void> main() async {
911
WidgetsFlutterBinding.ensureInitialized();
1012
await dotenv.load(fileName: ".env");
@@ -42,7 +44,7 @@ class TaskApp extends StatelessWidget {
4244
labelLarge: TextStyle(fontSize: 16, color: AppColors.primaryBlue),
4345
),
4446
),
45-
home: const LoginView(),
47+
home: const AuthGate(),
4648
debugShowCheckedModeBanner: false,
4749
);
4850
}

0 commit comments

Comments
 (0)