Merge pull request #1 from sBubshait/feature/login-page-authentication

Feature/login page authentication
This commit is contained in:
Saleh Bubshait 2025-07-21 10:19:09 +03:00 committed by GitHub
commit 969cbf1e8d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 1149 additions and 417 deletions

View File

@ -6,6 +6,7 @@ import jakarta.validation.Valid;
import online.wesal.wesal.dto.CreateUserRequest; import online.wesal.wesal.dto.CreateUserRequest;
import online.wesal.wesal.dto.LoginRequest; import online.wesal.wesal.dto.LoginRequest;
import online.wesal.wesal.dto.LoginResponse; import online.wesal.wesal.dto.LoginResponse;
import online.wesal.wesal.dto.UpdateUserRequest;
import online.wesal.wesal.dto.UsernameSelectionRequest; import online.wesal.wesal.dto.UsernameSelectionRequest;
import online.wesal.wesal.entity.User; import online.wesal.wesal.entity.User;
import online.wesal.wesal.service.AuthService; import online.wesal.wesal.service.AuthService;
@ -67,6 +68,22 @@ public class AuthController {
} }
} }
@PostMapping("/updateUser")
@Operation(summary = "Update user information", description = "Update authenticated user's fcmToken, displayName, avatar, or password")
public ResponseEntity<Map<String, Object>> updateUser(@Valid @RequestBody UpdateUserRequest request) {
try {
User user = userService.updateUser(
request.getFcmToken(),
request.getDisplayName(),
request.getAvatar(),
request.getPassword()
);
return ResponseEntity.ok(Map.of("status", 200, "user", user));
} catch (RuntimeException e) {
return ResponseEntity.badRequest().body(Map.of("status", 400, "message", e.getMessage()));
}
}
@PostMapping("/admin/createUser") @PostMapping("/admin/createUser")
@Operation(summary = "Create new user (Admin only)", description = "Creates a new user - requires admin privileges") @Operation(summary = "Create new user (Admin only)", description = "Creates a new user - requires admin privileges")
public ResponseEntity<?> createUser(@Valid @RequestBody CreateUserRequest request) { public ResponseEntity<?> createUser(@Valid @RequestBody CreateUserRequest request) {

View File

@ -0,0 +1,49 @@
package online.wesal.wesal.dto;
import jakarta.validation.constraints.Size;
public class UpdateUserRequest {
private String fcmToken;
private String displayName;
private String avatar;
@Size(min = 8, message = "Password must be at least 8 characters long")
private String password;
public UpdateUserRequest() {}
public String getFcmToken() {
return fcmToken;
}
public void setFcmToken(String fcmToken) {
this.fcmToken = fcmToken;
}
public String getDisplayName() {
return displayName;
}
public void setDisplayName(String displayName) {
this.displayName = displayName;
}
public String getAvatar() {
return avatar;
}
public void setAvatar(String avatar) {
this.avatar = avatar;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}

View File

@ -55,4 +55,26 @@ public class UserService {
User user = new User(email, passwordEncoder.encode(password), displayName); User user = new User(email, passwordEncoder.encode(password), displayName);
return userRepository.save(user); return userRepository.save(user);
} }
public User updateUser(String fcmToken, String displayName, String avatar, String password) {
User user = getCurrentUser();
if (fcmToken != null && !fcmToken.trim().isEmpty()) {
user.setFcmToken(fcmToken);
}
if (displayName != null && !displayName.trim().isEmpty()) {
user.setDisplayName(displayName);
}
if (avatar != null) {
user.setAvatar(avatar);
}
if (password != null && !password.trim().isEmpty()) {
user.setPassword(passwordEncoder.encode(password));
}
return userRepository.save(user);
}
} }

View File

@ -0,0 +1,10 @@
class ApiConstants {
static const String baseUrl = 'http://localhost:8080';
// Auth endpoints
static const String loginEndpoint = '/login';
// User endpoints
static const String getUserEndpoint = '/getUser';
static const String updateUserEndpoint = '/updateUser';
}

View File

