feat: Refactor code structure and optimize performance across multiple modules

This commit is contained in:
Achintha Isuru
2025-11-17 23:29:28 -05:00
parent 831570f2e0
commit a64cbd9edf
1508 changed files with 105319 additions and 0 deletions

View File

@@ -0,0 +1,126 @@
import 'dart:io';
import 'package:firebase_remote_config/firebase_remote_config.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:injectable/injectable.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:url_launcher/url_launcher_string.dart';
@singleton
class AppUpdateService {
Future<void> checkForUpdate(BuildContext context) async {
final remoteConfig = FirebaseRemoteConfig.instance;
await remoteConfig.setConfigSettings(RemoteConfigSettings(
fetchTimeout: const Duration(seconds: 10),
minimumFetchInterval: const Duration(seconds: 1),
));
await remoteConfig.fetchAndActivate();
final minBuildNumber = remoteConfig.getInt(Platform.isIOS?'min_build_number_staff_ios':'min_build_number_staff_android');
final canSkip = remoteConfig.getBool('can_skip_staff');
final canIgnore = remoteConfig.getBool('can_ignore_staff');
final message = remoteConfig.getString('message_staff');
final packageInfo = await PackageInfo.fromPlatform();
final currentBuildNumber = int.parse(packageInfo.buildNumber);
final prefs = await SharedPreferences.getInstance();
final skippedVersion = prefs.getInt('skipped_version') ?? 0;
if (minBuildNumber > currentBuildNumber &&
minBuildNumber > skippedVersion) {
await _showUpdateDialog(context, message, canSkip, canIgnore, minBuildNumber);
}
}
_showUpdateDialog(
BuildContext context, String message, bool canSkip, bool canIgnore, int minBuildNumber) {
return showDialog(
context: context,
barrierDismissible: canIgnore,
builder: (BuildContext context) {
if (Theme.of(context).platform == TargetPlatform.iOS) {
return WillPopScope(
onWillPop: () async => canIgnore,
child: CupertinoAlertDialog(
title: const Text('Update Available'),
content: Text(
message),
actions: <Widget>[
if (canSkip)
CupertinoDialogAction(
child: const Text('Skip this version'),
onPressed: () async {
final prefs = await SharedPreferences.getInstance();
await prefs.setInt('skipped_version', minBuildNumber);
Navigator.of(context).pop();
},
),
CupertinoDialogAction(
onPressed:
canIgnore ? () => Navigator.of(context).pop() : null,
child: const Text('Maybe later'),
),
CupertinoDialogAction(
child: const Text('Update'),
onPressed: () async {
var url = dotenv.env['IOS_STORE_URL'] ??
'';
if (await canLaunchUrlString(url)) {
await launchUrlString(url);
} else {
throw 'Could not launch $url';
}
},
),
],
),
);
} else {
return WillPopScope(
onWillPop: () async => canIgnore,
child: AlertDialog(
title: const Text('Update Available'),
content: Text(
message),
actions: <Widget>[
if (canSkip)
TextButton(
child: const Text('Skip this version'),
onPressed: () async {
final prefs = await SharedPreferences.getInstance();
await prefs.setInt('skipped_version', minBuildNumber);
Navigator.of(context).pop();
},
),
TextButton(
onPressed:
canIgnore ? () => Navigator.of(context).pop() : null,
child: const Text('Maybe later'),
),
TextButton(
child: const Text('Update'),
onPressed: () async {
var url = dotenv.env['ANDROID_STORE_URL'] ??
'';
if (await canLaunchUrlString(url)) {
await launchUrlString(url);
} else {
throw 'Could not launch $url';
}
},
),
],
),
);
}
},
);
}
}

View File

@@ -0,0 +1,120 @@
import 'dart:developer';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:injectable/injectable.dart';
import 'package:krow/core/application/clients/api/api_client.dart';
import 'package:krow/core/application/di/injectable.dart';
import 'package:krow/core/data/models/staff/staff.dart';
import 'package:krow/core/data/static/email_validation_constants.dart';
import 'package:krow/core/sevices/auth_state_service/auth_service_data_provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
enum AuthStatus {
authenticated,
adminValidation,
prepareProfile,
unauthenticated,
error
}
@injectable
class AuthService {
final AuthServiceDataProvider _dataProvider;
AuthService(this._dataProvider);
Future<AuthStatus> getAuthStatus() async {
User? user;
try {
user = FirebaseAuth.instance.currentUser;
if (user == null) {
return AuthStatus.unauthenticated;
}
user.getIdToken();
} catch (e) {
return AuthStatus.unauthenticated;
}
final staffStatus = await getCachedStaffStatus();
if (staffStatus == StaffStatus.deactivated) {
return AuthStatus.unauthenticated;
} else if (staffStatus == StaffStatus.pending) {
return AuthStatus.adminValidation;
} else if (staffStatus == StaffStatus.registered ||
staffStatus == StaffStatus.declined) {
return AuthStatus.prepareProfile;
} else {
return AuthStatus.authenticated;
}
}
Future<void> signInWithEmailLink({required Uri link}) async {
final auth = FirebaseAuth.instance;
//TODO: Investigate iOS issue that blocks correct usage of continue URL for seamless email verification
//
// if (!auth.isSignInWithEmailLink(link.toString())) {
// throw Exception('Invalid auth link provided.');
// }
//
// final sharedPrefs = await SharedPreferences.getInstance();
//
// final userEmail = sharedPrefs.getString(
// EmailValidationConstants.storedEmailKey,
// );
//
// if (userEmail == null) {
// throw Exception(
// 'Failed to sign in user with Email Link, '
// 'because the is no email stored',
// );
// }
//
// await auth.currentUser?.linkWithCredential(
// EmailAuthProvider.credentialWithLink(
// email: userEmail,
// emailLink: link.toString(),
// ),
// );
final oobCode = link.queryParameters[EmailValidationConstants.oobCodeKey];
if (oobCode == null) return;
log('Incoming auth link: $link');
return auth.applyActionCode(oobCode);
}
Future<StaffStatus?> getCachedStaffStatus() async {
final SharedPreferences prefs = await SharedPreferences.getInstance();
var uid = FirebaseAuth.instance.currentUser?.uid;
try {
var staffStatus = await _dataProvider.getStaffStatus();
prefs.setInt('staff_status_$uid', staffStatus.index);
return staffStatus;
} catch (e) {
log('Error in AuthService, on getCachedStaffStatus()', error: e);
}
if (uid != null) {
var staffStatusIndex = prefs.getInt('staff_status_$uid');
if (staffStatusIndex != null) {
return StaffStatus.values[staffStatusIndex];
}
}
return null;
}
void logout() {
FirebaseAuth.instance.signOut();
getIt<ApiClient>().dropCache();
}
Future<void> deleteAccount() async {
await FirebaseAuth.instance.currentUser!.delete();
getIt<ApiClient>().dropCache();
}
}

View File

@@ -0,0 +1,24 @@
import 'package:graphql_flutter/graphql_flutter.dart';
import 'package:injectable/injectable.dart';
import 'package:krow/core/application/clients/api/api_client.dart';
import 'package:krow/core/data/models/staff/staff.dart';
import 'package:krow/core/sevices/auth_state_service/gql.dart';
@injectable
class AuthServiceDataProvider {
final ApiClient _client;
AuthServiceDataProvider({required ApiClient client}) : _client = client;
Future<StaffStatus> getStaffStatus() async {
final QueryResult result = await _client.query(schema: getStaffStatusQuery);
if (result.hasException) {
throw Exception(result.exception.toString());
}
final Map<String, dynamic> data = result.data!['me'];
return Staff.fromJson(data).status!;
}
}

View File

@@ -0,0 +1,9 @@
const String getStaffStatusQuery = '''
query GetMe {
me {
id
status
}
}
''';

View File

@@ -0,0 +1,102 @@
import 'dart:async';
import 'dart:io';
import 'dart:ui';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart';
import 'package:flutter_background_service/flutter_background_service.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:injectable/injectable.dart';
import 'package:krow/core/application/di/injectable.dart';
import 'package:krow/core/sevices/background_service/task_registry.dart';
import 'package:krow/features/shifts/domain/services/clockout_checker_bg_task.dart';
import 'package:workmanager/workmanager.dart';
@Singleton()
class BackgroundService {
Future<void> initializeService() async {
// FirebaseMessaging.onBackgroundMessage(firebaseMessagingBackgroundHandler);
//
// if (Platform.isIOS) {
// final service = FlutterBackgroundService();
// await service.configure(
// iosConfiguration: IosConfiguration(
// autoStart: true,
// onForeground: (_) {},
// onBackground: iosDispatcher,
// ),
// androidConfiguration: AndroidConfiguration(
// autoStart: false,
// onStart: (_) {},
// isForegroundMode: false,
// autoStartOnBoot: true,
// ),
// );
// service.startService();
// } else if (Platform.isAndroid) {
// Workmanager().initialize(
// androidDispatcher, // The top level function, aka callbackDispatcher
// isInDebugMode: false,
// // If enabled it will post a notification whenever the task is running. Handy for debugging tasks
// );
// if (Platform.isAndroid) {
// Workmanager().registerPeriodicTask(
// 'androidDispatcher',
// 'androidDispatcher',
// frequency: const Duration(minutes: 15),
// );
// }
// }
}
}
@pragma('vm:entry-point')
Future firebaseMessagingBackgroundHandler(RemoteMessage message) async {
// if (message.data['type'] == 'location_check') {
// await _configDependencies();
// await iosDispatcher(null);
// }
}
@pragma('vm:entry-point')
Future<bool> iosDispatcher(ServiceInstance? service) async {
// await _configDependencies();
// TaskRegistry.registerTask(ContinuousClockoutCheckerTask());
// for (var task in TaskRegistry.getRegisteredTasks()) {
// await task.oneTime(service);
// }
return true;
}
@pragma('vm:entry-point')
void androidDispatcher() async {
// Workmanager().executeTask((task, inputData) async {
// print('BG Task: executeTask');
//
// try {
// await _configDependencies();
// TaskRegistry.registerTask(ContinuousClockoutCheckerTask());
// for (var task in TaskRegistry.getRegisteredTasks()) {
// await task.oneTime(null);
// }
// }catch (e) {
// return Future.error(e);
// }
// return Future.value(true);
// });
}
Future<void> _configDependencies() async {
WidgetsFlutterBinding.ensureInitialized();
DartPluginRegistrant.ensureInitialized();
// await initHiveForFlutter(); - do no init hive in background!
//todo: add env variable
await dotenv.load(fileName: '.env');
await Firebase.initializeApp();
try {
configureDependencies('background');
} catch (e) {
print('Error configuring dependencies: $e');
}
}

View File

@@ -0,0 +1,6 @@
import 'package:flutter_background_service/flutter_background_service.dart';
abstract class BackgroundTask {
Future<void> oneTime(ServiceInstance? service);
Future<void> stop();
}

View File

@@ -0,0 +1,11 @@
import 'package:krow/core/sevices/background_service/background_task.dart';
class TaskRegistry {
static final List<BackgroundTask> _tasks = [];
static void registerTask(BackgroundTask task) {
_tasks.add(task);
}
static List<BackgroundTask> getRegisteredTasks() => _tasks;
}

View File

@@ -0,0 +1,107 @@
import 'dart:developer';
import 'package:geolocator/geolocator.dart';
import 'package:injectable/injectable.dart';
@injectable
class GeofencingService {
Future<GeolocationStatus> requestGeolocationPermission() async {
LocationPermission permission;
permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) {
return GeolocationStatus.denied;
}
}
if (permission == LocationPermission.deniedForever) {
return GeolocationStatus.prohibited;
}
if (!(await Geolocator.isLocationServiceEnabled())) {
// Location services are not enabled
return GeolocationStatus.disabled;
}
// if (permission == LocationPermission.whileInUse) {
// return GeolocationStatus.onlyInUse;
// }
return GeolocationStatus.enabled;
}
bool _isInRange(
Position currentPosition,
double pointLat,
double pointLon,
int range,
) {
double distance = Geolocator.distanceBetween(
currentPosition.latitude,
currentPosition.longitude,
pointLat,
pointLon,
);
return distance <= range;
}
/// Checks if the user's current location is within [range] meters
/// of the given [pointLatitude] and [pointLongitude].
Future<bool> isInRangeCheck({
required double pointLatitude,
required double pointLongitude,
int range = 500,
}) async {
try {
Position position = await Geolocator.getCurrentPosition(
locationSettings: const LocationSettings(
accuracy: LocationAccuracy.best,
timeLimit: Duration(seconds: 7)));
return _isInRange(position, pointLatitude, pointLongitude, range);
} catch (except) {
log('Error getting location: $except');
return false;
}
}
/// Constantly checks if the user's current location is within [range] meters
/// of the given [pointLatitude] and [pointLongitude].
Stream<bool> isInRangeStream({
required double pointLatitude,
required double pointLongitude,
int range = 500,
}) async* {
yield _isInRange(
await Geolocator.getCurrentPosition(),
pointLatitude,
pointLongitude,
range,
);
await for (final position in Geolocator.getPositionStream(
locationSettings: const LocationSettings())) {
try {
yield _isInRange(position, pointLatitude, pointLongitude, range);
} catch (except) {
log('Error getting location: $except');
yield false;
}
}
}
}
/// [disabled] indicates that the user should enable geolocation on the device.
/// [denied] indicates that permission should be requested or re-requested.
/// [prohibited] indicates that permission is denied and can only be changed
/// via the Settings.
/// [enabled] - geolocation service is allowed and available for usage.
enum GeolocationStatus {
disabled,
denied,
prohibited,
onlyInUse,
enabled,
}