From 25150980026706ab2c35d1547c9da4ca0a593521 Mon Sep 17 00:00:00 2001 From: reasje Date: Fri, 26 Jul 2024 13:50:04 +0330 Subject: [PATCH] feat: blueberry ring auto sync --- assets/flutter_i18n/en.json | 17 +- .../axs_background_fetch.dart | 4 +- .../dapp_hooks_service.dart | 44 +++++ ...lueberry_ring_bckground_sync_use_case.dart | 123 ++++++++++++ .../dapp_hooks/dapp_hooks_page.dart | 19 +- .../dapp_hooks/dapp_hooks_presenter.dart | 26 ++- .../domain/dapp_hooks_use_case.dart | 176 ++++++++++++++---- .../background_fetch_config_use_case.dart | 1 + 8 files changed, 372 insertions(+), 38 deletions(-) create mode 100644 lib/features/common/packages/bluetooth/blueberry_ring/domain/blueberry_ring_bckground_sync_use_case.dart diff --git a/assets/flutter_i18n/en.json b/assets/flutter_i18n/en.json index 27e7fd11..0c17fa04 100644 --- a/assets/flutter_i18n/en.json +++ b/assets/flutter_i18n/en.json @@ -433,6 +433,19 @@ "low_battery": "Low battery", "blueberry_background_notifications_requirements_title": "This service requires: ", "blueberry_background_notifications_requirements_text_1": "1. Bluetooth to be ON", - "blueberry_background_notifications_requirements_text_2": "2. Blueberry to be reachable" - + "blueberry_background_notifications_requirements_text_2": "2. Blueberry to be reachable", + "auto_sync_started": "Blueberry Ring auto sync started 🏁", + "no_rings_selected_notification_title": "Looks like you haven't selected any Blueberry Rings. ℹī¸", + "no_rings_selected_notification_text": "Please head over to Blueberry Ring DApp for selecting rings.", + "auto_sync_successful_notification_title": "Blueberry Ring aut-claim successful ✅", + "auto_sync_successful_notification_text": "AXS wallet has been successfully synced your ring data", + "already_synced_notification_title": "Oops, Already synced ℹī¸", + "already_synced_notification_text": "AXS wallet tried to sync your ring data, But looks like you have synced the ring data today.", + "auto_sync_failed": "Blueberry Ring aut-sync failed ❌", + "no_rings_owned_notification": "Looks like this wallet doesn't own any Blueberry Rings. ℹī¸", + "syncing_data_from_ring": "Syncing data from Blueberry Ring #{0}. ⛏ī¸", + "already_synced_ring": "Blueberry Ring #{0}: Already synced. ℹī¸", + "data_synced_successfully_ring": "Blueberry Ring #{0}: Data synced successfully. ✅", + "data_syncing_failed": "Blueberry Ring #{0}: Data syncing process failed. ❌", + "blueberry_hooks": "Blueberry hooks" } \ No newline at end of file diff --git a/lib/core/src/background_process/axs_background_fetch.dart b/lib/core/src/background_process/axs_background_fetch.dart index b4edb33c..e4dc9428 100644 --- a/lib/core/src/background_process/axs_background_fetch.dart +++ b/lib/core/src/background_process/axs_background_fetch.dart @@ -28,6 +28,8 @@ class AXSBackgroundFetch { DAppHooksService.dappHooksServiceCallBackDispatcherForeground(taskId); } else if (taskId == BackgroundExecutionConfig.minerAutoClaimTask) { DAppHooksService.autoClaimServiceCallBackDispatcherForeground(taskId); + } else if (taskId == BackgroundExecutionConfig.blueberryAutoSyncTask) { + DAppHooksService.blueberryAutoSyncServiceCallBackDispatcherForeground(taskId); } else { bgFetch.BackgroundFetch.finish(taskId); } @@ -77,7 +79,7 @@ class AXSBackgroundFetch { requiresCharging: false, requiresStorageNotLow: false, requiresDeviceIdle: false, - requiredNetworkType: bgFetch.NetworkType.ANY), + requiredNetworkType: bgFetch.NetworkType.ANY,), handleCallBackDispatcher); // Android Only final backgroundFetchState = diff --git a/lib/core/src/background_process/dapp_hooks_service.dart b/lib/core/src/background_process/dapp_hooks_service.dart index 523d7535..744daede 100644 --- a/lib/core/src/background_process/dapp_hooks_service.dart +++ b/lib/core/src/background_process/dapp_hooks_service.dart @@ -66,6 +66,50 @@ class DAppHooksService { DAppHooksModel dappHooksData = dAppHooksUseCase.dappHooksData.value; final chainId = selectedNetwork.chainId; + final isLoggedIn = authUseCase.loggedIn; + final account = accountUseCase.account.value; + // final serviceEnabled = dappHooksData.enabled; + final minerHooksEnabled = dappHooksData.minerHooks.enabled; + final minerHooksTime = dappHooksData.minerHooks.time; + final selectedMiners = dappHooksData.minerHooks.selectedMiners; + // Make sure user is logged in + if (isLoggedIn && MXCChains.isMXCChains(chainId) && minerHooksEnabled) { + await AXSNotification() + .setupFlutterNotifications(shouldInitFirebase: false); + + await dAppHooksUseCase.executeMinerAutoClaim( + account: account!, + selectedMinerListId: selectedMiners, + minerAutoClaimTime: minerHooksTime); + BackgroundFetch.finish(taskId); + } else { + // terminate background fetch + BackgroundFetch.stop(taskId); + } + } catch (e) { + BackgroundFetch.finish(taskId); + } + } + + static void blueberryAutoSyncServiceCallBackDispatcherForeground( + String taskId) async { + try { + await loadProviders(); + + final container = ProviderContainer(); + final authUseCase = container.read(authUseCaseProvider); + final chainConfigurationUseCase = + container.read(chainConfigurationUseCaseProvider); + final accountUseCase = container.read(accountUseCaseProvider); + final dAppHooksUseCase = container.read(dAppHooksUseCaseProvider); + final contextLessTranslationUseCase = + container.read(contextLessTranslationUseCaseProvider); + + final selectedNetwork = + chainConfigurationUseCase.getCurrentNetworkWithoutRefresh(); + DAppHooksModel dappHooksData = dAppHooksUseCase.dappHooksData.value; + final chainId = selectedNetwork.chainId; + final isLoggedIn = authUseCase.loggedIn; final account = accountUseCase.account.value; // final serviceEnabled = dappHooksData.enabled; diff --git a/lib/features/common/packages/bluetooth/blueberry_ring/domain/blueberry_ring_bckground_sync_use_case.dart b/lib/features/common/packages/bluetooth/blueberry_ring/domain/blueberry_ring_bckground_sync_use_case.dart new file mode 100644 index 00000000..a829071b --- /dev/null +++ b/lib/features/common/packages/bluetooth/blueberry_ring/domain/blueberry_ring_bckground_sync_use_case.dart @@ -0,0 +1,123 @@ +import 'dart:async'; +import 'dart:convert'; +import 'package:datadashwallet/features/common/common.dart'; +import 'package:mxc_logic/mxc_logic.dart'; + +import 'package:datadashwallet/core/core.dart'; +import 'package:datadashwallet/features/settings/subfeatures/chain_configuration/domain/chain_configuration_use_case.dart'; + +import '../../../../../../app/logger.dart'; + +class BlueberryRingBackgroundNotificationsUseCase extends ReactiveUseCase { + BlueberryRingBackgroundNotificationsUseCase( + this._repository, + this._chainConfigurationUseCase, + this._bluetoothUseCase, + this._blueberryRingUseCase, + this._accountUserCase, + this._contextLessTranslationUseCase); + + final Web3Repository _repository; + final ChainConfigurationUseCase _chainConfigurationUseCase; + final BluetoothUseCase _bluetoothUseCase; + final BlueberryRingUseCase _blueberryRingUseCase; + final AccountUseCase _accountUserCase; + final ContextLessTranslationUseCase _contextLessTranslationUseCase; + + + // Context less translation, This should be only used for BG functions + String cTranslate(String key) => + _contextLessTranslationUseCase.translate(key); + + + Future sendSyncTransaction({ + required BlueberryRingMiner ring, + required Account account, + required void Function(String title, String? text) showNotification, + required String Function( + String key, + ) + translate, + }) async { + // Get rings list + + // showNotification( + // translate('no_token_to_claim_miner') + // .replaceFirst('{0}', miner.mep1004TokenId!), + // null, + // ); + // no_rings_owned_notification + // syncing_data_from_ring + // already_synced_ring + // data_synced_successfully_ring + // data_syncing_failed + final memo = await fetchRingData(); + + final postClaimRequest = PostClaimRequestModel( + sncode: ring.sncode, + sender: account.address, + ); + final postClaimResponse = await _repository.blueberryRingRepository.postClaim( + postClaimRequest, + ); + + final txSig = await _repository.blueberryRingRepository.sendSyncTransaction(account.privateKey, ring, postClaimResponse, memo); + + // showNotification( + // translate('no_token_to_claim_miner') + // .replaceFirst('{0}', miner.mep1004TokenId!), + // null, + // ); + + + } + + + Future fetchRingData() async { + collectLog('fetchRingData'); + + final sleep = await _blueberryRingUseCase.readSleep(); + final bloodOxygens = await _blueberryRingUseCase.readBloodOxygens(); + final steps = await _blueberryRingUseCase.readSteps(); + final heartRate = await _blueberryRingUseCase.readHeartRate(); + + final data = { + 'sleep': sleep.map((e) => e.toJson()).toList(), + 'steps': steps.map((e) => e.toJson()).toList(), + 'heartRate': heartRate.map((e) => e.toJson()).toList(), + 'bloodOxygens': bloodOxygens.map((e) => e.toJson()).toList(), + }; + + + final content = json.encode(data); + + collectLog('fetchRingData:content : $content'); + + final mep3355 = { + 'format': 'MEP-3355', + 'version': '1.0.0', + 'metadata': { + 'data_source': 'BlueberryRingV1', + 'data_collection_method': 'bluetooth', + 'preprocessing': 'weighted average of data', + }, + 'data': [ + { + 'type': 'sensor', + // 'content': await compress(content), + 'compression': 'brotli', + }, + ], + }; + + collectLog('fetchRingData:content : $mep3355'); + + final returndataMap = { + 'json': mep3355, + 'data': data, + }; + final returnDataJson = json.encode(returndataMap); + return returnDataJson; + } + +} diff --git a/lib/features/settings/subfeatures/dapp_hooks/dapp_hooks_page.dart b/lib/features/settings/subfeatures/dapp_hooks/dapp_hooks_page.dart index cb5352ce..d3393042 100644 --- a/lib/features/settings/subfeatures/dapp_hooks/dapp_hooks_page.dart +++ b/lib/features/settings/subfeatures/dapp_hooks/dapp_hooks_page.dart @@ -125,11 +125,28 @@ class DAppHooksPage extends HookConsumerWidget { const SizedBox(height: Sizes.spaceNormal), MXCDropDown( key: const Key('minerHooksTimingDropDown'), - onTap: dappHooksPresenter.showTimePickerDialog, + onTap: dappHooksPresenter.showTimePickerMinerDialog, selectedItem: autoClaimTime, enabled: isMXCChains && dappHooksState.dAppHooksData!.minerHooks.enabled, ), + // const SizedBox(height: Sizes.spaceLarge), + // MXCSwitchRowItem( + // key: const Key('blueberryRingHookSwitch'), + // title: translate('blueberry_hooks'), + // value: dappHooksState.dAppHooksData!.blueberryRingHooks.enabled, + // onChanged: dappHooksPresenter.changeBlueberryHooksEnabled, + // enabled: isMXCChains, + // titleStyle: FontTheme.of(context).h6(), + // ), + // const SizedBox(height: Sizes.spaceNormal), + // MXCDropDown( + // key: const Key('BlueberryHooksTimingDropDown'), + // onTap: dappHooksPresenter.showTimePickerBlueberryRingDialog, + // selectedItem: autoClaimTime, + // enabled: + // isMXCChains && dappHooksState.dAppHooksData!.blueberryRingHooks.enabled, + // ), ], ); } diff --git a/lib/features/settings/subfeatures/dapp_hooks/dapp_hooks_presenter.dart b/lib/features/settings/subfeatures/dapp_hooks/dapp_hooks_presenter.dart index 5e7ad2cd..90e83528 100644 --- a/lib/features/settings/subfeatures/dapp_hooks/dapp_hooks_presenter.dart +++ b/lib/features/settings/subfeatures/dapp_hooks/dapp_hooks_presenter.dart @@ -38,6 +38,13 @@ class DAppHooksPresenter extends CompletePresenter accountUseCase: _accountUseCase, backgroundFetchConfigUseCase: _backgroundFetchConfigUseCase); + BlueberryHooksHelper get blueberryRingHooksHelper => BlueberryHooksHelper( + translate: translate, + context: context, + dAppHooksUseCase: _dAppHooksUseCase, + accountUseCase: _accountUseCase, + backgroundFetchConfigUseCase: _backgroundFetchConfigUseCase); + WiFiHooksHelper get wifiHooksHelper => WiFiHooksHelper( translate: translate, context: context, @@ -82,6 +89,9 @@ class DAppHooksPresenter extends CompletePresenter void changeMinerHooksEnabled(bool value) => minerHooksHelper.changeMinerHooksEnabled(value); + void changeBlueberryHooksEnabled(bool value) => + blueberryRingHooksHelper.changeBLueberryRingHooksEnabled(value); + void showWiFiHooksFrequency() { showWiFiHooksFrequencyBottomSheet(context!, onTap: wifiHooksHelper.handleFrequencyChange, @@ -89,8 +99,20 @@ class DAppHooksPresenter extends CompletePresenter state.dAppHooksData!.wifiHooks.duration)); } - void showTimePickerDialog() async { + void showTimePickerMinerDialog() async { final currentTimeOfDay = state.dAppHooksData!.minerHooks.time; + showTimePickerDialog( + currentTimeOfDay, minerHooksHelper.changeMinerHookTiming); + } + + void showTimePickerBlueberryRingDialog() async { + final currentTimeOfDay = state.dAppHooksData!.blueberryRingHooks.time; + showTimePickerDialog(currentTimeOfDay, + blueberryRingHooksHelper.changeBlueberryRingHookTiming); + } + + void showTimePickerDialog(DateTime currentTimeOfDay, + Future Function(TimeOfDay value) changeTimeFunction) async { final initialTime = TimeOfDay.fromDateTime(currentTimeOfDay); final newTimeOfDay = await showTimePicker( context: context!, @@ -99,7 +121,7 @@ class DAppHooksPresenter extends CompletePresenter ); if (newTimeOfDay != null) { - minerHooksHelper.changeMinerHookTiming(newTimeOfDay); + changeTimeFunction(newTimeOfDay); } } diff --git a/lib/features/settings/subfeatures/dapp_hooks/domain/dapp_hooks_use_case.dart b/lib/features/settings/subfeatures/dapp_hooks/domain/dapp_hooks_use_case.dart index c28983c2..73eeb576 100644 --- a/lib/features/settings/subfeatures/dapp_hooks/domain/dapp_hooks_use_case.dart +++ b/lib/features/settings/subfeatures/dapp_hooks/domain/dapp_hooks_use_case.dart @@ -58,6 +58,8 @@ class DAppHooksUseCase extends ReactiveUseCase { String get wifiHookTasksTaskId => BackgroundExecutionConfig.wifiHooksTask; String get minerAutoClaimTaskTaskId => BackgroundExecutionConfig.minerAutoClaimTask; + String get blueberryAutoSyncTask => + BackgroundExecutionConfig.blueberryAutoSyncTask; void initialize() { _chainConfigurationUseCase.selectedNetwork.listen((network) { @@ -85,6 +87,34 @@ class DAppHooksUseCase extends ReactiveUseCase { updateItem(newDAppHooksData); } + void updateRingsList(List rings) { + final newDAppHooksData = dappHooksData.value.copyWith( + blueberryRingHooks: dappHooksData.value.blueberryRingHooks.copyWith( + selectedRings: rings, + ), + ); + updateItem(newDAppHooksData); + } + + void updateBlueberryRingHookTiming(TimeOfDay value) { + final newDAppHooksData = dappHooksData.value.copyWith( + blueberryRingHooks: dappHooksData.value.blueberryRingHooks.copyWith( + time: dappHooksData.value.blueberryRingHooks.time + .copyWith(hour: value.hour, minute: value.minute, second: 0), + ), + ); + updateItem(newDAppHooksData); + } + + void updateBlueberryRingHooksEnabled(bool value) { + final newDAppHooksData = dappHooksData.value.copyWith( + blueberryRingHooks: dappHooksData.value.blueberryRingHooks.copyWith( + enabled: value, + ), + ); + updateItem(newDAppHooksData); + } + void updateMinersList(List miners) { final newDAppHooksData = dappHooksData.value.copyWith( minerHooks: dappHooksData.value.minerHooks.copyWith( @@ -296,29 +326,7 @@ class DAppHooksUseCase extends ReactiveUseCase { // delay is in minutes Future startWifiHooksService(int delay) async { - try { - final result = await AXSBackgroundFetch.startBackgroundProcess( - taskId: wifiHookTasksTaskId); - - if (!result) return result; - - final scheduleState = - await bgFetch.BackgroundFetch.scheduleTask(bgFetch.TaskConfig( - taskId: BackgroundExecutionConfig.wifiHooksTask, - delay: delay * 60 * 1000, - periodic: true, - requiresNetworkConnectivity: true, - startOnBoot: true, - stopOnTerminate: false, - enableHeadless: true, - forceAlarmManager: false, - requiredNetworkType: bgFetch.NetworkType.ANY, - )); - - return scheduleState; - } catch (e) { - return false; - } + return await startPeriodicalBackgroundService(delay, wifiHookTasksTaskId); } bool isTimeReached(DateTime dateTime) { @@ -326,15 +334,27 @@ class DAppHooksUseCase extends ReactiveUseCase { return difference <= 15; } - // This function is called after execusion & for scheduling - Future scheduleAutoClaimTransaction( + Future scheduleTaskForExecution( DateTime dateTime, + Future Function(int delay) startServiceFunction, ) async { final difference = MXCTime.getMinutesDifferenceByDateTime(dateTime); final delay = difference.isNegative ? (24 * 60 + difference) : difference; - return await startAutoClaimService(delay); + return await startServiceFunction(delay); } + // This function is called after execusion & for scheduling + Future scheduleAutoClaimTransaction( + DateTime dateTime, + ) async => + scheduleTaskForExecution(dateTime, startAutoClaimService); + + // This function is called after execusion & for scheduling + Future scheduleBlueberryAutoSyncTransaction( + DateTime dateTime, + ) => + scheduleTaskForExecution(dateTime, startBlueberryAutoSyncService); + Future executeMinerAutoClaim( {required Account account, required List selectedMinerListId, @@ -348,17 +368,57 @@ class DAppHooksUseCase extends ReactiveUseCase { ); } - // delay is in minutes - Future startAutoClaimService(int delay) async { + Future executeBlueberryAutoSync( + {required Account account, + required List selectedMinerListId, + required DateTime minerAutoClaimTime}) async { + await claimMiners( + selectedMinerListId: dappHooksData.value.minerHooks.selectedMiners, + account: account, + minerAutoClaimTime: minerAutoClaimTime, + ); + return await scheduleBlueberryAutoSyncTransaction( + minerAutoClaimTime, + ); + } + + Future startPeriodicalBackgroundService( + int delay, String taskId) async { try { - final result = await AXSBackgroundFetch.startBackgroundProcess( - taskId: minerAutoClaimTaskTaskId); + final result = + await AXSBackgroundFetch.startBackgroundProcess(taskId: taskId); if (!result) return result; final scheduleState = await bgFetch.BackgroundFetch.scheduleTask(bgFetch.TaskConfig( - taskId: BackgroundExecutionConfig.minerAutoClaimTask, + taskId: taskId, + delay: delay * 60 * 1000, + periodic: true, + requiresNetworkConnectivity: true, + startOnBoot: true, + stopOnTerminate: false, + enableHeadless: true, + forceAlarmManager: false, + requiredNetworkType: bgFetch.NetworkType.ANY, + )); + + return scheduleState; + } catch (e) { + return false; + } + } + + Future startOneTimeBackgroundService(int delay, String taskId) async { + try { + final result = + await AXSBackgroundFetch.startBackgroundProcess(taskId: taskId); + + if (!result) return result; + + final scheduleState = + await bgFetch.BackgroundFetch.scheduleTask(bgFetch.TaskConfig( + taskId: taskId, delay: delay * 60 * 1000, periodic: false, requiresNetworkConnectivity: true, @@ -374,6 +434,17 @@ class DAppHooksUseCase extends ReactiveUseCase { return false; } } + + // delay is in minutes + Future startBlueberryAutoSyncService(int delay) async { + return await startOneTimeBackgroundService(delay, blueberryAutoSyncTask); + } + + // delay is in minutes + Future startAutoClaimService(int delay) async { + return await startOneTimeBackgroundService(delay, minerAutoClaimTaskTaskId); + } + Future stopBlueberryAutoSyncService({required bool turnOffAll}) async { return await AXSBackgroundFetch.stopServices( taskId: blueberryAutoSyncTask, turnOffAll: turnOffAll); @@ -401,7 +472,6 @@ class DAppHooksUseCase extends ReactiveUseCase { required Account account, required DateTime minerAutoClaimTime}) async { try { - AXSNotification() .showNotification(cTranslate('auto_claim_started'), null); @@ -437,6 +507,48 @@ class DAppHooksUseCase extends ReactiveUseCase { } } + // List of miners + Future syncBlueberryRing( + {required List selectedMinerListId, + required Account account, + required DateTime minerAutoClaimTime}) async { + try { + AXSNotification().showNotification(cTranslate('auto_sync_started'), null); + + if (selectedMinerListId.isEmpty) { + AXSNotification().showNotification( + cTranslate('no_rings_selected_notification_title'), + cTranslate('no_rings_selected_notification_text'), + ); + } else { + final ableToClaim = await _minerUseCase.claimMinersReward( + selectedMinerListId: selectedMinerListId, + account: account, + showNotification: AXSNotification().showLowPriorityNotification, + translate: cTranslate); + + if (ableToClaim) { + AXSNotification().showNotification( + cTranslate('auto_sync_successful_notification_title'), + cTranslate('auto_sync_successful_notification_text'), + ); + } else { + AXSNotification().showNotification( + cTranslate('already_synced_notification_title'), + cTranslate('already_synced_notification_text'), + ); + } + // Updating now date time + 1 day to set the timer for tomorrow + updateAutoClaimTime(minerAutoClaimTime); + } + } catch (e) { + _errorUseCase.handleBackgroundServiceError( + cTranslate('auto_sync_failed'), + e, + ); + } + } + Future updateAutoClaimTime(DateTime minerAutoClaimTime) async { final now = DateTime.now(); DateTime updatedAutoClaimTime = now.copyWith( diff --git a/lib/features/settings/subfeatures/notifications/domain/background_fetch_config_use_case.dart b/lib/features/settings/subfeatures/notifications/domain/background_fetch_config_use_case.dart index 7fb1fe2f..583dfa92 100644 --- a/lib/features/settings/subfeatures/notifications/domain/background_fetch_config_use_case.dart +++ b/lib/features/settings/subfeatures/notifications/domain/background_fetch_config_use_case.dart @@ -247,6 +247,7 @@ class BackgroundFetchConfigUseCase extends ReactiveUseCase { enableHeadless: true, forceAlarmManager: false, requiredNetworkType: bgFetch.NetworkType.ANY, + )); return scheduleState;