feat: Refactor code structure and optimize performance across multiple modules
This commit is contained in:
126
mobile-apps/staff-app/lib/core/sevices/app_update_service.dart
Normal file
126
mobile-apps/staff-app/lib/core/sevices/app_update_service.dart
Normal 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';
|
||||
}
|
||||
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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!;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
|
||||
const String getStaffStatusQuery = '''
|
||||
query GetMe {
|
||||
me {
|
||||
id
|
||||
status
|
||||
}
|
||||
}
|
||||
''';
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import 'package:flutter_background_service/flutter_background_service.dart';
|
||||
|
||||
abstract class BackgroundTask {
|
||||
Future<void> oneTime(ServiceInstance? service);
|
||||
Future<void> stop();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
107
mobile-apps/staff-app/lib/core/sevices/geofencing_serivce.dart
Normal file
107
mobile-apps/staff-app/lib/core/sevices/geofencing_serivce.dart
Normal 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,
|
||||
}
|
||||
Reference in New Issue
Block a user