WIP googleLoginBranch #1

Merged
beer3244 merged 5 commits from googleLoginBranch into main 2025-05-14 13:45:29 +02:00
25 changed files with 262 additions and 97 deletions

View File

@ -6,6 +6,7 @@ plugins {
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
id("org.jetbrains.kotlin.android") version "2.1.20"
}
android {

View File

@ -4,6 +4,9 @@ allprojects {
mavenCentral()
}
}
plugins {
id("org.jetbrains.kotlin.android") version "2.1.20" apply false
}
val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get()
rootProject.layout.buildDirectory.value(newBuildDir)
@ -18,4 +21,4 @@ subprojects {
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}
}

View File

@ -20,9 +20,12 @@ plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.7.0" apply false
// START: FlutterFire Configuration
id("com.google.gms.google-services") version("4.3.15") apply false
// END: FlutterFire Configuration
id("org.jetbrains.kotlin.android") version "1.8.22" apply false
// START: FlutterFire Configuration
id("com.google.gms.google-services") version("4.4.2") apply false
// END: FlutterFire Configuration
//id("org.jetbrains.kotlin.android") version "1.8.22" apply false
//id("org.jetbrains.kotlin.android") version "2.1.20" apply false
}
include(":app")

4
devtools_options.yaml Normal file
View File

@ -0,0 +1,4 @@
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:
- provider: true

View File

