feat: Add biometric permission and update activity to FlutterFragmentActivity; refactor bindings and remove unused repositories

This commit is contained in:
vergiLgood1 2025-05-17 07:50:58 +07:00
parent 803a28494d
commit 8d67e7bbb3
17 changed files with 78 additions and 596 deletions

View File

@ -5,6 +5,8 @@
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<!-- Biometric permission -->
<uses-permission android:name="android.permission.USE_BIOMETRIC"/>
<!-- ... -->
<application android:label="sigap"
android:name="${applicationName}"

View File

@ -1,5 +1,5 @@
package com.backspacex.sigap
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.android.FlutterFragmentActivity
class MainActivity : FlutterActivity()
class MainActivity: FlutterFragmentActivity()

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<style name="LaunchTheme" parent="@android:style/Theme.AppCompat.DayNight">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
@ -12,7 +12,7 @@
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<style name="NormalTheme" parent="@android:style/Theme.AppCompat.DayNight">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:get/get_navigation/src/root/get_material_app.dart';
import 'package:sigap/src/cores/bindings/general_bindings.dart';
import 'package:sigap/src/cores/bindings/app_bindings.dart';
import 'package:sigap/src/cores/routes/app_pages.dart';
import 'package:sigap/src/utils/constants/colors.dart';
import 'package:sigap/src/utils/constants/text_strings.dart';
@ -18,7 +18,7 @@ class App extends StatelessWidget {
theme: TAppTheme.lightTheme,
darkTheme: TAppTheme.darkTheme,
debugShowCheckedModeBanner: false,
initialBinding: GeneralBindings(),
initialBinding: AppBindings(),
localizationsDelegates: GlobalMaterialLocalizations.delegates,
supportedLocales: const [Locale('id', '')],
getPages: AppPages.routes,

View File

@ -1,10 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_native_splash/flutter_native_splash.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:sigap/app.dart';
import 'package:sigap/src/cores/repositories/panic-button/panic_button_repository.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
Future<void> main() async {
@ -32,9 +30,5 @@ Future<void> main() async {
storageOptions: const StorageClientOptions(retryAttempts: 10),
);
// Initialize repositories
Get.put(PanicButtonRepository());
await Get.find<PanicButtonRepository>().init();
runApp(const App());
}

View File

@ -0,0 +1,22 @@
import 'package:get/get.dart';
import 'package:sigap/src/cores/bindings/controller_bindings.dart';
import 'package:sigap/src/cores/bindings/general_bindings.dart';
import 'package:sigap/src/cores/bindings/repository_bindings.dart';
import 'package:sigap/src/cores/bindings/service_bindings.dart';
class AppBindings implements Bindings {
@override
Future<void> dependencies() async {
// Register general helpers and utilities
UtilityBindings().dependencies();
// Register all services
await ServiceBindings().dependencies();
// Register all repositories
RepositoryBindings().dependencies();
// Register all feature controllers
ControllerBindings().dependencies();
}
}

View File

@ -1,45 +1,17 @@
import 'package:get/get.dart';
import 'package:sigap/src/features/auth/controllers/forgot_password_controller.dart';
import 'package:sigap/src/features/auth/controllers/signin_controller.dart';
import 'package:sigap/src/features/auth/controllers/signup_controller.dart';
import 'package:sigap/src/features/onboarding/controllers/onboarding_controller.dart';
import 'package:sigap/src/features/onboarding/controllers/role_selection_controller.dart';
import 'package:sigap/src/features/auth/bindings/auth_bindings.dart';
import 'package:sigap/src/features/onboarding/bindings/onboarding_binding.dart';
// Onboarding controller bindings
class OnboardingControllerBinding extends Bindings {
class ControllerBindings extends Bindings {
@override
void dependencies() {
Get.lazyPut<OnboardingController>(() => OnboardingController());
// Register all controllers here
// Onboarding Bindings
OnboardingBindings().dependencies();
// Auth Bindings
AuthBindings().dependencies();
}
}
class RoleSelectionControllerBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<RoleSelectionController>(() => RoleSelectionController());
}
}
// Auth controller bindings
class SignInControllerBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<SignInController>(() => SignInController());
}
}
class SignUpControllerBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<SignUpController>(() => SignUpController());
}
}
class ForgotPasswordControllerBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<ForgotPasswordController>(() => ForgotPasswordController());
}
}
// Main controller bindings

