Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions lib/core/sync/sync_coordinator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ class SyncCoordinator {
bool _draining = false;
Future<void>? _activeDrain;
final Map<SyncKind, DateTime> _lastSuccessAt = <SyncKind, DateTime>{};
final StreamController<void> _successfulSyncs =
StreamController<void>.broadcast();

Stream<void> get successfulSyncs => _successfulSyncs.stream;

/// Per-kind waiters. A [sync] call registers a completer for each kind it
/// enqueues (or that is already pending) and awaits exactly those, so it
Expand Down Expand Up @@ -138,6 +142,7 @@ class SyncCoordinator {
final kinds = requested.map((k) => k.name).join(', ');
if (failures.isEmpty) {
log.fine('[SyncCoordinator] sync ok ($kinds) in ${elapsedMs}ms');
if (waits.isNotEmpty) _successfulSyncs.add(null);
return;
}
// One sanitized summary (kind:runtimeType only — no wallet identifiers
Expand Down Expand Up @@ -255,6 +260,7 @@ class SyncCoordinator {

void dispose() {
_lifecycleListener.dispose();
unawaited(_successfulSyncs.close());
}
}

Expand Down
4 changes: 4 additions & 0 deletions lib/core/sync/sync_locator.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'package:bb_mobile/core/sync/sync_coordinator.dart';
import 'package:bb_mobile/core/sync/watch_successful_foreground_syncs_usecase.dart';
import 'package:bb_mobile/core/swaps/domain/usecases/restart_swap_watcher_usecase.dart';
import 'package:bb_mobile/core/wallet/domain/usecases/get_wallets_usecase.dart';
import 'package:bb_mobile/core/wallet/domain/usecases/sync_wallet_usecase.dart';
Expand All @@ -16,5 +17,8 @@ class SyncLocator {
restartSwapWatcherUsecase: locator<RestartSwapWatcherUsecase>(),
),
);
locator.registerFactory<WatchSuccessfulForegroundSyncsUsecase>(
() => WatchSuccessfulForegroundSyncsUsecase(locator<SyncCoordinator>()),
);
}
}
9 changes: 9 additions & 0 deletions lib/core/sync/watch_successful_foreground_syncs_usecase.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import 'package:bb_mobile/core/sync/sync_coordinator.dart';

final class WatchSuccessfulForegroundSyncsUsecase {
final SyncCoordinator _coordinator;

const WatchSuccessfulForegroundSyncsUsecase(this._coordinator);

Stream<void> execute() => _coordinator.successfulSyncs;
}
39 changes: 34 additions & 5 deletions lib/features/backup_settings/backup_settings_locator.dart
Original file line number Diff line number Diff line change
@@ -1,21 +1,50 @@
import 'package:bb_mobile/core/settings/data/settings_repository.dart';
import 'package:bb_mobile/core/settings/domain/repositories/settings_repository.dart';
import 'package:bb_mobile/core/wallet/domain/usecases/get_wallets_usecase.dart';
import 'package:bb_mobile/features/backup_settings/domain/usecases/backup_wallet_now_usecase.dart';
import 'package:bb_mobile/features/backup_settings/domain/usecases/delete_wallet_backup_usecase.dart';
import 'package:bb_mobile/features/backup_settings/domain/usecases/set_wallet_backup_enabled_usecase.dart';
import 'package:bb_mobile/features/backup_settings/domain/usecases/watch_wallet_backup_usecase.dart';
import 'package:bb_mobile/features/backup_settings/presentation/cubit/backup_settings_cubit.dart';
import 'package:bb_mobile/features/backup_settings/presentation/cubit/wallet_backup_settings_cubit.dart';
import 'package:bb_mobile/features/wallet_backup/public/wallet_backup_facade.dart';
import 'package:bb_mobile/features/backup_settings/domain/usecases/backup_wallet_metadata_now_usecase.dart';
import 'package:bb_mobile/features/backup_settings/domain/usecases/delete_remote_wallet_metadata_backup_usecase.dart';
import 'package:bb_mobile/features/backup_settings/domain/usecases/load_backup_settings_usecase.dart';
import 'package:bb_mobile/features/backup_settings/domain/usecases/set_wallet_metadata_backup_enabled_usecase.dart';
import 'package:bb_mobile/features/backup_settings/presentation/cubit/backup_settings_cubit.dart';
import 'package:bb_mobile/features/wallet_metadata_backup/public/wallet_metadata_backup_facade.dart';
import 'package:get_it/get_it.dart';