@ -2,7 +2,10 @@ import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_core/firebase_core.dart';
import 'firebase_options.dart'; import 'firebase_options.dart';
import 'screens/home_screen.dart'; import 'screens/home_screen.dart';
import 'screens/notification_permission_screen.dart';
import 'services/notification_service.dart'; import 'services/notification_service.dart';
import 'services/auth_service.dart';
import 'services/user_service.dart';
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
@ -17,74 +20,107 @@ class MyApp extends StatelessWidget {
return MaterialApp( return MaterialApp(
title: 'Wesal', title: 'Wesal',
theme: ThemeData(primarySwatch: Colors.blue, fontFamily: 'Roboto'), theme: ThemeData(primarySwatch: Colors.blue, fontFamily: 'Roboto'),
home: LandingPage(), home: SplashScreen(),
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
); );
} }
} }
class SplashScreen extends StatefulWidget {
@override
_SplashScreenState createState() => _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen> {
@override
void initState() {
super.initState();
_checkAuthenticationStatus();
}
Future<void> _checkAuthenticationStatus() async {
await Future.delayed(Duration(milliseconds: 500));
final isLoggedIn = await AuthService.isLoggedIn();
if (isLoggedIn) {
final userResult = await UserService.getCurrentUser();
if (userResult['success'] == true) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (context) => HomeScreen()),
);
} else {
await AuthService.logout();
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (context) => LandingPage()),
);
}
} else {
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (context) => LandingPage()),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color(0xFF32B0A5),
Color(0xFF4600B9),
],
stops: [0.0, 0.5],
),
),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'وصال',
style: TextStyle(
fontSize: 160,
fontWeight: FontWeight.w200,
fontFamily: 'Blaka',
color: Colors.white,
shadows: [
Shadow(
offset: Offset(2, 2),
blurRadius: 4,
color: Colors.black.withOpacity(0.3),
),
],
),
),
SizedBox(height: 40),
CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
],
),
),
),
);
}
}
class LandingPage extends StatefulWidget { class LandingPage extends StatefulWidget {
@override @override
_LandingPageState createState() => _LandingPageState(); _LandingPageState createState() => _LandingPageState();
} }
class _LandingPageState extends State<LandingPage> class _LandingPageState extends State<LandingPage> {
with SingleTickerProviderStateMixin { void _navigateDirectlyToLogin() {
late AnimationController _animationController;
late Animation<double> _slideAnimation;
bool _showBottomSheet = false;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: Duration(milliseconds: 800),
vsync: this,
);
_slideAnimation = Tween<double>(begin: 1.0, end: 0.0).animate(
CurvedAnimation(parent: _animationController, curve: Curves.easeInOut),
);
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
void _showGetStartedBottomSheet() {
setState(() {
_showBottomSheet = true;
});
_animationController.forward();
// Request notification permissions on user action
NotificationService().requestPermissionAndSetup();
}
void _hideBottomSheet() {
_animationController.reverse().then((_) {
setState(() {
_showBottomSheet = false;
});
});
}
void _navigateToSignIn() {
Navigator.of( Navigator.of(
context, context,
).push(MaterialPageRoute(builder: (context) => SignInPage())); ).push(MaterialPageRoute(builder: (context) => SignInPage()));
} }
void _navigateToHome() {
// Request notification permissions on user action
NotificationService().requestPermissionAndSetup();
Navigator.of(
context,
).push(MaterialPageRoute(builder: (context) => HomeScreen()));
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@ -156,7 +192,7 @@ class _LandingPageState extends State<LandingPage>
width: 280, width: 280,
height: 56, height: 56,
child: ElevatedButton( child: ElevatedButton(
onPressed: _showGetStartedBottomSheet, onPressed: _navigateDirectlyToLogin,
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Color(0xFF6A4C93), backgroundColor: Color(0xFF6A4C93),
foregroundColor: Colors.white, foregroundColor: Colors.white,
@ -183,25 +219,6 @@ class _LandingPageState extends State<LandingPage>
), ),
SizedBox(height: 24), SizedBox(height: 24),
// Login link
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
GestureDetector(
onTap: _navigateToHome,
child: Text(
'Skip to Home',
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w600,
decoration: TextDecoration.underline,
),
),
),
],
),
], ],
), ),
), ),
@ -209,158 +226,6 @@ class _LandingPageState extends State<LandingPage>
], ],
), ),
), ),
// Bottom sheet overlay
if (_showBottomSheet)
AnimatedBuilder(
animation: _slideAnimation,
builder: (context, child) {
return Positioned(
bottom: 0,
left: 0,
right: 0,
child: Transform.translate(
offset: Offset(0, _slideAnimation.value * 400),
child: Container(
height: 400,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(24),
topRight: Radius.circular(24),
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 20,
offset: Offset(0, -5),
),
],
),
child: Column(
children: [
// Handle bar
Container(
margin: EdgeInsets.only(top: 12),
width: 40,
height: 4,
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: BorderRadius.circular(2),
),
),
// Close button
Align(
alignment: Alignment.topRight,
child: IconButton(
onPressed: _hideBottomSheet,
icon: Icon(Icons.close, color: Colors.grey[600]),
),
),
Expanded(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 24),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Welcome In',
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
SizedBox(height: 12),
Text(
'Join your community.. change your social life!',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16,
color: Colors.grey[600],
),
),
SizedBox(height: 40),
// Sign Up button
Container(
width: double.infinity,
height: 56,
child: ElevatedButton(
onPressed: () {
// Handle sign up
ScaffoldMessenger.of(
context,
).showSnackBar(
SnackBar(
content: Text('Sign Up pressed'),
),
);
},
style: ElevatedButton.styleFrom(
backgroundColor: Color(0xFF6A4C93),
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
12,
),
),
elevation: 2,
),
child: Text(
'Sign Up Now',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
),
),
SizedBox(height: 16),
// Login button
Container(
width: double.infinity,
height: 56,
child: OutlinedButton(
onPressed: _navigateToSignIn,
style: OutlinedButton.styleFrom(
foregroundColor: Color(0xFF6A4C93),
side: BorderSide(
color: Color(0xFF6A4C93),
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
12,
),
),
),
child: Text(
'Login',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
),
),
],
),
),
),
],
),
),
),
);
},
),
], ],
), ),
); );
@ -379,6 +244,72 @@ class _SignInPageState extends State<SignInPage> {
bool _isPasswordVisible = false; bool _isPasswordVisible = false;
bool _isLoading = false; bool _isLoading = false;
void _showHelpBottomSheet() {
showModalBottomSheet(
context: context,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
),
builder: (context) => Container(
padding: EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 40,
height: 4,
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: BorderRadius.circular(2),
),
),
SizedBox(height: 24),
Icon(Icons.contact_support, size: 48, color: Color(0xFF6A4C93)),
SizedBox(height: 16),
Text(
'Need Help?',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
SizedBox(height: 16),
Text(
'For account creation or password reset, please contact ERP Management Group from your Aramco email address.',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16,
color: Colors.grey[700],
height: 1.4,
),
),
SizedBox(height: 24),
Container(
width: double.infinity,
child: ElevatedButton(
onPressed: () => Navigator.pop(context),
style: ElevatedButton.styleFrom(
backgroundColor: Color(0xFF6A4C93),
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
padding: EdgeInsets.symmetric(vertical: 16),
),
child: Text(
'Got It',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
),
),
),
SizedBox(height: 16),
],
),
),
);
}
@override @override
void dispose() { void dispose() {
_emailController.dispose(); _emailController.dispose();
@ -392,19 +323,46 @@ class _SignInPageState extends State<SignInPage> {
_isLoading = true; _isLoading = true;
}); });
// Simulate API call final result = await AuthService.login(
await Future.delayed(Duration(seconds: 2)); _emailController.text.trim(),
_passwordController.text,
);
setState(() { setState(() {
_isLoading = false; _isLoading = false;
}); });
ScaffoldMessenger.of( if (result['success'] == true) {
context, final userResult = await UserService.getCurrentUser(forceRefresh: true);
).showSnackBar(SnackBar(content: Text('Sign in successful!')));
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (context) => NotificationPermissionScreen()),
);
} else {
_showErrorAlert(result['message'] ?? 'Login failed');
}
} }
} }
void _showErrorAlert(String message) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Login Failed'),
content: Text(message),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(
'OK',
style: TextStyle(color: Color(0xFF6A4C93)),
),
),
],
),
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@ -493,7 +451,7 @@ class _SignInPageState extends State<SignInPage> {
SizedBox(height: 8), SizedBox(height: 8),
Text( Text(
'Sign in to your account', 'Sign in to socialize with your colleagues\nand transform your social life!',
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
color: Colors.grey[600], color: Colors.grey[600],
@ -575,13 +533,7 @@ class _SignInPageState extends State<SignInPage> {
Align( Align(
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
child: TextButton( child: TextButton(
onPressed: () { onPressed: _showHelpBottomSheet,
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Forgot password pressed'),
),
);
},
child: Text( child: Text(
'Forgot Password?', 'Forgot Password?',
style: TextStyle( style: TextStyle(
@ -625,7 +577,7 @@ class _SignInPageState extends State<SignInPage> {
SizedBox(height: 20), SizedBox(height: 20),
// Sign up link // Contact link for new accounts
Row( Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
@ -634,13 +586,9 @@ class _SignInPageState extends State<SignInPage> {
style: TextStyle(color: Colors.grey[600]), style: TextStyle(color: Colors.grey[600]),
), ),
GestureDetector( GestureDetector(
onTap: () { onTap: _showHelpBottomSheet,
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Sign up pressed')),
);
},
child: Text( child: Text(
'Sign Up', 'Contact Support',
style: TextStyle( style: TextStyle(
color: Color(0xFF6A4C93), color: Color(0xFF6A4C93),
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,

View File

@ -0,0 +1,242 @@
import 'package:flutter/material.dart';
import '../services/notification_service.dart';
import '../services/user_service.dart';
import 'home_screen.dart';
class NotificationPermissionScreen extends StatefulWidget {
@override
_NotificationPermissionScreenState createState() =>
_NotificationPermissionScreenState();
}
class _NotificationPermissionScreenState
extends State<NotificationPermissionScreen> {
bool _isLoading = false;
Future<void> _requestNotificationPermission() async {
setState(() {
_isLoading = true;
});
try {
final notificationService = NotificationService();
await notificationService.requestPermissionAndSetup();
final fcmToken = await notificationService.getToken();
if (fcmToken != null) {
final result = await UserService.updateFCMToken(fcmToken);
if (result['success'] == true) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (context) => HomeScreen()),
);
} else {
_showErrorAndContinue(
result['message'] ?? 'Failed to update notification settings',
);
}
} else {
_showErrorAndContinue('Failed to get notification token');
}
} catch (e) {
_showErrorAndContinue('Error setting up notifications: $e');
}
setState(() {
_isLoading = false;
});
}
void _showErrorAndContinue(String message) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Notification Setup'),
content: Text(
'$message\n\nYou can enable notifications later in settings.',
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (context) => HomeScreen()),
);
},
child: Text('Continue', style: TextStyle(color: Color(0xFF6A4C93))),
),
],
),
);
}
void _skipNotifications() {
Navigator.of(
context,
).pushReplacement(MaterialPageRoute(builder: (context) => HomeScreen()));
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0xFF32B0A5), Color(0xFF4600B9)],
stops: [0.0, 0.5],
),
),
child: SafeArea(
child: Padding(
padding: EdgeInsets.all(24),
child: Column(
children: [
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 200,
height: 200,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
shape: BoxShape.circle,
),
child: Icon(
Icons.rocket_launch,
size: 100,
color: Colors.white,
),
),
SizedBox(height: 40),
Text(
'All Set!',
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
SizedBox(height: 16),
Text(
'But one last thing...',
style: TextStyle(
fontSize: 18,
color: Colors.white.withOpacity(0.9),
),
),
SizedBox(height: 40),
Container(
padding: EdgeInsets.all(24),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.1),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: Colors.white.withOpacity(0.3),
),
),
child: Column(
children: [
Icon(
Icons.notifications_active,
size: 40,
color: Colors.white,
),
SizedBox(height: 16),
Text(
'Stay Connected',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
SizedBox(height: 8),
Text(
'We will only send you updates on your own invitations or posts. You can always change your notification settings later.',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16,
color: Colors.white.withOpacity(0.8),
height: 1.4,
),
),
],
),
),
],
),
),
Column(
children: [
Container(
width: double.infinity,
height: 56,
child: ElevatedButton(
onPressed: _isLoading
? null
: _requestNotificationPermission,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: Color(0xFF6A4C93),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 4,
),
child: _isLoading
? CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(
Color(0xFF6A4C93),
),
)
: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.notifications, size: 20),
SizedBox(width: 8),
Text(
'Enable Notifications',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
],
),
),
),
SizedBox(height: 16),
TextButton(
onPressed: _isLoading ? null : _skipNotifications,
child: Text(
'Skip for now',
style: TextStyle(
color: Colors.white.withOpacity(0.8),
fontSize: 16,
decoration: TextDecoration.underline,
),
),
),
],
),
],
),
),
),
),
);
}
}

View File

@ -76,6 +76,7 @@ class _FeedPageState extends State<FeedPage> {
preferredSize: Size.fromHeight(1), preferredSize: Size.fromHeight(1),
child: Container(height: 1, color: Colors.grey[200]), child: Container(height: 1, color: Colors.grey[200]),
), ),
automaticallyImplyLeading: false,
), ),
body: RefreshIndicator( body: RefreshIndicator(
onRefresh: () async { onRefresh: () async {

View File

@ -79,6 +79,7 @@ class _InvitationsPageState extends State<InvitationsPage> {
preferredSize: Size.fromHeight(1), preferredSize: Size.fromHeight(1),
child: Container(height: 1, color: Colors.grey[200]), child: Container(height: 1, color: Colors.grey[200]),
), ),
automaticallyImplyLeading: false,
), ),
body: Padding( body: Padding(
padding: EdgeInsets.all(16), padding: EdgeInsets.all(16),
@ -256,7 +257,9 @@ class _InvitationsPageState extends State<InvitationsPage> {
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
onPressed: () { onPressed: () {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Create invitation functionality coming soon!')), SnackBar(
content: Text('Create invitation functionality coming soon!'),
),
); );
}, },
backgroundColor: Color(0xFF6A4C93), backgroundColor: Color(0xFF6A4C93),

View File

@ -1,6 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import '../../services/notification_service.dart'; import '../../services/notification_service.dart';
import '../../services/user_service.dart';
import '../../services/auth_service.dart';
class ProfilePage extends StatefulWidget { class ProfilePage extends StatefulWidget {
@override @override
@ -11,6 +13,8 @@ class _ProfilePageState extends State<ProfilePage> {
String? fcmToken; String? fcmToken;
final TextEditingController _tokenController = TextEditingController(); final TextEditingController _tokenController = TextEditingController();
bool isLoading = false; bool isLoading = false;
Map<String, dynamic>? userData;
bool isLoadingUser = true;
final List<Map<String, dynamic>> mockUserPosts = [ final List<Map<String, dynamic>> mockUserPosts = [
{ {
@ -46,6 +50,7 @@ class _ProfilePageState extends State<ProfilePage> {
void initState() { void initState() {
super.initState(); super.initState();
_loadFCMToken(); _loadFCMToken();
_loadUserData();
} }
@override @override
@ -75,6 +80,40 @@ class _ProfilePageState extends State<ProfilePage> {
} }
} }
Future<void> _loadUserData() async {
setState(() {
isLoadingUser = true;
});
final result = await UserService.getCurrentUser();
setState(() {
isLoadingUser = false;
if (result['success'] == true) {
userData = result['data'];
} else {
userData = null;
_showErrorAlert(result['message'] ?? 'Failed to load user data');
}
});
}
void _showErrorAlert(String message) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Error'),
content: Text(message),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text('OK', style: TextStyle(color: Color(0xFF6A4C93))),
),
],
),
);
}
Future<void> _copyToClipboard() async { Future<void> _copyToClipboard() async {
if (fcmToken != null && fcmToken!.isNotEmpty) { if (fcmToken != null && fcmToken!.isNotEmpty) {
await Clipboard.setData(ClipboardData(text: fcmToken!)); await Clipboard.setData(ClipboardData(text: fcmToken!));
@ -119,200 +158,236 @@ class _ProfilePageState extends State<ProfilePage> {
padding: EdgeInsets.all(24), padding: EdgeInsets.all(24),
child: Column( child: Column(
children: [ children: [
CircleAvatar( if (isLoadingUser)
radius: 50, CircularProgressIndicator(
backgroundColor: Color(0xFF6A4C93), valueColor: AlwaysStoppedAnimation<Color>(
child: Text( Color(0xFF6A4C93),
'A', ),
)
else if (userData != null) ...[
CircleAvatar(
radius: 50,
backgroundColor: Color(0xFF6A4C93),
child: Text(
(userData!['displayName'] ?? 'U')
.substring(0, 1)
.toUpperCase(),
style: TextStyle(
color: Colors.white,
fontSize: 36,
fontWeight: FontWeight.bold,
),
),
),
SizedBox(height: 16),
Text(
userData!['displayName'] ?? 'Unknown User',
style: TextStyle( style: TextStyle(
color: Colors.white, fontSize: 24,
fontSize: 36,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.black87,
), ),
), ),
), SizedBox(height: 4),
SizedBox(height: 16), Text(
Text( '@${userData!['username'] ?? 'unknown'}',
'Abu Norah', style: TextStyle(fontSize: 16, color: Colors.grey[600]),
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.black87,
), ),
), ] else ...[
SizedBox(height: 4), Icon(
Text( Icons.error_outline,
'@yasser_hajri', size: 50,
style: TextStyle(fontSize: 16, color: Colors.grey[600]), color: Colors.grey[400],
), ),
SizedBox(height: 16),
Text(
'Failed to load user data',
style: TextStyle(fontSize: 18, color: Colors.grey[600]),
),
SizedBox(height: 8),
ElevatedButton(
onPressed: _loadUserData,
style: ElevatedButton.styleFrom(
backgroundColor: Color(0xFF6A4C93),
foregroundColor: Colors.white,
),
child: Text('Retry'),
),
],
SizedBox(height: 20), SizedBox(height: 20),
Row( if (userData != null)
mainAxisAlignment: MainAxisAlignment.spaceEvenly, Row(
children: [ mainAxisAlignment: MainAxisAlignment.spaceEvenly,
Column(
children: [
Text(
'${mockUserPosts.length}',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Color(0xFF6A4C93),
),
),
Text(
'Posts',
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
),
),
],
),
Column(
children: [
Text(
'127',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Color(0xFF6A4C93),
),
),
Text(
'Followers',
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
),
),
],
),
],
),
],
),
),
Container(
height: 1,
color: Colors.grey[200],
margin: EdgeInsets.symmetric(horizontal: 16),
),
SizedBox(height: 16),
ListView.builder(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
padding: EdgeInsets.symmetric(horizontal: 16),
itemCount: mockUserPosts.length,
itemBuilder: (context, index) {
final post = mockUserPosts[index];
return Container(
margin: EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: Offset(0, 2),
),
],
),
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row( Column(
children: [ children: [
CircleAvatar( Text(
radius: 16, '${mockUserPosts.length}',
backgroundColor: Color(0xFF6A4C93), style: TextStyle(
child: Text( fontSize: 20,
'J', fontWeight: FontWeight.bold,
style: TextStyle( color: Color(0xFF6A4C93),
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 14,
),
), ),
), ),
SizedBox(width: 12), Text(
Expanded( 'Posts',
child: Column( style: TextStyle(
crossAxisAlignment: CrossAxisAlignment.start, fontSize: 14,
children: [ color: Colors.grey[600],
Text(
'Abu Norah',
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 14,
color: Colors.black87,
),
),
Text(
post['timestamp'],
style: TextStyle(
color: Colors.grey[600],
fontSize: 12,
),
),
],
), ),
), ),
], ],
), ),
SizedBox(height: 12), Column(
Text(
post['content'],
style: TextStyle(
fontSize: 15,
height: 1.4,
color: Colors.black87,
),
),
SizedBox(height: 12),
Row(
children: [ children: [
Icon(
post['isLiked']
? Icons.favorite
: Icons.favorite_border,
color: post['isLiked']
? Colors.red
: Colors.grey[600],
size: 20,
),
SizedBox(width: 4),
Text( Text(
'${post['likes']}', '127',
style: TextStyle( style: TextStyle(
color: Colors.grey[700], fontSize: 20,
fontSize: 14, fontWeight: FontWeight.bold,
color: Color(0xFF6A4C93),
), ),
), ),
SizedBox(width: 16),
Icon(
Icons.chat_bubble_outline,
color: Colors.grey[600],
size: 20,
),
SizedBox(width: 4),
Text( Text(
'${post['comments']}', 'Followers',
style: TextStyle( style: TextStyle(
color: Colors.grey[700],
fontSize: 14, fontSize: 14,
color: Colors.grey[600],
), ),
), ),
], ],
), ),
], ],
), ),
), ],
); ),
},
), ),
if (userData != null) ...[
Container(
height: 1,
color: Colors.grey[200],
margin: EdgeInsets.symmetric(horizontal: 16),
),
SizedBox(height: 16),
ListView.builder(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
padding: EdgeInsets.symmetric(horizontal: 16),
itemCount: mockUserPosts.length,
itemBuilder: (context, index) {
final post = mockUserPosts[index];
return Container(
margin: EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: Offset(0, 2),
),
],
),
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
CircleAvatar(
radius: 16,
backgroundColor: Color(0xFF6A4C93),
child: Text(
(userData!['displayName'] ?? 'U')
.substring(0, 1)
.toUpperCase(),
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
),
SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
userData!['displayName'] ??
'Unknown User',
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 14,
color: Colors.black87,
),
),
Text(
post['timestamp'],
style: TextStyle(
color: Colors.grey[600],
fontSize: 12,
),
),
],
),
),
],
),
SizedBox(height: 12),
Text(
post['content'],
style: TextStyle(
fontSize: 15,
height: 1.4,
color: Colors.black87,
),
),
SizedBox(height: 12),
Row(
children: [
Icon(
post['isLiked']
? Icons.favorite
: Icons.favorite_border,
color: post['isLiked']
? Colors.red
: Colors.grey[600],
size: 20,
),
SizedBox(width: 4),
Text(
'${post['likes']}',
style: TextStyle(
color: Colors.grey[700],
fontSize: 14,
),
),
SizedBox(width: 16),
Icon(
Icons.chat_bubble_outline,
color: Colors.grey[600],
size: 20,
),
SizedBox(width: 4),
Text(
'${post['comments']}',
style: TextStyle(
color: Colors.grey[700],
fontSize: 14,
),
),
],
),
],
),
),
);
},
),
],
SizedBox(height: 24), SizedBox(height: 24),
], ],
), ),
@ -376,7 +451,7 @@ class _SettingsPageState extends State<SettingsPage> {
} }
} }
void _signOut() { void _signOut() async {
showDialog( showDialog(
context: context, context: context,
builder: (BuildContext context) { builder: (BuildContext context) {
@ -389,11 +464,14 @@ class _SettingsPageState extends State<SettingsPage> {
child: Text('Cancel'), child: Text('Cancel'),
), ),
TextButton( TextButton(
onPressed: () { onPressed: () async {
Navigator.of(context).pop(); Navigator.of(context).pop();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Signed out successfully')), await AuthService.logout();
);
Navigator.of(
context,
).pushNamedAndRemoveUntil('/', (route) => false);
}, },
child: Text('Sign Out', style: TextStyle(color: Colors.red)), child: Text('Sign Out', style: TextStyle(color: Colors.red)),
), ),
@ -415,6 +493,7 @@ class _SettingsPageState extends State<SettingsPage> {
preferredSize: Size.fromHeight(1), preferredSize: Size.fromHeight(1),
child: Container(height: 1, color: Colors.grey[200]), child: Container(height: 1, color: Colors.grey[200]),
), ),
automaticallyImplyLeading: false,
), ),
body: Padding( body: Padding(
padding: EdgeInsets.all(16), padding: EdgeInsets.all(16),

View File

@ -0,0 +1,90 @@
import 'dart:convert';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:http/http.dart' as http;
import '../constants/api_constants.dart';
class AuthService {
static const FlutterSecureStorage _storage = FlutterSecureStorage();
static const String _tokenKey = 'jwt_token';
static const String _userDataKey = 'user_data';
static Future<Map<String, dynamic>> login(
String emailOrUsername,
String password,
) async {
try {
final response = await http.post(
Uri.parse('${ApiConstants.baseUrl}${ApiConstants.loginEndpoint}'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({
'emailOrUsername': emailOrUsername,
'password': password,
}),
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
final token = data['token'];
if (token != null) {
await _storage.write(key: _tokenKey, value: token);
return {'success': true};
}
return {'success': false, 'message': 'No token received'};
} else if (response.statusCode == 403 || response.statusCode == 400) {
return {'success': false, 'message': 'Invalid credentials'};
} else if (response.statusCode == 401) {
return {'success': false, 'message': 'Invalid credentials'};
} else {
return {
'success': false,
'message': 'Server error (${response.statusCode})',
};
}
} catch (e) {
print('Login error: $e');
return {
'success': false,
'message': 'Network error. Please check your connection.',
};
}
}
static Future<String?> getToken() async {
return await _storage.read(key: _tokenKey);
}
static Future<bool> isLoggedIn() async {
final token = await getToken();
return token != null && token.isNotEmpty;
}
static Future<void> logout() async {
await _storage.delete(key: _tokenKey);
await _storage.delete(key: _userDataKey);
}
static Future<Map<String, String>> getAuthHeaders() async {
final token = await getToken();
return {
'Content-Type': 'application/json',
if (token != null) 'Authorization': 'Bearer $token',
};
}
static Future<void> saveUserData(Map<String, dynamic> userData) async {
await _storage.write(key: _userDataKey, value: jsonEncode(userData));
}
static Future<Map<String, dynamic>?> getCachedUserData() async {
final userData = await _storage.read(key: _userDataKey);
if (userData != null) {
return jsonDecode(userData);
}
return null;
}
static Future<void> clearUserData() async {
await _storage.delete(key: _userDataKey);
}
}

View File

@ -0,0 +1,40 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../constants/api_constants.dart';
import 'auth_service.dart';
class HttpService {
static Future<http.Response> get(String endpoint) async {
final headers = await AuthService.getAuthHeaders();
return await http.get(
Uri.parse('${ApiConstants.baseUrl}$endpoint'),
headers: headers,
);
}
static Future<http.Response> post(String endpoint, Map<String, dynamic> body) async {
final headers = await AuthService.getAuthHeaders();
return await http.post(
Uri.parse('${ApiConstants.baseUrl}$endpoint'),
headers: headers,
body: jsonEncode(body),
);
}
static Future<http.Response> put(String endpoint, Map<String, dynamic> body) async {
final headers = await AuthService.getAuthHeaders();
return await http.put(
Uri.parse('${ApiConstants.baseUrl}$endpoint'),
headers: headers,
body: jsonEncode(body),
);
}
static Future<http.Response> delete(String endpoint) async {
final headers = await AuthService.getAuthHeaders();
return await http.delete(
Uri.parse('${ApiConstants.baseUrl}$endpoint'),
headers: headers,
);
}
}

View File

@ -0,0 +1,81 @@
import 'dart:convert';
import '../constants/api_constants.dart';
import 'http_service.dart';
import 'auth_service.dart';
class UserService {
static Future<Map<String, dynamic>> getCurrentUser({
bool forceRefresh = false,
}) async {
if (!forceRefresh) {
final cachedData = await AuthService.getCachedUserData();
if (cachedData != null) {
return {'success': true, 'data': cachedData};
}
}
try {
final response = await HttpService.get(ApiConstants.getUserEndpoint);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
await AuthService.saveUserData(data);
return {'success': true, 'data': data};
} else if (response.statusCode == 401) {
return {
'success': false,
'message': 'Session expired. Please login again.',
};
} else if (response.statusCode == 403) {
return {
'success': false,
'message': 'Access denied. Invalid credentials.',
};
} else {
return {
'success': false,
'message': 'Server error (${response.statusCode})',
};
}
} catch (e) {
print('Error fetching user: $e');
return {
'success': false,
'message': 'Network error. Please check your connection.',
};
}
}
static Future<Map<String, dynamic>> updateFCMToken(String fcmToken) async {
try {
final response = await HttpService.post(ApiConstants.updateUserEndpoint, {
'fcmToken': fcmToken,
});
if (response.statusCode == 200) {
return {'success': true};
} else if (response.statusCode == 401) {
return {
'success': false,
'message': 'Session expired. Please login again.',
};
} else if (response.statusCode == 403) {
return {
'success': false,
'message': 'Access denied. Invalid credentials.',
};
} else {
return {
'success': false,
'message': 'Server error (${response.statusCode})',
};
}
} catch (e) {
print('Error updating FCM token: $e');
return {
'success': false,
'message': 'Network error. Please check your connection.',
};
}
}
}

View File

@ -6,6 +6,10 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) { void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);
} }