View File

@ -2,7 +2,7 @@ import 'package:get/get.dart';
import 'package:logger/logger.dart';
import 'package:sigap/src/utils/helpers/network_manager.dart';
class GeneralBindings extends Bindings {
class UtilityBindings extends Bindings {
Logger? get logger => Logger();
@override

View File

@ -9,25 +9,14 @@ import 'package:sigap/src/cores/repositories/personalization/users_repository.da
class RepositoryBindings extends Bindings {
@override
void dependencies() {
// Auth Repository
Get.lazyPut<AuthenticationRepository>(
() => AuthenticationRepository(),
fenix: true,
);
// User Repository
Get.lazyPut<UserRepository>(() => UserRepository(), fenix: true);
// Officer Repository
Get.lazyPut<OfficerRepository>(() => OfficerRepository(), fenix: true);
// Unit Repository
Get.lazyPut<UnitRepository>(() => UnitRepository(), fenix: true);
// Profile Repository
Get.lazyPut<ProfileRepository>(() => ProfileRepository(), fenix: true);
// Role Repository
Get.lazyPut<RolesRepository>(() => RolesRepository(), fenix: true);
}
}

View File

@ -1,504 +0,0 @@
import 'package:flutter/services.dart';
import 'package:flutter_native_splash/flutter_native_splash.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:sigap/src/cores/services/biometric_service.dart';
import 'package:sigap/src/cores/services/location_service.dart';
import 'package:sigap/src/cores/services/supabase_service.dart';
import 'package:sigap/src/features/auth/screens/signin/signin_screen.dart';
import 'package:sigap/src/utils/constants/app_routes.dart';
import 'package:sigap/src/utils/exceptions/exceptions.dart';
import 'package:sigap/src/utils/exceptions/format_exceptions.dart';
import 'package:sigap/src/utils/exceptions/platform_exceptions.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
class AuthenticationRepository extends GetxController {
static AuthenticationRepository get instance => Get.find();
// Variable
final storage = GetStorage();
final _supabase = SupabaseService.instance.client;
final _locationService = LocationService.instance;
final _biometricService = Get.put(BiometricService());
// Getters that use the Supabase service
User? get authUser => SupabaseService.instance.currentUser;
String? get currentUserId => SupabaseService.instance.currentUserId;
@override
void onReady() {
FlutterNativeSplash.remove();
screenRedirect();
}
// Check for biometric login on app start
Future<bool> attemptBiometricLogin() async {
if (!await _biometricService.isBiometricLoginEnabled()) {
return false;
}
String? refreshToken = await _biometricService.attemptBiometricLogin();
if (refreshToken == null || refreshToken.isEmpty) {
return false;
}
try {
// Use the refresh token to recover the session
final response = await _supabase.auth.refreshSession(refreshToken);
if (response.session != null) {
Get.offAllNamed(AppRoutes.explore);
return true;
}
return false;
} catch (e) {
// If refresh token is invalid or expired, disable biometric login
await _biometricService.disableBiometricLogin();
return false;
}
}
// Check if the onboarding process is complete
Future<bool> isOnboardingComplete() async {
return storage.read('ONBOARDING_COMPLETED') ?? false;
}
// Screen redirect based on auth status
void screenRedirect() async {
final user = _supabase.auth.currentUser;
if (user != null) {
// User is authenticated
Get.offAllNamed(AppRoutes.panicButton);
} else {
// Check if onboarding is completed
final onboardingCompleted = await isOnboardingComplete();
if (onboardingCompleted) {
// Check location validity
final isLocationValid =
await _locationService.isLocationValidForFeature();
if (isLocationValid) {
// Go to role selection
Get.offAllNamed(AppRoutes.roleSelection);
} else {
// Show location warning
Get.offAllNamed(AppRoutes.locationWarning);
}
} else {
// Show onboarding
Get.offAllNamed(AppRoutes.onboarding);
}
}
}
// ----------------- Email and Password Sign In -----------------
Future<AuthResponse> signInWithEmailPassword({
required String email,
required String password,
}) async {
try {
// Check if the user is banned
final bannedUser = await _supabase.rpc(
'check_if_banned',
params: {'user_email': email},
);
if (bannedUser != null && bannedUser == true) {
throw TExceptions(
'This account has been banned due to violation of terms.',
);
}
final response = await _supabase.auth.signInWithPassword(
email: email,
password: password,
);
// Setup biometric login if available
if (_biometricService.isBiometricAvailable.value) {
// Ask user if they want to enable biometric login
// This would typically be done in the UI, but setting up the flow here
await _biometricService.enableBiometricLogin();
}
return response;
} on AuthException catch (e) {
throw TExceptions(e.message);
} on FormatException catch (_) {
throw const TFormatException();
} on PlatformException catch (e) {
throw TPlatformException(e.code).message;
} on PostgrestException catch (error) {
throw TExceptions.fromCode(error.code ?? 'unknown_error');
} catch (e) {
throw TExceptions('Something went wrong. Please try again later.');
}
}
// [SESSION] - CHECK SESSION
Future<Map<String, dynamic>?> getSession() async {
try {
final session = _supabase.auth.currentSession;
if (session != null) {
return {'user': session.user, 'session': session};
}
return null;
} on PostgrestException catch (error) {
throw TExceptions.fromCode(error.code ?? 'unknown_error');
} catch (e) {
throw TExceptions('Something went wrong. Please try again later.');
}
}
// [Email Verification] - EMAIL VERIFICATION
Future<void> sendEmailVerification() async {
try {
await _supabase.auth.resend(
type: OtpType.signup,
email: _supabase.auth.currentUser!.email,
);
} on AuthException catch (e) {
throw TExceptions(e.message);
} on FormatException catch (_) {
throw const TFormatException();
} on PlatformException catch (e) {
throw TPlatformException(e.code).message;
} on PostgrestException catch (error) {
throw TExceptions.fromCode(error.code ?? 'unknown_error');
} catch (e) {
throw TExceptions('Something went wrong. Please try again later.');
}
}
// [Email Reset Password ] - RESET PASSWORD
Future<void> sendResetPasswordForEmail(String email) async {
try {
await _supabase.auth.resetPasswordForEmail(email);
} on AuthException catch (e) {
throw TExceptions(e.message);
} on FormatException catch (_) {
throw const TFormatException();
} on PlatformException catch (e) {
throw TPlatformException(e.code).message;
} on PostgrestException catch (error) {
throw TExceptions.fromCode(error.code ?? 'unknown_error');
} catch (e) {
throw TExceptions('Something went wrong. Please try again later.');
}
}
// Compare OTP
Future<AuthResponse> verifyOtp(String otp) async {
try {
final AuthResponse res = await _supabase.auth.verifyOTP(
type: OtpType.signup,
token: otp,
);
final Session? session = res.session;
final User? user = res.user;
if (session == null && user == null) {
throw TExceptions('Failed to verify OTP. Please try again.');
}
return res;
} on AuthException catch (e) {
throw TExceptions(e.message);
} on FormatException catch (_) {
throw const TFormatException();
} on PlatformException catch (e) {
throw TPlatformException(e.code).message;
} on PostgrestException catch (error) {
throw TExceptions.fromCode(error.code ?? 'unknown_error');
} catch (e) {
throw TExceptions('Something went wrong. Please try again later.');
}
}
// Update password after reset
Future<UserResponse> updatePasswordAfterReset(String newPassword) async {
try {
final response = await _supabase.auth.updateUser(
UserAttributes(password: newPassword),
);
return response;
} on AuthException catch (e) {
throw TExceptions(e.message);
} on FormatException catch (_) {
throw const TFormatException();
} on PlatformException catch (e) {
throw TPlatformException(e.code).message;
} on PostgrestException catch (error) {
throw TExceptions.fromCode(error.code ?? 'unknown_error');
} catch (e) {
throw TExceptions('Something went wrong. Please try again later.');
}
}
Future<String> changePassword(
String currentPassword,
String newPassword,
String email,
) async {
try {
// Reauthenticate user
await _supabase.auth.reauthenticate();
// Update password
await _supabase.auth.updateUser(UserAttributes(password: newPassword));
// Password changed successfully
return 'Password changed successfully.';
} on AuthException catch (e) {
throw TExceptions(e.message);
} on FormatException catch (_) {
throw const TFormatException();
} on PlatformException catch (e) {
throw TPlatformException(e.code).message;
} on PostgrestException catch (error) {
throw TExceptions.fromCode(error.code ?? 'unknown_error');
} catch (e) {
throw TExceptions('Something went wrong. Please try again later.');
}
}
// [Email Verification] - CREATE NEW VERIFICATION USER EMAIL
Future<void> resendEmailVerification(String email) async {
try {
await _supabase.auth.resend(type: OtpType.signup, email: email);
} on AuthException catch (e) {
throw TExceptions(e.message);
} on FormatException catch (_) {
throw const TFormatException();
} on PlatformException catch (e) {
throw TPlatformException(e.code).message;
} on PostgrestException catch (error) {
throw TExceptions.fromCode(error.code ?? 'unknown_error');
} catch (e) {
throw TExceptions('Something went wrong. Please try again later.');
}
}
// ----------------- Social Sign In -----------------
// [GoogleAuthentication] - GOOGLE
Future<bool> signInWithGoogle() async {
try {
return await _supabase.auth.signInWithOAuth(
OAuthProvider.google,
authScreenLaunchMode: LaunchMode.inAppWebView,
);
} on AuthException catch (e) {
throw TExceptions(e.message);
} on FormatException catch (_) {
throw const TFormatException();
} on PlatformException catch (e) {
throw TPlatformException(e.code).message;
} on PostgrestException catch (error) {
throw TExceptions.fromCode(error.code ?? 'unknown_error');
} catch (e) {
throw TExceptions('Something went wrong. Please try again later.');
}
}
// [FacebookAuthentication] - FACEBOOK
Future<bool> signInWithFacebook() async {
try {
return await _supabase.auth.signInWithOAuth(
OAuthProvider.facebook,
authScreenLaunchMode: LaunchMode.inAppWebView,
);
} on AuthException catch (e) {
throw TExceptions(e.message);
} on FormatException catch (_) {
throw const TFormatException();
} on PlatformException catch (e) {
throw TPlatformException(e.code).message;
} on PostgrestException catch (error) {
throw TExceptions.fromCode(error.code ?? 'unknown_error');
} catch (e) {
throw TExceptions('Something went wrong. Please try again later.');
}
}
// [AppleAuthentication] - APPLE
Future<bool> signInWithApple() async {
try {
return await _supabase.auth.signInWithOAuth(
OAuthProvider.apple,
authScreenLaunchMode: LaunchMode.inAppWebView,
);
} on AuthException catch (e) {
throw TExceptions(e.message);
} on FormatException catch (_) {
throw const TFormatException();
} on PlatformException catch (e) {
throw TPlatformException(e.code).message;
} on PostgrestException catch (error) {
throw TExceptions.fromCode(error.code ?? 'unknown_error');
} catch (e) {
throw TExceptions('Something went wrong. Please try again later.');
}
}
// [GithubAuthentication] - GITHUB
Future<bool> signInWithGithub() async {
try {
return await _supabase.auth.signInWithOAuth(
OAuthProvider.github,
authScreenLaunchMode: LaunchMode.inAppWebView,
);
} on AuthException catch (e) {
throw TExceptions(e.message);
} on FormatException catch (_) {
throw const TFormatException();
} on PlatformException catch (e) {
throw TPlatformException(e.code).message;
} on PostgrestException catch (error) {
throw TExceptions.fromCode(error.code ?? 'unknown_error');
} catch (e) {
throw TExceptions('Something went wrong. Please try again later.');
}
}
// [TwitterAuthentication] - TWITTER
Future<bool> signInWithTwitter() async {
try {
return await _supabase.auth.signInWithOAuth(
OAuthProvider.twitter,
authScreenLaunchMode: LaunchMode.inAppWebView,
);
} on AuthException catch (e) {
throw TExceptions(e.message);
} on FormatException catch (_) {
throw const TFormatException();
} on PlatformException catch (e) {
throw TPlatformException(e.code).message;
} on PostgrestException catch (error) {
throw TExceptions.fromCode(error.code ?? 'unknown_error');
} catch (e) {
throw TExceptions('Something went wrong. Please try again later.');
}
}
// [Email AUTH] - SIGN UP
Future<AuthResponse> signUpWithEmailPassword({
required String email,
required String password,
Map<String, dynamic>? userMetadata,
}) async {
try {
// Validate location for registration
bool isLocationValid = await _locationService.isLocationValidForFeature();
if (!isLocationValid) {
throw TExceptions('Registration is only available within Jember area. Please ensure your location services are enabled and you are not using a mock location app.');
}
// Create user with email and password
final response = await _supabase.auth.signUp(
email: email,
password: password,
data: userMetadata,
);
return response;
} on AuthException catch (e) {
throw TExceptions(e.message);
} on FormatException catch (_) {
throw const TFormatException();
} on PlatformException catch (e) {
throw TPlatformException(e.code).message;
} catch (e) {
if (e is TExceptions) rethrow;
throw TExceptions('Something went wrong. Please try again later.');
}
}
// Enable or disable biometric login
Future<void> toggleBiometricLogin(bool enable) async {
if (enable) {
await _biometricService.enableBiometricLogin();
} else {
await _biometricService.disableBiometricLogin();
}
}
// Check if biometric login is enabled
Future<bool> isBiometricLoginEnabled() async {
return await _biometricService.isBiometricLoginEnabled();
}
// Check if biometrics are available on the device
Future<bool> isBiometricAvailable() async {
return _biometricService.isBiometricAvailable.value;
}
// ----------------- Logout -----------------
// [Sign Out] - SIGN OUT
Future<void> signOut() async {
try {
await _supabase.auth.signOut();
Get.offAll(() => const SignInScreen());
} on AuthException catch (e) {
throw TExceptions(e.message);
} on FormatException catch (_) {
throw const TFormatException();
} on PlatformException catch (e) {
throw TPlatformException(e.code).message;
} on PostgrestException catch (error) {
throw TExceptions.fromCode(error.code ?? 'unknown_error');
} catch (e) {
throw TExceptions('Something went wrong. Please try again later.');
}
}
// ----------------- Delete Account -----------------
Future<void> deleteAccount() async {
try {
final userId = _supabase.auth.currentUser!.id;
await _supabase.rpc('delete_account', params: {'user_id': userId});
} on AuthException catch (e) {
throw TExceptions(e.message);
} on FormatException catch (_) {
throw const TFormatException();
} on PlatformException catch (e) {
throw TPlatformException(e.code).message;
} on PostgrestException catch (error) {
throw TExceptions.fromCode(error.code ?? 'unknown_error');
} catch (e) {
throw TExceptions('Something went wrong. Please try again later.');
}
}
/// Updates a user's profile with the officer status and metadata
Future<UserResponse> updateUserRole({
required bool isOfficer,
Map<String, dynamic>? officerData,
}) async {
try {
// Prepare metadata with the officer flag
final userMetadata = <String, dynamic>{'is_officer': isOfficer};
// Add officer data if provided
if (isOfficer && officerData != null) {
userMetadata['officer_data'] = officerData;
}
// Update the user metadata which will trigger the role change function
final response = await _supabase.auth.updateUser(
UserAttributes(data: userMetadata),
);
return response;
} on AuthException catch (e) {
throw TExceptions(e.message);
} on FormatException catch (_) {
throw const TFormatException();
} on PlatformException catch (e) {
throw TPlatformException(e.code).message;
} on PostgrestException catch (error) {
throw TExceptions.fromCode(error.code ?? 'unknown_error');
} catch (e) {
throw TExceptions('Something went wrong. Please try again later.');
}
}
}

View File

@ -1,5 +1,4 @@
import 'package:get/get.dart';
import 'package:sigap/src/cores/bindings/controller_bindings.dart';
import 'package:sigap/src/features/auth/screens/forgot-password/forgot_password.dart';
import 'package:sigap/src/features/auth/screens/signin/signin_screen.dart';
import 'package:sigap/src/features/auth/screens/signup/signup_screen.dart';
@ -12,46 +11,21 @@ class AppPages {
static final routes = [
// Onboarding
GetPage(
name: AppRoutes.onboarding,
page: () => const OnboardingScreen(),
binding: OnboardingControllerBinding(),
),
GetPage(name: AppRoutes.onboarding, page: () => const OnboardingScreen()),
GetPage(
name: AppRoutes.roleSelection,
page: () => const RoleSelectionScreen(),
binding: RoleSelectionControllerBinding(),
),
// Auth
GetPage(
name: AppRoutes.signIn,
page: () => const SignInScreen(),
binding: SignInControllerBinding(),
),
GetPage(name: AppRoutes.signIn, page: () => const SignInScreen()),
GetPage(
name: AppRoutes.signUp,
page: () => const SignUpScreen(),
binding: SignUpControllerBinding(),
),
GetPage(name: AppRoutes.signUp, page: () => const SignUpScreen()),
GetPage(
name: AppRoutes.forgotPassword,
page: () => const ForgotPasswordScreen(),
binding: ForgotPasswordControllerBinding(),
),
// Main pages
// GetPage(name: AppRoutes.explore, page: () => const ExploreScreen()),
// GetPage(name: AppRoutes.map, page: () => const MapScreen()),
// GetPage(name: AppRoutes.panicButton, page: () => const PanicButtonScreen()),
// GetPage(name: AppRoutes.communityWatch, page: () => const CommunityWatchScreen()),
// GetPage(name: AppRoutes.dailyOps, page: () => const DailyOpsScreen()),
// Personalization
// GetPage(name: AppRoutes.profile, page: () => const ProfileScreen()),
// GetPage(name: AppRoutes.settings, page: () => const SettingsScreen()),
];
}

View File

@ -0,0 +1,18 @@
import 'package:get/get.dart';
import 'package:sigap/src/features/auth/controllers/email_verification_controller.dart';
import 'package:sigap/src/features/auth/controllers/forgot_password_controller.dart';
import 'package:sigap/src/features/auth/controllers/signin_controller.dart';
import 'package:sigap/src/features/auth/controllers/signup_controller.dart';
import 'package:sigap/src/features/auth/controllers/step_form_controller.dart';
class AuthBindings extends Bindings {
@override
void dependencies() {
// Register all feature auth controllers
Get.lazyPut(() => SignInController());
Get.lazyPut(() => SignUpController());
Get.lazyPut(() => StepFormController());
Get.lazyPut(() => EmailVerificationController());
Get.lazyPut(() => ForgotPasswordController());
}
}

View File

@ -0,0 +1,12 @@
import 'package:get/get.dart';
import 'package:sigap/src/features/onboarding/controllers/onboarding_controller.dart';
import 'package:sigap/src/features/onboarding/controllers/role_selection_controller.dart';
class OnboardingBindings extends Bindings {
@override
void dependencies() {
// Register all feature onboarding controllers
Get.lazyPut(() => OnboardingController());
Get.lazyPut(() => RoleSelectionController());
}
}

View File

@ -93,6 +93,9 @@ dependencies:
# Fonts
google_fonts:
# Localization
flutter_localizations
dev_dependencies:
flutter_test:
sdk: flutter