Merge pull request #1 from sBubshait/feature/login-page-authentication
Feature/login page authentication
This commit is contained in:
commit
969cbf1e8d
@ -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) {
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
10
frontend/lib/constants/api_constants.dart
Normal file
10
frontend/lib/constants/api_constants.dart
Normal 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';
|
||||||
|
}
|
||||||
@ -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,18 +323,45 @@ 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) {
|
||||||
@ -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,
|
||||||
|
|||||||
242
frontend/lib/screens/notification_permission_screen.dart
Normal file
242
frontend/lib/screens/notification_permission_screen.dart
Normal 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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 {
|
||||||
|
|||||||
@ -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),
|
||||||
|
|||||||
@ -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,11 +158,20 @@ class _ProfilePageState extends State<ProfilePage> {
|
|||||||
padding: EdgeInsets.all(24),
|
padding: EdgeInsets.all(24),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
|
if (isLoadingUser)
|
||||||
|
CircularProgressIndicator(
|
||||||
|
valueColor: AlwaysStoppedAnimation<Color>(
|
||||||
|
Color(0xFF6A4C93),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else if (userData != null) ...[
|
||||||
CircleAvatar(
|
CircleAvatar(
|
||||||
radius: 50,
|
radius: 50,
|
||||||
backgroundColor: Color(0xFF6A4C93),
|
backgroundColor: Color(0xFF6A4C93),
|
||||||
child: Text(
|
child: Text(
|
||||||
'A',
|
(userData!['displayName'] ?? 'U')
|
||||||
|
.substring(0, 1)
|
||||||
|
.toUpperCase(),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontSize: 36,
|
fontSize: 36,
|
||||||
@ -133,7 +181,7 @@ class _ProfilePageState extends State<ProfilePage> {
|
|||||||
),
|
),
|
||||||
SizedBox(height: 16),
|
SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
'Abu Norah',
|
userData!['displayName'] ?? 'Unknown User',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 24,
|
fontSize: 24,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
@ -142,10 +190,32 @@ class _ProfilePageState extends State<ProfilePage> {
|
|||||||
),
|
),
|
||||||
SizedBox(height: 4),
|
SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
'@yasser_hajri',
|
'@${userData!['username'] ?? 'unknown'}',
|
||||||
style: TextStyle(fontSize: 16, color: Colors.grey[600]),
|
style: TextStyle(fontSize: 16, color: Colors.grey[600]),
|
||||||
),
|
),
|
||||||
|
] else ...[
|
||||||
|
Icon(
|
||||||
|
Icons.error_outline,
|
||||||
|
size: 50,
|
||||||
|
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),
|
||||||
|
if (userData != null)
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
children: [
|
children: [
|
||||||
@ -192,6 +262,7 @@ class _ProfilePageState extends State<ProfilePage> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (userData != null) ...[
|
||||||
Container(
|
Container(
|
||||||
height: 1,
|
height: 1,
|
||||||
color: Colors.grey[200],
|
color: Colors.grey[200],
|
||||||
@ -229,7 +300,9 @@ class _ProfilePageState extends State<ProfilePage> {
|
|||||||
radius: 16,
|
radius: 16,
|
||||||
backgroundColor: Color(0xFF6A4C93),
|
backgroundColor: Color(0xFF6A4C93),
|
||||||
child: Text(
|
child: Text(
|
||||||
'J',
|
(userData!['displayName'] ?? 'U')
|
||||||
|
.substring(0, 1)
|
||||||
|
.toUpperCase(),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
@ -243,7 +316,8 @@ class _ProfilePageState extends State<ProfilePage> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Abu Norah',
|
userData!['displayName'] ??
|
||||||
|
'Unknown User',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
@ -313,6 +387,7 @@ class _ProfilePageState extends State<ProfilePage> {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
],
|
||||||
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),
|
||||||
|
|||||||
90
frontend/lib/services/auth_service.dart
Normal file
90
frontend/lib/services/auth_service.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
40
frontend/lib/services/http_service.dart
Normal file
40
frontend/lib/services/http_service.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
81
frontend/lib/services/user_service.dart
Normal file
81
frontend/lib/services/user_service.dart
Normal 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.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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"))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -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"));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user