@ -1,5 +1,7 @@
import 'package:flutter/material.dart';
import '../services/googleAuthService.dart';
class ProfilePage extends StatelessWidget {
const ProfilePage({super.key});
@ -15,6 +17,15 @@ class ProfilePage extends StatelessWidget {
'Profil',
style: TextStyle(color: Colors.white),
),
actions: [
IconButton(
icon: const Icon(Icons.logout, color: Colors.white),
tooltip: 'Logga ut',
onPressed: () async {
await GoogleAuthService.logout(context);
},
),
],
),
body: SafeArea(
child: Center(

View File

@ -1,5 +1,7 @@
import 'package:flutter/material.dart';
import '../services/googleAuthService.dart';
class ProfilePageAdmin extends StatelessWidget {
const ProfilePageAdmin({super.key});
@ -15,6 +17,15 @@ class ProfilePageAdmin extends StatelessWidget {
'Profil',
style: TextStyle(color: Colors.white),
),
actions: [
IconButton(
icon: const Icon(Icons.logout, color: Colors.white),
tooltip: 'Logga ut',
onPressed: () async {
await GoogleAuthService.logout(context);
},
),
],
),
body: SafeArea(
child: Center(

View File

@ -6,6 +6,8 @@ import 'package:insparkspokalen_ui/models/activityModel.dart';
class AdminCalendarPage extends StatefulWidget {
const AdminCalendarPage({super.key});
@override
State<AdminCalendarPage> createState() => AdminCalendarPageState();
}
@ -36,8 +38,8 @@ class AdminCalendarPageState extends State<AdminCalendarPage> {
void _showDialogCalendar() {
print("Dialogen för kalendern öppnas!");
final _titleController = TextEditingController();
final _placeController = TextEditingController();
final titleController = TextEditingController();
final placeController = TextEditingController();
_slutTidController.clear();
_beskrivningController.clear();
valdDatumTid = DateTime.now();
@ -58,11 +60,11 @@ class AdminCalendarPageState extends State<AdminCalendarPage> {
),
const SizedBox(height: 16),
TextField(
controller: _titleController,
controller: titleController,
decoration: const InputDecoration(labelText: 'Titel'),
),
TextField(
controller: _placeController,
controller: placeController,
decoration: const InputDecoration(labelText: 'Plats'),
),
const SizedBox(height: 16),
@ -95,8 +97,8 @@ class AdminCalendarPageState extends State<AdminCalendarPage> {
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
final titel = _titleController.text;
final plats = _placeController.text;
final titel = titleController.text;
final plats = placeController.text;
final slutTid = _slutTidController.text.isNotEmpty ? _slutTidController.text : null;
final beskrivning = _beskrivningController.text.isNotEmpty ? _beskrivningController.text : null;

View File

@ -4,6 +4,8 @@ import 'package:insparkspokalen_ui/services/activityService.dart';
import 'package:insparkspokalen_ui/models/activityModel.dart';
class Calendar extends StatefulWidget {
const Calendar({super.key});
@override
State<Calendar> createState() => _CalendarState();
}

View File

@ -1,7 +1,6 @@
import 'package:flutter/material.dart';
import 'package:insparkspokalen_ui/models/postModel.dart';
import 'package:insparkspokalen_ui/models/teamModel.dart';
import 'package:insparkspokalen_ui/feed/adminHomeScreen/main_page_for_admin.dart';
class AdminInfoBlock extends StatefulWidget {
final TeamModel group;

View File

@ -1,8 +1,6 @@
import 'package:flutter/material.dart';
import 'package:insparkspokalen_ui/services/teamService.dart';
import 'package:provider/provider.dart';
import 'dart:io';
import 'package:image_picker/image_picker.dart';
import 'package:insparkspokalen_ui/models/teamModel.dart';
import 'package:insparkspokalen_ui/models/postModel.dart';
import 'package:insparkspokalen_ui/feed/adminHomeScreen/admin_info_block.dart';

View File

@ -1,6 +1,5 @@
import 'package:flutter/material.dart';
import 'package:insparkspokalen_ui/models/teamModel.dart';
import 'package:insparkspokalen_ui/feed/usersHomeScreen/info_smallpage.dart';
//Fixa denna är rätt, kommer ej vara main
class InfoSmallpage extends StatefulWidget {

View File

@ -12,6 +12,7 @@ class BaseScaffold extends StatelessWidget {
final Widget body;
final String role; // USER el ADMIN
const BaseScaffold({
super.key,
required this.title,
@ -19,6 +20,7 @@ class BaseScaffold extends StatelessWidget {
required this.onTap,
required this.body,
required this.role, //USER el ADMIN
});
@override

View File

@ -3,7 +3,6 @@ import 'package:insparkspokalen_ui/layout/baseScaffold.dart';
import 'package:insparkspokalen_ui/calendar/calendar.dart';
import 'package:insparkspokalen_ui/leaderboard/leaderboard.dart';
import 'package:insparkspokalen_ui/teams/teamPage.dart';
import 'package:insparkspokalen_ui/feed/feedPage.dart';
import 'package:insparkspokalen_ui/feed/usersHomeScreen/main_page_for_info.dart';
class UserHomePage extends StatefulWidget {

View File

@ -5,7 +5,7 @@ import 'package:insparkspokalen_ui/models/teamModel.dart';
import 'package:insparkspokalen_ui/services/teamService.dart';
class Leaderboard extends StatefulWidget {
const Leaderboard({Key? key}) : super(key: key);
const Leaderboard({super.key});
@override
State<Leaderboard> createState() => _LeaderboardState();
@ -114,7 +114,7 @@ class _TopplistaBody extends StatelessWidget {
group: entry.value,
place: entry.key + 1,
))
.toList(),
,
const SizedBox(height: 16),
],
);
@ -125,7 +125,7 @@ class GruppKort extends StatelessWidget {
final TeamModel group;
final int place;
const GruppKort.GroupCard({required this.group, required this.place});
const GruppKort.GroupCard({super.key, required this.group, required this.place});
@override
Widget build(BuildContext context) {

View File

@ -3,9 +3,10 @@ import 'package:provider/provider.dart';
import 'package:flutter/material.dart';
import 'package:insparkspokalen_ui/models/teamModel.dart';
import 'package:insparkspokalen_ui/services/teamService.dart';
import 'package:insparkspokalen_ui/services/googleAuthService.dart';
class LeaderboardAdmin extends StatefulWidget {
const LeaderboardAdmin({Key? key}) : super(key: key);
const LeaderboardAdmin({super.key});
@override
State<LeaderboardAdmin> createState() => _LeaderboardState();
@ -112,7 +113,7 @@ class _TopplistaBody extends StatelessWidget {
group: entry.value,
place: entry.key + 1,
))
.toList(),
,
const SizedBox(height: 16),
],
);
@ -123,7 +124,7 @@ class GruppKort extends StatefulWidget {
final TeamModel group;
final int place;
const GruppKort.GroupCard({required this.group, required this.place});
const GruppKort.GroupCard({super.key, required this.group, required this.place});
@override
State<GruppKort> createState() => _GruppKortState();
@ -226,13 +227,25 @@ Future<void> _updateScore(int scoreChange) async {
IconButton(
icon: const Icon(Icons.add, color: Color(0xFFDAA520)),
onPressed: () => _updateScore(1),
onPressed: () async {
if (!GoogleAuthService.isLoggedIn()) {
await GoogleAuthService.signInWithGoogle(context);
if (!GoogleAuthService.isLoggedIn()) return; // user cancelled or login failed
}
_updateScore(1);
},
),
const SizedBox(width: 8),
IconButton(
icon: const Icon(Icons.remove, color: Color(0xFFDAA520)),
onPressed: () => _updateScore(-1),
onPressed: () async {
if (!GoogleAuthService.isLoggedIn()) {
await GoogleAuthService.signInWithGoogle(context);
if (!GoogleAuthService.isLoggedIn()) return; // user cancelled or login failed
}
_updateScore(1);
},
),
],
),

View File

@ -1,8 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:insparkspokalen_ui/login/loginPage.dart';
import 'package:insparkspokalen_ui/services/authService.dart';
import 'package:insparkspokalen_ui/layout/adminHomePage.dart';
import 'package:insparkspokalen_ui/services/googleAuthService.dart';
import 'package:insparkspokalen_ui/layout/userHomePage.dart';
import 'package:sign_button/sign_button.dart';
class StartPage extends StatelessWidget {
const StartPage({super.key});
@ -16,14 +15,13 @@ class StartPage extends StatelessWidget {
'Insparkspokalen',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Color(0xFFFEDF00),
color: Colors.yellow,
fontSize: 40,
),
),
centerTitle: true,
backgroundColor: const Color.fromARGB(255, 42, 41, 41),
elevation: 0,
iconTheme: const IconThemeData(color: Colors.white),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(20),
@ -37,72 +35,36 @@ class StartPage extends StatelessWidget {
height: 250,
),
),
const SizedBox(height: 20),
// Logga in med Google
SizedBox(
width: 250,
child: ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: Colors.black,
minimumSize: const Size (250, 45),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
icon: SvgPicture.asset(
'assets/icons/googleLogo.svg',
height: 24,
),
label: const Text('Logga in med Google'),
onPressed: () async {
try {
await AuthService().signInWithGoogle();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Inloggning lyckades!')),
);
Navigator.pushReplacement(
const SizedBox(height: 60),
const SizedBox(height: 30),
SignInButton(
buttonType: ButtonType.google,
buttonSize: ButtonSize.large,
onPressed: () => GoogleAuthService.signInWithGoogle(context),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const AdminHomePage()),
MaterialPageRoute(builder: (context) => const UserHomePage()),
);
} catch (_) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Inloggning misslyckades.')),
);
}
},
),
),
const SizedBox(height: 20),
// Logga in som gäst
SizedBox(
width: 250,
child: ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const LogInPage(),
),
);
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFFEDF00),
foregroundColor: Colors.black,
minimumSize: const Size (250, 45),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.yellow,
foregroundColor: const Color.fromARGB(255, 42, 41, 41),
padding: const EdgeInsets.symmetric(horizontal: 34, vertical: 22),
),
child: const Text('Continue as guest'),
),
child: const Text('Fortsätt som gäst'),
),
],
),
],
),
),
);
}
}
}

View File

@ -1,3 +1,4 @@
import 'package:flutter/cupertino.dart';
import 'package:json_annotation/json_annotation.dart';
part 'userModel.g.dart';
@ -6,9 +7,9 @@ part 'userModel.g.dart';
class UserModel {
final String email;
final String name;
final String role; //USER or ADMIN
UserModel(this.email, this.name, this.role);
final String profilePicture; // URL string
// final String role;
UserModel(this.email, this.name, this.profilePicture);
factory UserModel.fromJson(Map<String, dynamic> json) =>
_$UserModelFromJson(json);

View File

@ -9,11 +9,11 @@ part of 'userModel.dart';
UserModel _$UserModelFromJson(Map<String, dynamic> json) => UserModel(
json['email'] as String,
json['name'] as String,
json['role'] as String,
json['profilePicture'] as String,
);
Map<String, dynamic> _$UserModelToJson(UserModel instance) => <String, dynamic>{
'email': instance.email,
'name': instance.name,
'role': instance.role,
'profilePicture': instance.profilePicture,
};

View File

@ -1,5 +1,6 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:insparkspokalen_ui/services/googleAuthService.dart';
import 'package:insparkspokalen_ui/models/activityModel.dart';
class ActivityService {
@ -8,6 +9,7 @@ class ActivityService {
// Hämtar alla aktiviteter
Future<List<ActivityModel>> fetchActivities() async {
try {
final response = await http.get(Uri.parse('$baseUrl/all'));
@ -33,6 +35,8 @@ class ActivityService {
// Skapa en ny aktivitet med post
Future<void> createActivity(ActivityModel activity) async {
try {
final response = await http.post(
Uri.parse('$baseUrl/add'),
@ -51,4 +55,6 @@ class ActivityService {
}
//Ta bort en aktivitet
}

View File

@ -0,0 +1,79 @@
import 'package:flutter/material.dart';
import 'package:google_sign_in/google_sign_in.dart';
import 'package:insparkspokalen_ui/login/startPage.dart';
// import 'package:shared_preferences/shared_preferences.dart';
import '../layout/userHomePage.dart';
import 'userService.dart';
/*
För att kräva login gör såhär:
Lägg till detta paket i klassen ->
import 'package:insparkspokalen_ui/services/googleAuthService.dart';
Sedan lägg till följande för alla knappar som kräver att användaren är inloggad ->
onPressed:() async {
final isAuthenticated = await GoogleAuthService.ensureLoggedIn(context);
if (!isAuthenticated) return;
},
Kör appen med detta kommando vår konsol endast släpper in redirects från port 8080 via webben för tillfället
flutter run -d chrome --web-port=8080
*/
class GoogleAuthService {
static final GoogleSignIn _googleSignIn = GoogleSignIn(
clientId: '278095802777-40qt28sou07o3qb58pcoccl0eolas3k0.apps.googleusercontent.com', //web
// clientId: '278095802777-8tj05ph2nbb8117abl1k4nlnq4ukd7ao.apps.googleusercontent.com', //android
scopes: <String>['email', 'profile'],
);
// todo review this after final login solution is done
static String? accessToken;
static Future<void> signInWithGoogle(BuildContext context) async {
try {
await _googleSignIn.signOut();
final account = await _googleSignIn.signIn();
if (account == null) return;
final auth = await account.authentication;
accessToken = auth.accessToken;
if (accessToken == null) {
print("Missing ID token.");
return;
}
// Create user model
await UserService.createOrFetchUser(account);
print("Signed in as ${account.email}, ID Token: $accessToken, name: ${account.displayName}");
//Continue to app home
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const UserHomePage()));
} catch (e) {
print("Google Sign-In failed: $e");
}
}
static Future<void> logout(BuildContext context) async {
await _googleSignIn.signOut();
// Clear cache when user logs out removed for now something went wrong with loading the cache
// final prefs = await SharedPreferences.getInstance();
// await prefs.remove('user');
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (_) => const StartPage()), // Import StartPage
(route) => false,
);
accessToken = null;
}
static Future<bool> ensureLoggedIn(BuildContext context) async {
if (!isLoggedIn()) {
await signInWithGoogle(context);
}
return isLoggedIn();
}
static bool isLoggedIn() => accessToken != null;
}

View File

@ -37,9 +37,9 @@ class TeamService {
//Ta bort grupp
Future<bool> removeTeam(int teamId, String teamName) async {
final adminToken = 'secret123'; // Admin-token, hårdkodad i backend
final url = Uri.parse(
'$baseUrl/remove/team/$teamId?adminToken=${Uri.encodeQueryComponent(adminToken)}',
'$baseUrl/remove/team/$teamId',
);
final response = await http.get(url);
@ -50,9 +50,14 @@ class TeamService {
// Funktion för att lägga till poäng
Future<bool> updateScore(int teamId, int scoreChange) async {
final url = Uri.parse('$baseUrl/change/$teamId/$scoreChange');
final response = await http.get(url);
return response.statusCode == 200;
}
}

View File

@ -1,13 +1,15 @@
import 'package:google_sign_in/google_sign_in.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'package:insparkspokalen_ui/models/userModel.dart';
// import 'package:shared_preferences/shared_preferences.dart';
class UserService {
final String baseUrl = 'https://group-10-15.pvt.dsv.su.se';
//final String baseUrl = 'http://localhost:8080';
Future<List<UserModel>> showUsers() async {
final endpoint = Uri.parse('$baseUrl/all/');
final endpoint = Uri.parse('$baseUrl/user/');
//final endpoint = Uri.parse('$baseUrl/all');
try {
@ -22,4 +24,47 @@ class UserService {
return [];
}
}
static Future<void> createOrFetchUser(GoogleSignInAccount account) async {
final service = UserService();
final email = account.email;
try {
final getUserEndpoint = Uri.parse('${service.baseUrl}/user/getuser?email=$email');
final getUserResponse = await http.get(getUserEndpoint);
UserModel user;
if (getUserResponse.statusCode == 200 && getUserResponse.body.isNotEmpty) {
user = UserModel.fromJson(jsonDecode(getUserResponse.body));
print("User fetched: ${user.toJson()}");
} else {
user = UserModel(
email,
account.displayName ?? 'Unknown',
account.photoUrl ?? '',
);
final createUserEndpoint = Uri.parse('${service.baseUrl}/user/createuser');
final createUserResponse = await http.post(
createUserEndpoint,
headers: {'Content-Type': 'application/json'},
body: jsonEncode(user.toJson()),
);
if (!(createUserResponse.statusCode == 200 || createUserResponse.statusCode == 201)) {
throw Exception("Failed to create user");
}
print("User created: ${user.toJson()}");
}
// // Cache user
// final prefs = await SharedPreferences.getInstance();
// prefs.setString('user', jsonEncode(user.toJson()));
} catch (e) {
print("UserService error: $e");
}
}
}

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:insparkspokalen_ui/models/teamModel.dart';
import 'package:insparkspokalen_ui/services/googleAuthService.dart';
class TeamCard extends StatelessWidget {
final TeamModel group;
@ -31,7 +32,11 @@ class TeamCard extends StatelessWidget {
],
),
trailing: ElevatedButton(
onPressed: onJoin,
onPressed:() async {
final isAuthenticated = await GoogleAuthService.ensureLoggedIn(context);
if (!isAuthenticated) return;
onJoin();
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.grey[300],
foregroundColor: Colors.black,

View File

@ -4,6 +4,7 @@ import 'createTeamsButton.dart';
import 'showTeams.dart';
import 'package:insparkspokalen_ui/services/teamService.dart';
import 'package:provider/provider.dart';
import 'package:insparkspokalen_ui/services/googleAuthService.dart';
class TeamPage extends StatefulWidget {
const TeamPage({super.key});
@ -77,7 +78,12 @@ class TeamPageState extends State<TeamPage> {
),
),
ElevatedButton(
onPressed: _openDialog,
onPressed: () async {
final isAuthenticated = await GoogleAuthService.ensureLoggedIn(context);
if (!isAuthenticated) return;
_openDialog(); // or your next step
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.grey,
shape: RoundedRectangleBorder(

View File

@ -44,7 +44,16 @@ dependencies:
firebase_storage: ^12.4.5
uuid: ^4.5.1
# Required for signin with google
google_sign_in: ^6.3.0
# Required for the google button on startPage.dart
sign_button: ^2.0.6
# cache test
# shared_preferences: ^2.2.2
# google_sign_in_all_platforms: ^1.0.0
url_launcher: ^6.0.20
dev_dependencies:
flutter_test: