first commit
This commit is contained in:
295
lib/app/core/services/bottom_sheet_service.dart
Normal file
295
lib/app/core/services/bottom_sheet_service.dart
Normal file
@@ -0,0 +1,295 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:frontend_eccp_mobile/app/core/constants/constants.dart';
|
||||
import 'package:frontend_eccp_mobile/app/core/navigation/app_resume_once.dart';
|
||||
import 'package:frontend_eccp_mobile/app/core/navigation/settings_navigator.dart';
|
||||
import 'package:frontend_eccp_mobile/app/core/services/location_permission_service.dart';
|
||||
import 'package:internet_connection_checker/internet_connection_checker.dart';
|
||||
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
|
||||
|
||||
typedef BottomSheetBuilder = Widget Function(BuildContext context);
|
||||
|
||||
typedef LocationSheetBuilder =
|
||||
Widget Function(
|
||||
BuildContext context, {
|
||||
required VoidCallback onOpenSettings,
|
||||
VoidCallback? onRequestNow,
|
||||
});
|
||||
|
||||
class BottomSheetService {
|
||||
BottomSheetService._();
|
||||
|
||||
static bool _isShown = false;
|
||||
|
||||
static BottomSheetBuilder? _noInternetBuilder;
|
||||
static BottomSheetBuilder? _serverErrorBuilder;
|
||||
static BottomSheetBuilder? _notFoundBuilder;
|
||||
static BottomSheetBuilder? _sessionExpiredBuilder;
|
||||
static LocationSheetBuilder? _locationPermissionBuilder;
|
||||
|
||||
static StreamSubscription<InternetConnectionStatus>? _connSub;
|
||||
|
||||
static void configure({
|
||||
BottomSheetBuilder? noInternetBuilder,
|
||||
BottomSheetBuilder? serverErrorBuilder,
|
||||
BottomSheetBuilder? notFoundBuilder,
|
||||
BottomSheetBuilder? sessionExpiredBuilder,
|
||||
LocationSheetBuilder? locationPermissionBuilder,
|
||||
}) {
|
||||
_noInternetBuilder = noInternetBuilder;
|
||||
_serverErrorBuilder = serverErrorBuilder;
|
||||
_notFoundBuilder = notFoundBuilder;
|
||||
_sessionExpiredBuilder = sessionExpiredBuilder;
|
||||
_locationPermissionBuilder = locationPermissionBuilder;
|
||||
}
|
||||
|
||||
static Future<void> showNoInternet() => _show(
|
||||
_noInternetBuilder,
|
||||
_defaultNoInternet,
|
||||
autoDismissOnReconnect: true,
|
||||
);
|
||||
|
||||
static Future<void> showServerError() =>
|
||||
_show(_serverErrorBuilder, _defaultServerError);
|
||||
|
||||
static Future<void> showNotFound() =>
|
||||
_show(_notFoundBuilder, _defaultNotFound);
|
||||
|
||||
static Future<void> showSessionExpired() => _show(
|
||||
_sessionExpiredBuilder,
|
||||
_defaultSessionExpired,
|
||||
isDismissible: false,
|
||||
);
|
||||
|
||||
static Future<void> showCustom(
|
||||
BottomSheetBuilder builder, {
|
||||
bool isDismissible = true,
|
||||
}) {
|
||||
return _show(builder, builder, isDismissible: isDismissible);
|
||||
}
|
||||
|
||||
static Future<T?> showCustomReturn<T>(
|
||||
Widget Function(BuildContext) builder, {
|
||||
bool isDismissible = true,
|
||||
}) async {
|
||||
return showModalBottomSheet<T>(
|
||||
context: navigatorKey.currentContext!,
|
||||
isDismissible: isDismissible,
|
||||
isScrollControlled: true,
|
||||
|
||||
backgroundColor: Colors.transparent,
|
||||
enableDrag: isDismissible,
|
||||
useRootNavigator: true,
|
||||
builder: builder,
|
||||
);
|
||||
}
|
||||
|
||||
static Future<void> showLocationPermission({
|
||||
required VoidCallback onOpenSettings,
|
||||
VoidCallback? onRequestNow,
|
||||
}) {
|
||||
final custom = _locationPermissionBuilder == null
|
||||
? null
|
||||
: (BuildContext ctx) => _locationPermissionBuilder!(
|
||||
ctx,
|
||||
onOpenSettings: onOpenSettings,
|
||||
onRequestNow: onRequestNow,
|
||||
);
|
||||
|
||||
return _show(
|
||||
custom,
|
||||
(ctx) => _defaultLocationPermission(
|
||||
ctx,
|
||||
onOpenSettings: onOpenSettings,
|
||||
onRequestNow: onRequestNow,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static Future<void> _show(
|
||||
BottomSheetBuilder? custom,
|
||||
BottomSheetBuilder fallback, {
|
||||
bool autoDismissOnReconnect = false,
|
||||
bool isDismissible = true,
|
||||
}) async {
|
||||
final navState = navigatorKey.currentState;
|
||||
final overlayContext = navState?.overlay?.context ?? navState?.context;
|
||||
|
||||
if (overlayContext == null || _isShown) return;
|
||||
|
||||
_isShown = true;
|
||||
|
||||
if (autoDismissOnReconnect) {
|
||||
await _connSub?.cancel();
|
||||
_connSub = InternetConnectionChecker.instance.onStatusChange.listen(
|
||||
(status) {
|
||||
if (status == InternetConnectionStatus.connected && _isShown) {
|
||||
final navigator = navigatorKey.currentState;
|
||||
if (navigator != null && navigator.canPop()) {
|
||||
navigator.pop();
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
if (!overlayContext.mounted) return;
|
||||
|
||||
await showMaterialModalBottomSheet<void>(
|
||||
context: overlayContext,
|
||||
backgroundColor: Colors.transparent,
|
||||
isDismissible: isDismissible,
|
||||
enableDrag: isDismissible,
|
||||
useRootNavigator: true,
|
||||
duration: AppDurations.modalTransition,
|
||||
builder: (ctx) {
|
||||
return WillPopScope(
|
||||
onWillPop: () async => isDismissible,
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
bottom: false,
|
||||
child: custom != null ? custom(ctx) : fallback(ctx),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
_isShown = false;
|
||||
await _connSub?.cancel();
|
||||
_connSub = null;
|
||||
}
|
||||
}
|
||||
|
||||
static Widget _defaultNoInternet(BuildContext context) => Padding(
|
||||
padding: AppPadding.p24,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.wifi_off, size: AppIconSizes.xxl),
|
||||
AppSpacing.h12,
|
||||
Text('Tidak ada internet', style: AppTextStyles.h3),
|
||||
AppSpacing.h8,
|
||||
Text(
|
||||
'Periksa koneksi kamu lalu coba lagi.',
|
||||
style: AppTextStyles.small,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
static Widget _defaultServerError(BuildContext context) => Padding(
|
||||
padding: AppPadding.p24,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.cloud_off, size: AppIconSizes.xxl),
|
||||
AppSpacing.h12,
|
||||
Text('Gangguan server', style: AppTextStyles.h3),
|
||||
AppSpacing.h8,
|
||||
Text(
|
||||
'Coba beberapa saat lagi.',
|
||||
style: AppTextStyles.small,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
static Widget _defaultNotFound(BuildContext context) => Padding(
|
||||
padding: AppPadding.p24,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.cloud_off, size: AppIconSizes.xxl),
|
||||
AppSpacing.h12,
|
||||
Text('Opps, kamu tersesat', style: AppTextStyles.h3),
|
||||
AppSpacing.h8,
|
||||
Text(
|
||||
'Baik, dimengerti. Mari kembali ke jalur semula.',
|
||||
style: AppTextStyles.small,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
static Widget _defaultSessionExpired(BuildContext context) => Padding(
|
||||
padding: AppPadding.p24,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.lock_outline, size: AppIconSizes.xxl),
|
||||
AppSpacing.h12,
|
||||
Text('Sesi berakhir', style: AppTextStyles.h3),
|
||||
AppSpacing.h8,
|
||||
Text(
|
||||
'Silakan login ulang untuk melanjutkan.',
|
||||
style: AppTextStyles.small,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
AppSpacing.h16,
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text('Login Ulang', style: AppTextStyles.button),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
static Widget _defaultLocationPermission(
|
||||
BuildContext context, {
|
||||
required VoidCallback onOpenSettings,
|
||||
VoidCallback? onRequestNow,
|
||||
}) {
|
||||
return Padding(
|
||||
padding: AppPadding.p24,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.my_location, size: AppIconSizes.xxl),
|
||||
AppSpacing.h12,
|
||||
Text('Permintaan Akses Lokasi', style: AppTextStyles.h3),
|
||||
AppSpacing.h8,
|
||||
Text(
|
||||
'Aplikasi ingin meminta akses lokasi kamu agar dapat memberikan pengalaman terbaik. Kamu dapat mengizinkan akses lokasi melalui pengaturan atau langsung sekarang.',
|
||||
style: AppTextStyles.small,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
AppSpacing.h16,
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: () async {
|
||||
final resumeFut = waitRecheckOnNextResume(() async {
|
||||
if (await LocationPermissionService.recheckGranted()) {
|
||||
onOpenSettings();
|
||||
}
|
||||
});
|
||||
|
||||
await SettingsNavigator.openAppSettings();
|
||||
await resumeFut;
|
||||
},
|
||||
child: Text('Buka Pengaturan', style: AppTextStyles.button),
|
||||
),
|
||||
),
|
||||
if (onRequestNow != null) ...[
|
||||
AppSpacing.h8,
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
onRequestNow();
|
||||
},
|
||||
child: Text('Izinkan Sekarang', style: AppTextStyles.label),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
75
lib/app/core/services/crashlytics_service.dart
Normal file
75
lib/app/core/services/crashlytics_service.dart
Normal file
@@ -0,0 +1,75 @@
|
||||
import 'dart:async';
|
||||
import 'dart:developer' as developer;
|
||||
|
||||
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:frontend_eccp_mobile/app/core/config/app_flavor.dart';
|
||||
|
||||
class CrashlyticsService {
|
||||
CrashlyticsService._();
|
||||
|
||||
static FirebaseCrashlytics get _instance => FirebaseCrashlytics.instance;
|
||||
|
||||
static Future<void> init() async {
|
||||
await _instance.setCrashlyticsCollectionEnabled(
|
||||
Flavor.isProd,
|
||||
);
|
||||
if (kDebugMode) return;
|
||||
|
||||
FlutterError.onError = (FlutterErrorDetails details) async {
|
||||
await _instance.recordFlutterFatalError(details);
|
||||
};
|
||||
|
||||
PlatformDispatcher.instance.onError = (error, stack) {
|
||||
unawaited(_instance.recordError(error, stack, fatal: true));
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
static Future<void> log(String message) async {
|
||||
if (kDebugMode) return;
|
||||
await _instance.log(message);
|
||||
}
|
||||
|
||||
static Future<void> recordError(
|
||||
Object error,
|
||||
StackTrace stack, {
|
||||
String? reason,
|
||||
bool fatal = false,
|
||||
Map<String, dynamic>? context,
|
||||
}) async {
|
||||
if (kDebugMode) return;
|
||||
if (context != null) {
|
||||
context.forEach((key, value) async {
|
||||
await _instance.setCustomKey(key, value.toString());
|
||||
});
|
||||
}
|
||||
developer.log('CrashlyticsService.recordError: $reason');
|
||||
await _instance.recordError(
|
||||
error,
|
||||
stack,
|
||||
reason: reason,
|
||||
fatal: fatal,
|
||||
);
|
||||
}
|
||||
|
||||
static Future<void> setUser({
|
||||
required String id,
|
||||
String? nrp,
|
||||
String? name,
|
||||
}) async {
|
||||
if (kDebugMode) return;
|
||||
await _instance.setUserIdentifier(id);
|
||||
if (nrp != null) {
|
||||
await _instance.setCustomKey('nrp', nrp);
|
||||
}
|
||||
if (name != null) {
|
||||
await _instance.setCustomKey('name', name);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> clearUser() async {
|
||||
if (kDebugMode) return;
|
||||
await _instance.setUserIdentifier('');
|
||||
}
|
||||
}
|
||||
56
lib/app/core/services/file_downloader.dart
Normal file
56
lib/app/core/services/file_downloader.dart
Normal file
@@ -0,0 +1,56 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
|
||||
class FileDownloader {
|
||||
static Future<String?> downloadAndSave({
|
||||
required String url,
|
||||
required String fileName,
|
||||
void Function(int received, int total)? onProgress,
|
||||
}) async {
|
||||
final dio = Dio();
|
||||
|
||||
final response = await dio.get<List<int>>(
|
||||
url,
|
||||
options: Options(responseType: ResponseType.bytes),
|
||||
onReceiveProgress: onProgress,
|
||||
);
|
||||
|
||||
final bytes = response.data;
|
||||
|
||||
if (bytes == null) return null;
|
||||
|
||||
final savedPath = await FilePicker.saveFile(
|
||||
dialogTitle: 'Save File',
|
||||
fileName: fileName,
|
||||
type: FileType.custom,
|
||||
allowedExtensions: ['pdf'],
|
||||
bytes: Uint8List.fromList(bytes),
|
||||
);
|
||||
|
||||
return savedPath;
|
||||
}
|
||||
|
||||
static Future<Map<String, dynamic>> getFileInfo(String url) async {
|
||||
final dio = Dio();
|
||||
|
||||
final response = await dio.head<dynamic>(url);
|
||||
final headers = response.headers;
|
||||
final size = headers.value('content-length');
|
||||
final sizeInBytes = int.tryParse(size ?? '0') ?? 0;
|
||||
final sizeInMB = sizeInBytes / (1024 * 1024);
|
||||
var fileName = url.split('/').last;
|
||||
final disposition = headers.value('content-disposition');
|
||||
|
||||
if (disposition != null && disposition.contains('filename=')) {
|
||||
fileName = disposition.split('filename=').last.replaceAll('"', '');
|
||||
}
|
||||
|
||||
return {
|
||||
'name': fileName,
|
||||
'sizeMB': sizeInMB,
|
||||
'sizeBytes': sizeInBytes,
|
||||
};
|
||||
}
|
||||
}
|
||||
21
lib/app/core/services/image_picker_service.dart
Normal file
21
lib/app/core/services/image_picker_service.dart
Normal file
@@ -0,0 +1,21 @@
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
|
||||
class ImagePickerService {
|
||||
ImagePickerService._();
|
||||
|
||||
static final ImagePicker _picker = ImagePicker();
|
||||
|
||||
static Future<XFile?> pickFromGallery() async {
|
||||
return _picker.pickImage(
|
||||
source: ImageSource.gallery,
|
||||
imageQuality: 85,
|
||||
);
|
||||
}
|
||||
|
||||
static Future<XFile?> pickFromCamera() async {
|
||||
return _picker.pickImage(
|
||||
source: ImageSource.camera,
|
||||
imageQuality: 85,
|
||||
);
|
||||
}
|
||||
}
|
||||
153
lib/app/core/services/location_permission_service.dart
Normal file
153
lib/app/core/services/location_permission_service.dart
Normal file
@@ -0,0 +1,153 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:frontend_eccp_mobile/app/core/constants/enum/location_status_enum.dart';
|
||||
import 'package:frontend_eccp_mobile/app/core/services/bottom_sheet_service.dart';
|
||||
import 'package:frontend_eccp_mobile/app/core/utils/token_manager.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
|
||||
class LocationPermissionService {
|
||||
LocationPermissionService._();
|
||||
|
||||
static Future<SimpleLocationStatus> checkStatus() async {
|
||||
final serviceEnabled = await Geolocator.isLocationServiceEnabled();
|
||||
if (!serviceEnabled) return SimpleLocationStatus.serviceDisabled;
|
||||
|
||||
final perm = await Geolocator.checkPermission();
|
||||
switch (perm) {
|
||||
case LocationPermission.always:
|
||||
case LocationPermission.whileInUse:
|
||||
return SimpleLocationStatus.granted;
|
||||
case LocationPermission.denied:
|
||||
case LocationPermission.unableToDetermine:
|
||||
return SimpleLocationStatus.denied;
|
||||
case LocationPermission.deniedForever:
|
||||
return SimpleLocationStatus.deniedForever;
|
||||
}
|
||||
}
|
||||
|
||||
static bool _requesting = false;
|
||||
static Completer<void>? _waiter;
|
||||
|
||||
static Future<T> _withLock<T>(Future<T> Function() body) async {
|
||||
while (_requesting) {
|
||||
_waiter ??= Completer<void>();
|
||||
await _waiter!.future;
|
||||
}
|
||||
_requesting = true;
|
||||
try {
|
||||
return await body();
|
||||
} finally {
|
||||
_requesting = false;
|
||||
_waiter?.complete();
|
||||
_waiter = null;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<bool> request() => _withLock<bool>(() async {
|
||||
var p = await Geolocator.checkPermission();
|
||||
if (p == LocationPermission.always || p == LocationPermission.whileInUse) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (p == LocationPermission.deniedForever) {
|
||||
return false;
|
||||
}
|
||||
|
||||
p = await Geolocator.requestPermission();
|
||||
return p == LocationPermission.always || p == LocationPermission.whileInUse;
|
||||
});
|
||||
|
||||
static Future<void> ensure({
|
||||
required VoidCallback onGranted,
|
||||
VoidCallback? onNotGranted,
|
||||
bool showSheetIfDenied = true,
|
||||
bool returnImmediatelyOnDenied = false,
|
||||
String? onlyOnceKey,
|
||||
}) async {
|
||||
final suppressSheet =
|
||||
(showSheetIfDenied && onlyOnceKey != null) &&
|
||||
await TokenManager.getOneTimeFlag(onlyOnceKey);
|
||||
|
||||
final serviceEnabled = await Geolocator.isLocationServiceEnabled();
|
||||
if (!serviceEnabled) {
|
||||
onNotGranted?.call();
|
||||
if (showSheetIfDenied && !suppressSheet) {
|
||||
final f = _showGoToSettingsSheet(
|
||||
forServiceDisabled: true,
|
||||
onAfterReturn: onGranted,
|
||||
onlyOnceKey: onlyOnceKey,
|
||||
allowRequestNow: false,
|
||||
);
|
||||
if (returnImmediatelyOnDenied) {
|
||||
unawaited(f);
|
||||
return;
|
||||
}
|
||||
await f;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
final grantedNow = await request();
|
||||
if (grantedNow) {
|
||||
onGranted();
|
||||
return;
|
||||
}
|
||||
|
||||
onNotGranted?.call();
|
||||
|
||||
if (showSheetIfDenied && !suppressSheet) {
|
||||
final f = _showGoToSettingsSheet(
|
||||
forServiceDisabled: false,
|
||||
onAfterReturn: onGranted,
|
||||
onlyOnceKey: onlyOnceKey,
|
||||
);
|
||||
if (returnImmediatelyOnDenied) {
|
||||
unawaited(f);
|
||||
return;
|
||||
}
|
||||
await f;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> showGoToSettingsSheet({
|
||||
required VoidCallback onAfterReturn,
|
||||
bool forServiceDisabled = false,
|
||||
bool allowRequestNow = true,
|
||||
String? onlyOnceKey,
|
||||
}) async {
|
||||
await _showGoToSettingsSheet(
|
||||
onAfterReturn: onAfterReturn,
|
||||
forServiceDisabled: forServiceDisabled,
|
||||
allowRequestNow: allowRequestNow,
|
||||
onlyOnceKey: onlyOnceKey,
|
||||
);
|
||||
}
|
||||
|
||||
static Future<bool> recheckGranted() async =>
|
||||
(await checkStatus()) == SimpleLocationStatus.granted;
|
||||
|
||||
static Future<void> _showGoToSettingsSheet({
|
||||
required VoidCallback onAfterReturn,
|
||||
required bool forServiceDisabled,
|
||||
bool allowRequestNow = true,
|
||||
String? onlyOnceKey,
|
||||
}) async {
|
||||
if (onlyOnceKey != null) {
|
||||
await TokenManager.setOneTimeFlag(onlyOnceKey, value: true);
|
||||
}
|
||||
|
||||
await BottomSheetService.showLocationPermission(
|
||||
onOpenSettings: () async {
|
||||
await Future<dynamic>.delayed(const Duration(milliseconds: 100));
|
||||
if (await recheckGranted()) onAfterReturn();
|
||||
},
|
||||
onRequestNow: allowRequestNow
|
||||
? () async {
|
||||
final ok = await request();
|
||||
if (ok) onAfterReturn();
|
||||
}
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
47
lib/app/core/services/remote_config_service.dart
Normal file
47
lib/app/core/services/remote_config_service.dart
Normal file
@@ -0,0 +1,47 @@
|
||||
import 'dart:convert';
|
||||
import 'package:firebase_remote_config/firebase_remote_config.dart';
|
||||
import 'package:frontend_eccp_mobile/app/core/config/app_flavor.dart';
|
||||
import 'package:frontend_eccp_mobile/app/core/config/app_update_config.dart';
|
||||
import 'package:frontend_eccp_mobile/app/core/services/crashlytics_service.dart';
|
||||
|
||||
class RemoteConfigService {
|
||||
RemoteConfigService._();
|
||||
|
||||
static final FirebaseRemoteConfig _rc = FirebaseRemoteConfig.instance;
|
||||
|
||||
static Future<void> init() async {
|
||||
try {
|
||||
await _rc.setConfigSettings(
|
||||
RemoteConfigSettings(
|
||||
fetchTimeout: const Duration(seconds: 10),
|
||||
minimumFetchInterval: const Duration(minutes: 1),
|
||||
),
|
||||
);
|
||||
|
||||
await _rc.fetchAndActivate();
|
||||
} catch (e, s) {
|
||||
await CrashlyticsService.recordError(
|
||||
e,
|
||||
s,
|
||||
reason: 'RemoteConfig init failed',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<AppUpdateConfig?> getUpdateConfig() async {
|
||||
try {
|
||||
final raw = _rc.getString(Flavor.name);
|
||||
if (raw.isEmpty) return null;
|
||||
|
||||
final json = jsonDecode(raw) as Map<String, dynamic>;
|
||||
return AppUpdateConfig.fromJson(json);
|
||||
} catch (e, s) {
|
||||
await CrashlyticsService.recordError(
|
||||
e,
|
||||
s,
|
||||
reason: 'Invalid update config JSON',
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user