class BackupSettingsLocator {
static void setup(GetIt locator) {
// Blocs
locator.registerFactory<LoadBackupSettingsUsecase>(
() => LoadBackupSettingsUsecase(
locator<GetWalletsUsecase>(),
locator<SettingsRepository>(),
locator<WalletMetadataBackupFacade>(),
),
);
locator.registerFactory<SetWalletMetadataBackupEnabledUsecase>(
() => SetWalletMetadataBackupEnabledUsecase(
locator<WalletMetadataBackupFacade>(),
),
);
locator.registerFactory<BackupWalletMetadataNowUsecase>(
() =>
BackupWalletMetadataNowUsecase(locator<WalletMetadataBackupFacade>()),
);
locator.registerFactory<DeleteRemoteWalletMetadataBackupUsecase>(
() => DeleteRemoteWalletMetadataBackupUsecase(
locator<WalletMetadataBackupFacade>(),
),
);
locator.registerFactory<BackupSettingsCubit>(
() => BackupSettingsCubit(
getWalletsUsecase: locator<GetWalletsUsecase>(),
settingsRepository: locator<SettingsRepository>(),
loadSettings: locator<LoadBackupSettingsUsecase>(),
setMetadataBackupEnabled:
locator<SetWalletMetadataBackupEnabledUsecase>(),
backupMetadataNow: locator<BackupWalletMetadataNowUsecase>(),
deleteRemoteMetadata:
locator<DeleteRemoteWalletMetadataBackupUsecase>(),
),
);
locator.registerFactory<WatchWalletBackupUsecase>(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
enum WalletMetadataBackupNowStatus { saved, unchanged, notReady }

final class WalletMetadataBackupSettingsSnapshot {
final bool enabled;
final bool dirty;
final bool blocked;
final bool hasRemoteBackup;
final DateTime? lastVerifiedAt;

const WalletMetadataBackupSettingsSnapshot({
required this.enabled,
required this.dirty,
required this.blocked,
this.hasRemoteBackup = false,
this.lastVerifiedAt,
});
}

final class BackupSettingsSnapshot {
final bool isDefaultPhysicalBackupTested;
final DateTime? lastPhysicalBackup;
final bool isDefaultEncryptedBackupTested;
final DateTime? lastEncryptedBackup;
final WalletMetadataBackupSettingsSnapshot walletMetadata;

const BackupSettingsSnapshot({
required this.isDefaultPhysicalBackupTested,
required this.lastPhysicalBackup,
required this.isDefaultEncryptedBackupTested,
required this.lastEncryptedBackup,
required this.walletMetadata,
});
}

final class WalletMetadataBackupNowResult {
final WalletMetadataBackupNowStatus status;
final WalletMetadataBackupSettingsSnapshot settings;

const WalletMetadataBackupNowResult({
required this.status,
required this.settings,
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import 'package:bb_mobile/core/utils/logger.dart';
import 'package:bb_mobile/core/utils/result.dart';
import 'package:bb_mobile/features/backup_settings/domain/backup_settings_failure.dart';
import 'package:bb_mobile/features/backup_settings/domain/entities/backup_settings_snapshot.dart';
import 'package:bb_mobile/features/backup_settings/domain/wallet_metadata_backup_settings_mapping.dart';
import 'package:bb_mobile/features/wallet_metadata_backup/public/wallet_metadata_backup_facade.dart';
import 'package:meta/meta.dart';

class BackupWalletMetadataNowUsecase {
final WalletMetadataBackupFacade _metadataBackup;

const BackupWalletMetadataNowUsecase(this._metadataBackup);

@useResult
Future<Result<WalletMetadataBackupNowResult, BackupSettingsFailure>>
execute() async {
try {
final publicationResult = await _metadataBackup.backupNow();
final WalletMetadataPublishOutcome publication;
switch (publicationResult) {
case Ok(:final value):
publication = value;
case Err(:final failure):
return Err(mapWalletMetadataBackupFailure(failure));
}
final stateResult = await _metadataBackup.getState();
final WalletMetadataBackupState state;
switch (stateResult) {
case Ok(:final value):
state = value;
case Err(:final failure):
return Err(mapWalletMetadataBackupFailure(failure));
}
final status = switch (publication.status) {
WalletMetadataPublishStatus.stored =>
WalletMetadataBackupNowStatus.saved,
WalletMetadataPublishStatus.unchanged ||
WalletMetadataPublishStatus.initialEmpty =>
WalletMetadataBackupNowStatus.unchanged,
WalletMetadataPublishStatus.notReady =>
WalletMetadataBackupNowStatus.notReady,
};
return Ok(
WalletMetadataBackupNowResult(
status: status,
settings: mapWalletMetadataBackupSettings(state),
),
);
} on Exception catch (error, stack) {
log.warning(
'Manual wallet metadata backup failed',
error: StateError(error.runtimeType.toString()),
trace: stack,
);
return Err(BackupSettingsUnexpectedFailure(error.runtimeType.toString()));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import 'package:bb_mobile/core/utils/result.dart';
import 'package:bb_mobile/features/backup_settings/domain/backup_settings_failure.dart';
import 'package:bb_mobile/features/backup_settings/domain/entities/backup_settings_snapshot.dart';
import 'package:bb_mobile/features/backup_settings/domain/wallet_metadata_backup_settings_mapping.dart';
import 'package:bb_mobile/features/wallet_metadata_backup/public/wallet_metadata_backup_facade.dart';
import 'package:meta/meta.dart';

class DeleteRemoteWalletMetadataBackupUsecase {
final WalletMetadataBackupFacade _metadataBackup;

const DeleteRemoteWalletMetadataBackupUsecase(this._metadataBackup);

@useResult
Future<Result<WalletMetadataBackupSettingsSnapshot, BackupSettingsFailure>>
execute() async {
final deleted = await _metadataBackup.deleteRemoteBackup();
if (deleted case Err(:final failure)) {
return Err(mapWalletMetadataBackupFailure(failure));
}
final state = await _metadataBackup.getState();
return switch (state) {
Ok(:final value) => Ok(mapWalletMetadataBackupSettings(value)),
Err(:final failure) => Err(mapWalletMetadataBackupFailure(failure)),
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import 'package:bb_mobile/core/settings/domain/repositories/settings_repository.dart';
import 'package:bb_mobile/core/utils/logger.dart';
import 'package:bb_mobile/core/utils/result.dart';
import 'package:bb_mobile/core/wallet/domain/entities/wallet.dart';
import 'package:bb_mobile/core/wallet/domain/usecases/get_wallets_usecase.dart';
import 'package:bb_mobile/features/backup_settings/domain/backup_settings_failure.dart';
import 'package:bb_mobile/features/backup_settings/domain/entities/backup_settings_snapshot.dart';
import 'package:bb_mobile/features/backup_settings/domain/wallet_metadata_backup_settings_mapping.dart';
import 'package:bb_mobile/features/wallet_metadata_backup/public/wallet_metadata_backup_facade.dart';
import 'package:meta/meta.dart';

class LoadBackupSettingsUsecase {
final GetWalletsUsecase _getWallets;
final SettingsRepository _settingsRepository;
final WalletMetadataBackupFacade _metadataBackup;

const LoadBackupSettingsUsecase(
this._getWallets,
this._settingsRepository,
this._metadataBackup,
);

@useResult
Future<Result<BackupSettingsSnapshot, BackupSettingsFailure>>
execute() async {
try {
final metadataResult = await _metadataBackup.getState();
final WalletMetadataBackupState metadataState;
switch (metadataResult) {
case Ok(:final value):
metadataState = value;
case Err(:final failure):
return Err(mapWalletMetadataBackupFailure(failure));
}

final List<Wallet> defaultWallets;
try {
defaultWallets = await _getWallets.execute(onlyDefaults: true);
} on NoWalletsFoundException {
return Ok(_emptyWalletSnapshot(metadataState));
}
if (defaultWallets.isEmpty) {
return Ok(_emptyWalletSnapshot(metadataState));
}

final settings = await _settingsRepository.fetch();
final network = Network.fromEnvironment(
isTestnet: settings.environment.isTestnet,
isLiquid: false,
);
final networkWallet = defaultWallets
.where((wallet) => wallet.network == network)
.firstOrNull;
return Ok(
BackupSettingsSnapshot(
isDefaultPhysicalBackupTested: defaultWallets.every(
(wallet) => wallet.isPhysicalBackupTested,
),
lastPhysicalBackup: networkWallet?.latestPhysicalBackup,
isDefaultEncryptedBackupTested: defaultWallets.every(
(wallet) => wallet.isEncryptedVaultTested,
),
lastEncryptedBackup: networkWallet?.latestEncryptedBackup,
walletMetadata: mapWalletMetadataBackupSettings(metadataState),
),
);
} on Exception catch (error, stack) {
log.warning(
'Load backup settings failed',
error: StateError(error.runtimeType.toString()),
trace: stack,
);
return Err(BackupSettingsUnexpectedFailure(error.runtimeType.toString()));
}
}

BackupSettingsSnapshot _emptyWalletSnapshot(
WalletMetadataBackupState metadataState,
) {
return BackupSettingsSnapshot(
isDefaultPhysicalBackupTested: false,
lastPhysicalBackup: null,
isDefaultEncryptedBackupTested: false,
lastEncryptedBackup: null,
walletMetadata: mapWalletMetadataBackupSettings(metadataState),
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import 'package:bb_mobile/core/utils/logger.dart';
import 'package:bb_mobile/core/utils/result.dart';
import 'package:bb_mobile/features/backup_settings/domain/backup_settings_failure.dart';
import 'package:bb_mobile/features/backup_settings/domain/entities/backup_settings_snapshot.dart';
import 'package:bb_mobile/features/backup_settings/domain/wallet_metadata_backup_settings_mapping.dart';
import 'package:bb_mobile/features/wallet_metadata_backup/public/wallet_metadata_backup_facade.dart';
import 'package:meta/meta.dart';

class SetWalletMetadataBackupEnabledUsecase {
final WalletMetadataBackupFacade _metadataBackup;

const SetWalletMetadataBackupEnabledUsecase(this._metadataBackup);

@useResult
Future<Result<WalletMetadataBackupSettingsSnapshot, BackupSettingsFailure>>
execute({required bool enabled, required bool disclosureAccepted}) async {
try {
final currentResult = await _metadataBackup.getState();
final WalletMetadataBackupState current;
switch (currentResult) {
case Ok(:final value):
current = value;
case Err(:final failure):
return Err(mapWalletMetadataBackupFailure(failure));
}

if (enabled && !disclosureAccepted) {
return Ok(mapWalletMetadataBackupSettings(current));
}

final result = await _metadataBackup.setEnabled(enabled);
return switch (result) {
Ok(:final value) => Ok(mapWalletMetadataBackupSettings(value)),
Err(:final failure) => Err(mapWalletMetadataBackupFailure(failure)),
};
} on Exception catch (error, stack) {
log.warning(
'Set wallet metadata backup failed',
error: StateError(error.runtimeType.toString()),
trace: stack,
);
return Err(BackupSettingsUnexpectedFailure(error.runtimeType.toString()));
}
}
}
Loading