View File

@ -3,6 +3,7 @@
# #
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
flutter_secure_storage_linux
) )
list(APPEND FLUTTER_FFI_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST

View File

@ -7,8 +7,12 @@ import Foundation
import firebase_core import firebase_core
import firebase_messaging import firebase_messaging
import flutter_secure_storage_macos
import path_provider_foundation
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin")) FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin"))
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
} }

View File

@ -81,6 +81,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.3" version: "1.3.3"
ffi:
dependency: transitive
description:
name: ffi
sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
firebase_core: firebase_core:
dependency: "direct main" dependency: "direct main"
description: description:
@ -142,6 +150,54 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.0.0" version: "5.0.0"
flutter_secure_storage:
dependency: "direct main"
description:
name: flutter_secure_storage
sha256: "9cad52d75ebc511adfae3d447d5d13da15a55a92c9410e50f67335b6d21d16ea"
url: "https://pub.dev"
source: hosted
version: "9.2.4"
flutter_secure_storage_linux:
dependency: transitive
description:
name: flutter_secure_storage_linux
sha256: be76c1d24a97d0b98f8b54bce6b481a380a6590df992d0098f868ad54dc8f688
url: "https://pub.dev"
source: hosted
version: "1.2.3"
flutter_secure_storage_macos:
dependency: transitive
description:
name: flutter_secure_storage_macos
sha256: "6c0a2795a2d1de26ae202a0d78527d163f4acbb11cde4c75c670f3a0fc064247"
url: "https://pub.dev"
source: hosted
version: "3.1.3"
flutter_secure_storage_platform_interface:
dependency: transitive
description:
name: flutter_secure_storage_platform_interface
sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8
url: "https://pub.dev"
source: hosted
version: "1.1.2"
flutter_secure_storage_web:
dependency: transitive
description:
name: flutter_secure_storage_web
sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9
url: "https://pub.dev"
source: hosted
version: "1.2.1"
flutter_secure_storage_windows:
dependency: transitive
description:
name: flutter_secure_storage_windows
sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709
url: "https://pub.dev"
source: hosted
version: "3.1.2"
flutter_test: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
@ -184,6 +240,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.1.2" version: "4.1.2"
js:
dependency: transitive
description:
name: js
sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
url: "https://pub.dev"
source: hosted
version: "0.6.7"
leak_tracker: leak_tracker:
dependency: transitive dependency: transitive
description: description:
@ -248,6 +312,62 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.9.1" version: "1.9.1"
path_provider:
dependency: transitive
description:
name: path_provider
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
url: "https://pub.dev"
source: hosted
version: "2.1.5"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9
url: "https://pub.dev"
source: hosted
version: "2.2.17"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
url: "https://pub.dev"
source: hosted
version: "2.2.1"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
url: "https://pub.dev"
source: hosted
version: "2.3.0"
platform:
dependency: transitive
description:
name: platform
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
url: "https://pub.dev"
source: hosted
version: "3.1.6"
plugin_platform_interface: plugin_platform_interface:
dependency: transitive dependency: transitive
description: description:
@ -341,6 +461,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.1" version: "1.1.1"
win32:
dependency: transitive
description:
name: win32
sha256: "66814138c3562338d05613a6e368ed8cfb237ad6d64a9e9334be3f309acfca03"
url: "https://pub.dev"
source: hosted
version: "5.14.0"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
sdks: sdks:
dart: ">=3.8.1 <4.0.0" dart: ">=3.8.1 <4.0.0"
flutter: ">=3.22.0" flutter: ">=3.27.0"

View File

@ -14,6 +14,7 @@ dependencies:
firebase_messaging: ^15.2.9 firebase_messaging: ^15.2.9
http: ^1.1.0 http: ^1.1.0
googleapis_auth: ^1.6.0 googleapis_auth: ^1.6.0
flutter_secure_storage: ^9.2.2
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View File

@ -7,8 +7,11 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <firebase_core/firebase_core_plugin_c_api.h> #include <firebase_core/firebase_core_plugin_c_api.h>
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
void RegisterPlugins(flutter::PluginRegistry* registry) { void RegisterPlugins(flutter::PluginRegistry* registry) {
FirebaseCorePluginCApiRegisterWithRegistrar( FirebaseCorePluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FirebaseCorePluginCApi")); registry->GetRegistrarForPlugin("FirebaseCorePluginCApi"));
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
} }

View File

@ -4,6 +4,7 @@
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
firebase_core firebase_core
flutter_secure_storage_windows
) )
list(APPEND FLUTTER_FFI_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST