From 48b069869ef5297bfa3a605994ed00dace54e283 Mon Sep 17 00:00:00 2001 From: ethicnology Date: Tue, 19 Nov 2024 08:43:22 -0500 Subject: [PATCH 001/401] feat: encrypted wallet backup --- lib/_model/backup.dart | 26 +++ lib/_model/backup.freezed.dart | 286 ++++++++++++++++++++++++ lib/_model/backup.g.dart | 36 +++ lib/_model/bip329_label.dart | 7 +- lib/_pkg/crypto.dart | 52 +++++ lib/backup/bloc/cubit.dart | 100 +++++++++ lib/backup/bloc/state.dart | 8 + lib/backup/page.dart | 149 ++++++++++++ lib/routes.dart | 8 + lib/settings/bitcoin_settings_page.dart | 2 + lib/settings/settings_page.dart | 22 ++ 11 files changed, 691 insertions(+), 5 deletions(-) create mode 100644 lib/_model/backup.dart create mode 100644 lib/_model/backup.freezed.dart create mode 100644 lib/_model/backup.g.dart create mode 100644 lib/_pkg/crypto.dart create mode 100644 lib/backup/bloc/cubit.dart create mode 100644 lib/backup/bloc/state.dart create mode 100644 lib/backup/page.dart diff --git a/lib/_model/backup.dart b/lib/_model/backup.dart new file mode 100644 index 000000000..4799837d3 --- /dev/null +++ b/lib/_model/backup.dart @@ -0,0 +1,26 @@ +import 'package:bb_mobile/_model/bip329_label.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'backup.freezed.dart'; +part 'backup.g.dart'; + +@freezed +class Backup with _$Backup { + const factory Backup({ + @Default(1) int version, + @Default([]) List mnemonic, + @Default([]) List passphrases, + @Default([]) List labels, + @Default([]) List descriptors, + }) = _Backup; + + factory Backup.fromJson(Map json) => _$BackupFromJson(json); + + const Backup._(); + + bool get isEmpty => + mnemonic.isEmpty && + passphrases.isEmpty && + labels.isEmpty && + descriptors.isEmpty; +} diff --git a/lib/_model/backup.freezed.dart b/lib/_model/backup.freezed.dart new file mode 100644 index 000000000..cf8e3cea7 --- /dev/null +++ b/lib/_model/backup.freezed.dart @@ -0,0 +1,286 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'backup.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +Backup _$BackupFromJson(Map json) { + return _Backup.fromJson(json); +} + +/// @nodoc +mixin _$Backup { + int get version => throw _privateConstructorUsedError; + List get mnemonic => throw _privateConstructorUsedError; + List get passphrases => throw _privateConstructorUsedError; + List get labels => throw _privateConstructorUsedError; + List get descriptors => throw _privateConstructorUsedError; + + /// Serializes this Backup to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of Backup + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $BackupCopyWith get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $BackupCopyWith<$Res> { + factory $BackupCopyWith(Backup value, $Res Function(Backup) then) = + _$BackupCopyWithImpl<$Res, Backup>; + @useResult + $Res call( + {int version, + List mnemonic, + List passphrases, + List labels, + List descriptors}); +} + +/// @nodoc +class _$BackupCopyWithImpl<$Res, $Val extends Backup> + implements $BackupCopyWith<$Res> { + _$BackupCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of Backup + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? version = null, + Object? mnemonic = null, + Object? passphrases = null, + Object? labels = null, + Object? descriptors = null, + }) { + return _then(_value.copyWith( + version: null == version + ? _value.version + : version // ignore: cast_nullable_to_non_nullable + as int, + mnemonic: null == mnemonic + ? _value.mnemonic + : mnemonic // ignore: cast_nullable_to_non_nullable + as List, + passphrases: null == passphrases + ? _value.passphrases + : passphrases // ignore: cast_nullable_to_non_nullable + as List, + labels: null == labels + ? _value.labels + : labels // ignore: cast_nullable_to_non_nullable + as List, + descriptors: null == descriptors + ? _value.descriptors + : descriptors // ignore: cast_nullable_to_non_nullable + as List, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$BackupImplCopyWith<$Res> implements $BackupCopyWith<$Res> { + factory _$$BackupImplCopyWith( + _$BackupImpl value, $Res Function(_$BackupImpl) then) = + __$$BackupImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {int version, + List mnemonic, + List passphrases, + List labels, + List descriptors}); +} + +/// @nodoc +class __$$BackupImplCopyWithImpl<$Res> + extends _$BackupCopyWithImpl<$Res, _$BackupImpl> + implements _$$BackupImplCopyWith<$Res> { + __$$BackupImplCopyWithImpl( + _$BackupImpl _value, $Res Function(_$BackupImpl) _then) + : super(_value, _then); + + /// Create a copy of Backup + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? version = null, + Object? mnemonic = null, + Object? passphrases = null, + Object? labels = null, + Object? descriptors = null, + }) { + return _then(_$BackupImpl( + version: null == version + ? _value.version + : version // ignore: cast_nullable_to_non_nullable + as int, + mnemonic: null == mnemonic + ? _value._mnemonic + : mnemonic // ignore: cast_nullable_to_non_nullable + as List, + passphrases: null == passphrases + ? _value._passphrases + : passphrases // ignore: cast_nullable_to_non_nullable + as List, + labels: null == labels + ? _value._labels + : labels // ignore: cast_nullable_to_non_nullable + as List, + descriptors: null == descriptors + ? _value._descriptors + : descriptors // ignore: cast_nullable_to_non_nullable + as List, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$BackupImpl extends _Backup { + const _$BackupImpl( + {this.version = 1, + final List mnemonic = const [], + final List passphrases = const [], + final List labels = const [], + final List descriptors = const []}) + : _mnemonic = mnemonic, + _passphrases = passphrases, + _labels = labels, + _descriptors = descriptors, + super._(); + + factory _$BackupImpl.fromJson(Map json) => + _$$BackupImplFromJson(json); + + @override + @JsonKey() + final int version; + final List _mnemonic; + @override + @JsonKey() + List get mnemonic { + if (_mnemonic is EqualUnmodifiableListView) return _mnemonic; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_mnemonic); + } + + final List _passphrases; + @override + @JsonKey() + List get passphrases { + if (_passphrases is EqualUnmodifiableListView) return _passphrases; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_passphrases); + } + + final List _labels; + @override + @JsonKey() + List get labels { + if (_labels is EqualUnmodifiableListView) return _labels; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_labels); + } + + final List _descriptors; + @override + @JsonKey() + List get descriptors { + if (_descriptors is EqualUnmodifiableListView) return _descriptors; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_descriptors); + } + + @override + String toString() { + return 'Backup(version: $version, mnemonic: $mnemonic, passphrases: $passphrases, labels: $labels, descriptors: $descriptors)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$BackupImpl && + (identical(other.version, version) || other.version == version) && + const DeepCollectionEquality().equals(other._mnemonic, _mnemonic) && + const DeepCollectionEquality() + .equals(other._passphrases, _passphrases) && + const DeepCollectionEquality().equals(other._labels, _labels) && + const DeepCollectionEquality() + .equals(other._descriptors, _descriptors)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + version, + const DeepCollectionEquality().hash(_mnemonic), + const DeepCollectionEquality().hash(_passphrases), + const DeepCollectionEquality().hash(_labels), + const DeepCollectionEquality().hash(_descriptors)); + + /// Create a copy of Backup + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$BackupImplCopyWith<_$BackupImpl> get copyWith => + __$$BackupImplCopyWithImpl<_$BackupImpl>(this, _$identity); + + @override + Map toJson() { + return _$$BackupImplToJson( + this, + ); + } +} + +abstract class _Backup extends Backup { + const factory _Backup( + {final int version, + final List mnemonic, + final List passphrases, + final List labels, + final List descriptors}) = _$BackupImpl; + const _Backup._() : super._(); + + factory _Backup.fromJson(Map json) = _$BackupImpl.fromJson; + + @override + int get version; + @override + List get mnemonic; + @override + List get passphrases; + @override + List get labels; + @override + List get descriptors; + + /// Create a copy of Backup + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$BackupImplCopyWith<_$BackupImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/_model/backup.g.dart b/lib/_model/backup.g.dart new file mode 100644 index 000000000..d6e23d062 --- /dev/null +++ b/lib/_model/backup.g.dart @@ -0,0 +1,36 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'backup.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$BackupImpl _$$BackupImplFromJson(Map json) => _$BackupImpl( + version: (json['version'] as num?)?.toInt() ?? 1, + mnemonic: (json['mnemonic'] as List?) + ?.map((e) => e as String) + .toList() ?? + const [], + passphrases: (json['passphrases'] as List?) + ?.map((e) => e as String) + .toList() ?? + const [], + labels: (json['labels'] as List?) + ?.map((e) => Bip329Label.fromJson(e as Map)) + .toList() ?? + const [], + descriptors: (json['descriptors'] as List?) + ?.map((e) => e as String) + .toList() ?? + const [], + ); + +Map _$$BackupImplToJson(_$BackupImpl instance) => + { + 'version': instance.version, + 'mnemonic': instance.mnemonic, + 'passphrases': instance.passphrases, + 'labels': instance.labels, + 'descriptors': instance.descriptors, + }; diff --git a/lib/_model/bip329_label.dart b/lib/_model/bip329_label.dart index dfa549295..c73e91e58 100644 --- a/lib/_model/bip329_label.dart +++ b/lib/_model/bip329_label.dart @@ -2,14 +2,11 @@ import 'dart:convert'; import 'dart:io'; -import 'dart:math'; -import 'dart:typed_data'; +import 'package:bb_mobile/_pkg/crypto.dart'; import 'package:bb_mobile/_pkg/error.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:path_provider/path_provider.dart'; -import 'package:pointycastle/api.dart'; -import 'package:pointycastle/export.dart' as pc; part 'bip329_label.freezed.dart'; part 'bip329_label.g.dart'; @@ -72,7 +69,7 @@ extension Bip329LabelHelpers on Bip329Label { ); } final encryptedContents = await file.readAsString(); - final decryptedContents = _decrypt(encryptedContents, key); + final decryptedContents = Crypto.aesDecrypt(encryptedContents, key); final lines = LineSplitter.split(decryptedContents); return ( lines diff --git a/lib/_pkg/crypto.dart b/lib/_pkg/crypto.dart new file mode 100644 index 000000000..b15370a68 --- /dev/null +++ b/lib/_pkg/crypto.dart @@ -0,0 +1,52 @@ +import 'dart:convert'; +import 'dart:math'; +import 'dart:typed_data'; + +import 'package:hex/hex.dart'; +import 'package:pointycastle/api.dart'; +import 'package:pointycastle/export.dart' as pc; + +class Crypto { + static String aesEncrypt(String plainText, String key) { + final keyBytes = Uint8List.fromList(HEX.decode(key)); + final iv = generateRandomBytes(16); + final params = PaddedBlockCipherParameters( + ParametersWithIV(KeyParameter(keyBytes), iv), + null, + ); + final paddedBlockCipher = pc.PaddedBlockCipher('AES/CBC/PKCS7') + ..init(true, params); + + final input = Uint8List.fromList(utf8.encode(plainText)); + final encrypted = paddedBlockCipher.process(input); + + return base64Encode(iv) + ',' + base64Encode(encrypted); + } + + static String aesDecrypt(String encryptedBase64Text, String key) { + final keyBytes = Uint8List.fromList(HEX.decode(key)); + + final parts = encryptedBase64Text.split(','); + final iv = base64Decode(parts[0]); + final encrypted = base64Decode(parts[1]); + final params = PaddedBlockCipherParameters( + ParametersWithIV(KeyParameter(keyBytes), iv), + null, + ); + final paddedBlockCipher = pc.PaddedBlockCipher('AES/CBC/PKCS7') + ..init(false, params); + + final decrypted = paddedBlockCipher.process(encrypted); + + return utf8.decode(decrypted); + } + + static Uint8List generateRandomBytes(int length) { + final secureRandom = Random.secure(); + final randomIV = Uint8List(length); + for (int i = 0; i < length; i++) { + randomIV[i] = secureRandom.nextInt(256); + } + return randomIV; + } +} diff --git a/lib/backup/bloc/cubit.dart b/lib/backup/bloc/cubit.dart new file mode 100644 index 000000000..9a596e856 --- /dev/null +++ b/lib/backup/bloc/cubit.dart @@ -0,0 +1,100 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:bb_mobile/_model/backup.dart'; +import 'package:bb_mobile/_model/seed.dart'; +import 'package:bb_mobile/_pkg/crypto.dart'; +import 'package:bb_mobile/_pkg/error.dart'; +import 'package:bb_mobile/_pkg/file_storage.dart'; +import 'package:bb_mobile/_pkg/wallet/labels.dart'; +import 'package:bb_mobile/_pkg/wallet/repository/sensitive_storage.dart'; +import 'package:bb_mobile/backup/bloc/state.dart'; +import 'package:bb_mobile/wallet/bloc/wallet_bloc.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:hex/hex.dart'; +import 'package:intl/intl.dart'; + +class BackupCubit extends Cubit { + BackupCubit({ + required WalletBloc walletBloc, + required WalletSensitiveStorageRepository walletSensitiveStorage, + required FileStorage fileStorage, + }) : _walletBloc = walletBloc, + _walletSensitiveStorage = walletSensitiveStorage, + _fileStorage = fileStorage, + super(BackupState(backup: const Backup())); + + final FileStorage _fileStorage; + final WalletBloc _walletBloc; + final WalletSensitiveStorageRepository _walletSensitiveStorage; + + Future loadBackupData() async { + emit(BackupState(loading: true, backup: const Backup())); + + final wallet = _walletBloc.state.wallet!; + final (seed, error) = await _walletSensitiveStorage.readSeed( + fingerprintIndex: wallet.getRelatedSeedStorageString(), + ); + final mnemonic = seed?.mnemonic.split(' ') ?? []; + + final passphrases = []; + for (final Passphrase passphrase in seed?.passphrases ?? []) { + passphrases.add(passphrase.passphrase); + } + + final descriptors = [wallet.getDescriptorCombined()]; + + final walletLabels = WalletLabels(); + final labels = await walletLabels.txsToBip329( + wallet.transactions, + wallet.originString(), + ) + ..addAll( + await walletLabels.addressesToBip329( + wallet.myAddressBook, + wallet.originString(), + ), + ); + + final backup = Backup( + mnemonic: mnemonic, + passphrases: passphrases, + descriptors: descriptors, + labels: labels, + ); + + emit(BackupState(backup: backup)); + return backup; + } + + Future<(String?, Err?)> writeEncryptedBackup({ + required bool hasMnemonic, + required bool hasPassphrases, + required bool hasDescriptors, + }) async { + var backup = state.backup; + if (!hasMnemonic) backup = backup.copyWith(mnemonic: []); + if (!hasPassphrases) backup = backup.copyWith(passphrases: []); + if (!hasDescriptors) backup = backup.copyWith(descriptors: []); + if (backup.isEmpty) return (null, Err('Empty backup')); + + final secret = HEX.encode(Crypto.generateRandomBytes(32)); + final plaintext = json.encode(backup.toJson()); + final ciphertext = Crypto.aesEncrypt(plaintext, secret); + + final now = DateTime.now(); + final formattedDate = DateFormat('yyyy-MM-dd_HH-mm-ss').format(now); + final filename = '${formattedDate}_backup.txt'; + + final (directory, errDir) = await _fileStorage.getDownloadDirectory(); + if (errDir != null) return (null, Err('Fail to get Download directory')); + final file = File(directory! + '/' + filename); + + final (f, errSave) = await _fileStorage.saveToFile(file, ciphertext); + if (errSave != null) return (null, Err('Fail to save backup')); + + print(f?.path); + + return (secret, null); + } +} diff --git a/lib/backup/bloc/state.dart b/lib/backup/bloc/state.dart new file mode 100644 index 000000000..0ebaa596d --- /dev/null +++ b/lib/backup/bloc/state.dart @@ -0,0 +1,8 @@ +import 'package:bb_mobile/_model/backup.dart'; + +class BackupState { + BackupState({this.loading = false, required this.backup}); + + final bool loading; + final Backup backup; +} diff --git a/lib/backup/page.dart b/lib/backup/page.dart new file mode 100644 index 000000000..356ecf2b4 --- /dev/null +++ b/lib/backup/page.dart @@ -0,0 +1,149 @@ +import 'package:bb_mobile/_model/backup.dart'; +import 'package:bb_mobile/_pkg/file_storage.dart'; +import 'package:bb_mobile/_pkg/wallet/repository/sensitive_storage.dart'; +import 'package:bb_mobile/_ui/app_bar.dart'; +import 'package:bb_mobile/_ui/components/text.dart'; +import 'package:bb_mobile/_ui/toast.dart'; +import 'package:bb_mobile/backup/bloc/cubit.dart'; +import 'package:bb_mobile/locator.dart'; +import 'package:bb_mobile/wallet/bloc/wallet_bloc.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; + +class TheBackupPage extends StatefulWidget { + const TheBackupPage({ + super.key, + required this.walletBloc, + }); + + final WalletBloc walletBloc; + + @override + _TheBackupPageState createState() => _TheBackupPageState(); +} + +class _TheBackupPageState extends State { + bool hasMnemonic = false; + bool hasPassphrases = false; + bool hasDescriptors = false; + String backupKey = ''; + + @override + Widget build(BuildContext context) { + final backupCubit = BackupCubit( + walletBloc: widget.walletBloc, + walletSensitiveStorage: locator(), + fileStorage: locator(), + ); + + return MultiBlocProvider( + providers: [ + BlocProvider.value(value: widget.walletBloc), + BlocProvider.value(value: backupCubit), + ], + child: Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + flexibleSpace: BBAppBar( + text: 'Backup', + onBack: () => context.pop(), + ), + ), + body: FutureBuilder( + future: backupCubit.loadBackupData(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } else if (snapshot.hasError) { + return Center(child: Text('Error: ${snapshot.error}')); + } else { + final backup = snapshot.data!; + + return Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (backup.mnemonic.isNotEmpty && backupKey.isEmpty) + CheckboxListTile( + title: const BBText.body('Mnemonic'), + value: hasMnemonic, + tileColor: Colors.amber, + onChanged: (value) { + setState(() => hasMnemonic = value ?? false); + }, + ), + if (backup.passphrases.isNotEmpty && backupKey.isEmpty) + CheckboxListTile( + title: const BBText.body('Passphrases'), + value: hasPassphrases, + tileColor: Colors.amber, + onChanged: (value) { + setState(() => hasPassphrases = value ?? false); + }, + ), + if (backup.descriptors.isNotEmpty && backupKey.isEmpty) + CheckboxListTile( + title: const BBText.body('Descriptors'), + value: hasDescriptors, + tileColor: Colors.amber, + onChanged: (value) { + setState(() => hasDescriptors = value ?? false); + }, + ), + const SizedBox(height: 20), + if (backupKey.isNotEmpty) + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.amber, width: 2), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Backup Key:', + style: TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 10), + SelectableText( + backupKey, + style: + const TextStyle(fontWeight: FontWeight.bold), + ), + ], + ), + ), + const SizedBox(height: 20), + if (backupKey.isEmpty) + ElevatedButton( + onPressed: () async { + final (secret, error) = + await backupCubit.writeEncryptedBackup( + hasMnemonic: hasMnemonic, + hasPassphrases: hasPassphrases, + hasDescriptors: hasDescriptors, + ); + if (error != null) { + ScaffoldMessenger.of(context).showSnackBar( + context.showToast(error.toString()), + ); + } + + if (secret != null) backupKey = secret; + setState(() {}); + }, + child: const Text('Encrypt'), + ), + ], + ), + ); + } + }, + ), + ), + ); + } +} diff --git a/lib/routes.dart b/lib/routes.dart index 6bfd08adb..fefe1b4e8 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -4,6 +4,7 @@ import 'package:bb_mobile/_model/swap.dart'; import 'package:bb_mobile/_model/transaction.dart'; import 'package:bb_mobile/_ui/logger_page.dart'; import 'package:bb_mobile/auth/page.dart'; +import 'package:bb_mobile/backup/page.dart'; import 'package:bb_mobile/create/page.dart'; import 'package:bb_mobile/home/home_page.dart'; import 'package:bb_mobile/home/market.dart'; @@ -194,6 +195,13 @@ GoRouter setupRouter() => GoRouter( return WalletSettingsPage(wallet: wallet); }, ), + GoRoute( + path: '/backup', + builder: (context, state) { + final walletBloc = state.extra! as WalletBloc; + return TheBackupPage(walletBloc: walletBloc); + }, + ), // GoRoute( // path: '/wallet-settings/open-test-backup', // builder: (context, state) { diff --git a/lib/settings/bitcoin_settings_page.dart b/lib/settings/bitcoin_settings_page.dart index 9fb76082f..41b97dbc5 100644 --- a/lib/settings/bitcoin_settings_page.dart +++ b/lib/settings/bitcoin_settings_page.dart @@ -73,6 +73,8 @@ class _Screen extends StatelessWidget { HardwareButton(), Gap(8), SwapHistoryButton(), + Gap(8), + BackupButton(), // SearchAddressButton(), // Gap(8), diff --git a/lib/settings/settings_page.dart b/lib/settings/settings_page.dart index 1dae6f00b..7bf015600 100644 --- a/lib/settings/settings_page.dart +++ b/lib/settings/settings_page.dart @@ -3,7 +3,9 @@ import 'package:bb_mobile/_pkg/launcher.dart'; import 'package:bb_mobile/_ui/app_bar.dart'; import 'package:bb_mobile/_ui/components/button.dart'; import 'package:bb_mobile/_ui/components/text.dart'; +import 'package:bb_mobile/home/bloc/home_cubit.dart'; import 'package:bb_mobile/locator.dart'; +import 'package:bb_mobile/network/bloc/network_cubit.dart'; import 'package:bb_mobile/settings/bloc/settings_cubit.dart'; import 'package:bb_mobile/styles.dart'; import 'package:flutter/material.dart'; @@ -157,6 +159,26 @@ class SwapHistoryButton extends StatelessWidget { } } +class BackupButton extends StatelessWidget { + const BackupButton({super.key}); + + @override + Widget build(BuildContext context) { + return BBButton.textWithStatusAndRightArrow( + label: 'Backup', + onPressed: () { + final network = context.read().state.getBBNetwork(); + final walletBlocs = + context.read().state.walletBlocsFromNetwork(network); + context.push( + '/backup', + extra: walletBlocs.first, + ); + }, + ); + } +} + class ApplicationSettingsButton extends StatelessWidget { const ApplicationSettingsButton({super.key}); From 99d065c5acf363d1a19a1d59b383edf0ab889d0a Mon Sep 17 00:00:00 2001 From: ethicnology Date: Tue, 19 Nov 2024 09:01:30 -0500 Subject: [PATCH 002/401] feat: backup multi wallet --- lib/backup/bloc/cubit.dart | 91 ++++++++++----------- lib/backup/bloc/state.dart | 4 +- lib/backup/page.dart | 53 ++---------- lib/routes.dart | 4 +- lib/settings/bitcoin_settings_page.dart | 2 - lib/settings/core_wallet_settings_page.dart | 23 +++++- lib/settings/settings_page.dart | 22 ----- 7 files changed, 75 insertions(+), 124 deletions(-) diff --git a/lib/backup/bloc/cubit.dart b/lib/backup/bloc/cubit.dart index 9a596e856..6bafc5bd9 100644 --- a/lib/backup/bloc/cubit.dart +++ b/lib/backup/bloc/cubit.dart @@ -16,70 +16,69 @@ import 'package:intl/intl.dart'; class BackupCubit extends Cubit { BackupCubit({ - required WalletBloc walletBloc, + required List wallets, required WalletSensitiveStorageRepository walletSensitiveStorage, required FileStorage fileStorage, - }) : _walletBloc = walletBloc, + }) : _wallets = wallets, _walletSensitiveStorage = walletSensitiveStorage, _fileStorage = fileStorage, - super(BackupState(backup: const Backup())); + super(BackupState(backups: [])); final FileStorage _fileStorage; - final WalletBloc _walletBloc; + final List _wallets; final WalletSensitiveStorageRepository _walletSensitiveStorage; - Future loadBackupData() async { - emit(BackupState(loading: true, backup: const Backup())); + Future> loadBackupData() async { + emit(BackupState(loading: true, backups: [])); - final wallet = _walletBloc.state.wallet!; - final (seed, error) = await _walletSensitiveStorage.readSeed( - fingerprintIndex: wallet.getRelatedSeedStorageString(), - ); - final mnemonic = seed?.mnemonic.split(' ') ?? []; + final backups = []; - final passphrases = []; - for (final Passphrase passphrase in seed?.passphrases ?? []) { - passphrases.add(passphrase.passphrase); - } + for (final walletBloc in _wallets) { + final wallet = walletBloc.state.wallet!; - final descriptors = [wallet.getDescriptorCombined()]; - - final walletLabels = WalletLabels(); - final labels = await walletLabels.txsToBip329( - wallet.transactions, - wallet.originString(), - ) - ..addAll( - await walletLabels.addressesToBip329( - wallet.myAddressBook, - wallet.originString(), + final (seed, error) = await _walletSensitiveStorage.readSeed( + fingerprintIndex: wallet.getRelatedSeedStorageString(), + ); + final mnemonic = seed?.mnemonic.split(' ') ?? []; + + final passphrases = []; + for (final Passphrase passphrase in seed?.passphrases ?? []) { + passphrases.add(passphrase.passphrase); + } + + final descriptors = [wallet.getDescriptorCombined()]; + + final walletLabels = WalletLabels(); + final labels = await walletLabels.txsToBip329( + wallet.transactions, + wallet.originString(), + ) + ..addAll( + await walletLabels.addressesToBip329( + wallet.myAddressBook, + wallet.originString(), + ), + ); + + backups.add( + Backup( + mnemonic: mnemonic, + passphrases: passphrases, + descriptors: descriptors, + labels: labels, ), ); + } - final backup = Backup( - mnemonic: mnemonic, - passphrases: passphrases, - descriptors: descriptors, - labels: labels, - ); - - emit(BackupState(backup: backup)); - return backup; + emit(BackupState(backups: backups)); + return backups; } - Future<(String?, Err?)> writeEncryptedBackup({ - required bool hasMnemonic, - required bool hasPassphrases, - required bool hasDescriptors, - }) async { - var backup = state.backup; - if (!hasMnemonic) backup = backup.copyWith(mnemonic: []); - if (!hasPassphrases) backup = backup.copyWith(passphrases: []); - if (!hasDescriptors) backup = backup.copyWith(descriptors: []); - if (backup.isEmpty) return (null, Err('Empty backup')); + Future<(String?, Err?)> writeEncryptedBackup() async { + final backups = state.backups; final secret = HEX.encode(Crypto.generateRandomBytes(32)); - final plaintext = json.encode(backup.toJson()); + final plaintext = json.encode(backups.map((i) => i.toJson()).toList()); final ciphertext = Crypto.aesEncrypt(plaintext, secret); final now = DateTime.now(); diff --git a/lib/backup/bloc/state.dart b/lib/backup/bloc/state.dart index 0ebaa596d..42178650f 100644 --- a/lib/backup/bloc/state.dart +++ b/lib/backup/bloc/state.dart @@ -1,8 +1,8 @@ import 'package:bb_mobile/_model/backup.dart'; class BackupState { - BackupState({this.loading = false, required this.backup}); + BackupState({this.loading = false, required this.backups}); final bool loading; - final Backup backup; + final List backups; } diff --git a/lib/backup/page.dart b/lib/backup/page.dart index 356ecf2b4..6921d27a4 100644 --- a/lib/backup/page.dart +++ b/lib/backup/page.dart @@ -2,7 +2,6 @@ import 'package:bb_mobile/_model/backup.dart'; import 'package:bb_mobile/_pkg/file_storage.dart'; import 'package:bb_mobile/_pkg/wallet/repository/sensitive_storage.dart'; import 'package:bb_mobile/_ui/app_bar.dart'; -import 'package:bb_mobile/_ui/components/text.dart'; import 'package:bb_mobile/_ui/toast.dart'; import 'package:bb_mobile/backup/bloc/cubit.dart'; import 'package:bb_mobile/locator.dart'; @@ -12,12 +11,9 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; class TheBackupPage extends StatefulWidget { - const TheBackupPage({ - super.key, - required this.walletBloc, - }); + const TheBackupPage({super.key, required this.wallets}); - final WalletBloc walletBloc; + final List wallets; @override _TheBackupPageState createState() => _TheBackupPageState(); @@ -32,16 +28,13 @@ class _TheBackupPageState extends State { @override Widget build(BuildContext context) { final backupCubit = BackupCubit( - walletBloc: widget.walletBloc, + wallets: widget.wallets, walletSensitiveStorage: locator(), fileStorage: locator(), ); return MultiBlocProvider( - providers: [ - BlocProvider.value(value: widget.walletBloc), - BlocProvider.value(value: backupCubit), - ], + providers: [BlocProvider.value(value: backupCubit)], child: Scaffold( appBar: AppBar( automaticallyImplyLeading: false, @@ -50,7 +43,7 @@ class _TheBackupPageState extends State { onBack: () => context.pop(), ), ), - body: FutureBuilder( + body: FutureBuilder>( future: backupCubit.loadBackupData(), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { @@ -65,34 +58,6 @@ class _TheBackupPageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (backup.mnemonic.isNotEmpty && backupKey.isEmpty) - CheckboxListTile( - title: const BBText.body('Mnemonic'), - value: hasMnemonic, - tileColor: Colors.amber, - onChanged: (value) { - setState(() => hasMnemonic = value ?? false); - }, - ), - if (backup.passphrases.isNotEmpty && backupKey.isEmpty) - CheckboxListTile( - title: const BBText.body('Passphrases'), - value: hasPassphrases, - tileColor: Colors.amber, - onChanged: (value) { - setState(() => hasPassphrases = value ?? false); - }, - ), - if (backup.descriptors.isNotEmpty && backupKey.isEmpty) - CheckboxListTile( - title: const BBText.body('Descriptors'), - value: hasDescriptors, - tileColor: Colors.amber, - onChanged: (value) { - setState(() => hasDescriptors = value ?? false); - }, - ), - const SizedBox(height: 20), if (backupKey.isNotEmpty) Container( padding: const EdgeInsets.all(16), @@ -121,11 +86,7 @@ class _TheBackupPageState extends State { ElevatedButton( onPressed: () async { final (secret, error) = - await backupCubit.writeEncryptedBackup( - hasMnemonic: hasMnemonic, - hasPassphrases: hasPassphrases, - hasDescriptors: hasDescriptors, - ); + await backupCubit.writeEncryptedBackup(); if (error != null) { ScaffoldMessenger.of(context).showSnackBar( context.showToast(error.toString()), @@ -135,7 +96,7 @@ class _TheBackupPageState extends State { if (secret != null) backupKey = secret; setState(() {}); }, - child: const Text('Encrypt'), + child: const Text('Backup'), ), ], ), diff --git a/lib/routes.dart b/lib/routes.dart index fefe1b4e8..0123f2cf9 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -198,8 +198,8 @@ GoRouter setupRouter() => GoRouter( GoRoute( path: '/backup', builder: (context, state) { - final walletBloc = state.extra! as WalletBloc; - return TheBackupPage(walletBloc: walletBloc); + final wallets = state.extra! as List; + return TheBackupPage(wallets: wallets); }, ), // GoRoute( diff --git a/lib/settings/bitcoin_settings_page.dart b/lib/settings/bitcoin_settings_page.dart index 41b97dbc5..9fb76082f 100644 --- a/lib/settings/bitcoin_settings_page.dart +++ b/lib/settings/bitcoin_settings_page.dart @@ -73,8 +73,6 @@ class _Screen extends StatelessWidget { HardwareButton(), Gap(8), SwapHistoryButton(), - Gap(8), - BackupButton(), // SearchAddressButton(), // Gap(8), diff --git a/lib/settings/core_wallet_settings_page.dart b/lib/settings/core_wallet_settings_page.dart index a3f312437..1d120269c 100644 --- a/lib/settings/core_wallet_settings_page.dart +++ b/lib/settings/core_wallet_settings_page.dart @@ -45,12 +45,10 @@ class _Screen extends StatelessWidget { SecureBitcoinWallet(), Gap(8), InstantPaymentsWallet(), - // Gap(8), - // InformationButton(), + Gap(8), + BackupButton(), Gap(8), _ButtonList(), - // ColdcardWallet(), - // Gap(80), ], ), ), @@ -115,6 +113,23 @@ class InstantPaymentsWallet extends StatelessWidget { } } +class BackupButton extends StatelessWidget { + const BackupButton({super.key}); + + @override + Widget build(BuildContext context) { + return BBButton.textWithStatusAndRightArrow( + label: 'Backup', + onPressed: () { + final network = context.read().state.getBBNetwork(); + final wallets = + context.read().state.walletBlocsFromNetwork(network); + context.push('/backup', extra: wallets); + }, + ); + } +} + class _ButtonList extends StatefulWidget { const _ButtonList(); diff --git a/lib/settings/settings_page.dart b/lib/settings/settings_page.dart index 7bf015600..1dae6f00b 100644 --- a/lib/settings/settings_page.dart +++ b/lib/settings/settings_page.dart @@ -3,9 +3,7 @@ import 'package:bb_mobile/_pkg/launcher.dart'; import 'package:bb_mobile/_ui/app_bar.dart'; import 'package:bb_mobile/_ui/components/button.dart'; import 'package:bb_mobile/_ui/components/text.dart'; -import 'package:bb_mobile/home/bloc/home_cubit.dart'; import 'package:bb_mobile/locator.dart'; -import 'package:bb_mobile/network/bloc/network_cubit.dart'; import 'package:bb_mobile/settings/bloc/settings_cubit.dart'; import 'package:bb_mobile/styles.dart'; import 'package:flutter/material.dart'; @@ -159,26 +157,6 @@ class SwapHistoryButton extends StatelessWidget { } } -class BackupButton extends StatelessWidget { - const BackupButton({super.key}); - - @override - Widget build(BuildContext context) { - return BBButton.textWithStatusAndRightArrow( - label: 'Backup', - onPressed: () { - final network = context.read().state.getBBNetwork(); - final walletBlocs = - context.read().state.walletBlocsFromNetwork(network); - context.push( - '/backup', - extra: walletBlocs.first, - ); - }, - ); - } -} - class ApplicationSettingsButton extends StatelessWidget { const ApplicationSettingsButton({super.key}); From f3625788daf15a29a43652fb79f4c5321e0e5094 Mon Sep 17 00:00:00 2001 From: ethicnology Date: Tue, 19 Nov 2024 09:34:27 -0500 Subject: [PATCH 003/401] feat: bip85 backup key derivation --- lib/backup/{page.dart => backup_page.dart} | 5 +---- .../bloc/{cubit.dart => backup_cubit.dart} | 20 +++++++++++++++---- .../bloc/{state.dart => backup_state.dart} | 0 lib/main.dart | 2 ++ lib/routes.dart | 2 +- linux/flutter/generated_plugins.cmake | 1 + pubspec.lock | 9 +++++++++ pubspec.yaml | 5 +++++ 8 files changed, 35 insertions(+), 9 deletions(-) rename lib/backup/{page.dart => backup_page.dart} (96%) rename lib/backup/bloc/{cubit.dart => backup_cubit.dart} (79%) rename lib/backup/bloc/{state.dart => backup_state.dart} (100%) diff --git a/lib/backup/page.dart b/lib/backup/backup_page.dart similarity index 96% rename from lib/backup/page.dart rename to lib/backup/backup_page.dart index 6921d27a4..761133728 100644 --- a/lib/backup/page.dart +++ b/lib/backup/backup_page.dart @@ -3,7 +3,7 @@ import 'package:bb_mobile/_pkg/file_storage.dart'; import 'package:bb_mobile/_pkg/wallet/repository/sensitive_storage.dart'; import 'package:bb_mobile/_ui/app_bar.dart'; import 'package:bb_mobile/_ui/toast.dart'; -import 'package:bb_mobile/backup/bloc/cubit.dart'; +import 'package:bb_mobile/backup/bloc/backup_cubit.dart'; import 'package:bb_mobile/locator.dart'; import 'package:bb_mobile/wallet/bloc/wallet_bloc.dart'; import 'package:flutter/material.dart'; @@ -20,9 +20,6 @@ class TheBackupPage extends StatefulWidget { } class _TheBackupPageState extends State { - bool hasMnemonic = false; - bool hasPassphrases = false; - bool hasDescriptors = false; String backupKey = ''; @override diff --git a/lib/backup/bloc/cubit.dart b/lib/backup/bloc/backup_cubit.dart similarity index 79% rename from lib/backup/bloc/cubit.dart rename to lib/backup/bloc/backup_cubit.dart index 6bafc5bd9..511212d3e 100644 --- a/lib/backup/bloc/cubit.dart +++ b/lib/backup/bloc/backup_cubit.dart @@ -8,8 +8,10 @@ import 'package:bb_mobile/_pkg/error.dart'; import 'package:bb_mobile/_pkg/file_storage.dart'; import 'package:bb_mobile/_pkg/wallet/labels.dart'; import 'package:bb_mobile/_pkg/wallet/repository/sensitive_storage.dart'; -import 'package:bb_mobile/backup/bloc/state.dart'; +import 'package:bb_mobile/backup/bloc/backup_state.dart'; import 'package:bb_mobile/wallet/bloc/wallet_bloc.dart'; +import 'package:bdk_flutter/bdk_flutter.dart' as bdk; +import 'package:bip85/bip85.dart' as bip85; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:hex/hex.dart'; import 'package:intl/intl.dart'; @@ -77,9 +79,19 @@ class BackupCubit extends Cubit { Future<(String?, Err?)> writeEncryptedBackup() async { final backups = state.backups; - final secret = HEX.encode(Crypto.generateRandomBytes(32)); + final firstMnemonic = backups.first.mnemonic; + final bdkMnemonic = await bdk.Mnemonic.fromString(firstMnemonic.join(' ')); + final xprv = bdk.DescriptorSecretKey.create( + network: bdk.Network.bitcoin, // TODO: handle testnet? + mnemonic: bdkMnemonic, + password: '', // TODO: which passphrase? + ).toString(); + + const derivation = "m/1608'/0'"; // TODO: key rotation ? + final derived = bip85.derive(xprv: xprv, path: derivation); + final backupKey = HEX.encode(derived); final plaintext = json.encode(backups.map((i) => i.toJson()).toList()); - final ciphertext = Crypto.aesEncrypt(plaintext, secret); + final ciphertext = Crypto.aesEncrypt(plaintext, backupKey); final now = DateTime.now(); final formattedDate = DateFormat('yyyy-MM-dd_HH-mm-ss').format(now); @@ -94,6 +106,6 @@ class BackupCubit extends Cubit { print(f?.path); - return (secret, null); + return (backupKey, null); } } diff --git a/lib/backup/bloc/state.dart b/lib/backup/bloc/backup_state.dart similarity index 100% rename from lib/backup/bloc/state.dart rename to lib/backup/bloc/backup_state.dart diff --git a/lib/main.dart b/lib/main.dart index c731c0a06..1765942e8 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -20,6 +20,7 @@ import 'package:bb_mobile/settings/bloc/settings_cubit.dart'; import 'package:bb_mobile/styles.dart'; import 'package:bb_mobile/swap/listeners.dart'; import 'package:bb_mobile/swap/watcher_bloc/watchtxs_bloc.dart'; +import 'package:bip85/bip85.dart'; import 'package:boltz/boltz.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -40,6 +41,7 @@ Future main({bool fromTest = false}) async { await core.init(); await LibLwk.init(); await LibBoltz.init(); + await LibBip85.init(); await dotenv.load(isOptional: true); Bloc.observer = BBlocObserver(); // await FlutterWindowManager.addFlags(FlutterWindowManager.FLAG_SECURE); diff --git a/lib/routes.dart b/lib/routes.dart index 0123f2cf9..a1c9d7217 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -4,7 +4,7 @@ import 'package:bb_mobile/_model/swap.dart'; import 'package:bb_mobile/_model/transaction.dart'; import 'package:bb_mobile/_ui/logger_page.dart'; import 'package:bb_mobile/auth/page.dart'; -import 'package:bb_mobile/backup/page.dart'; +import 'package:bb_mobile/backup/backup_page.dart'; import 'package:bb_mobile/create/page.dart'; import 'package:bb_mobile/home/home_page.dart'; import 'package:bb_mobile/home/market.dart'; diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 32f64c052..407c9d317 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -8,6 +8,7 @@ list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + bip85 lwk ) diff --git a/pubspec.lock b/pubspec.lock index beff0d548..85256ced6 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -70,6 +70,15 @@ packages: url: "https://pub.dev" source: hosted version: "0.31.2" + bip85: + dependency: "direct main" + description: + path: "bindings/dart-bip85" + ref: master + resolved-ref: "2321b17f3e1c74f15bdb95638bb8c65bbbe2a2af" + url: "https://github.com/ethicnology/rust-bip85.git" + source: git + version: "1.0.2" bitcoin_utils: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 3227345a1..58f89390c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -94,6 +94,11 @@ dependencies: permission_handler: ^11.3.1 rxdart: ^0.28.0 synchronized: ^3.3.0+3 + bip85: + git: + url: https://github.com/ethicnology/rust-bip85.git + path: bindings/dart-bip85 + ref: master dev_dependencies: build_runner: ^2.4.9 From 964fdadbedc8e281a7c511d1d67f0e94d44fd18d Mon Sep 17 00:00:00 2001 From: ethicnology Date: Thu, 21 Nov 2024 10:11:23 -0500 Subject: [PATCH 004/401] refactor: disable temporarly bip85 --- lib/backup/bloc/backup_cubit.dart | 24 +++++++++++------------- lib/main.dart | 3 +-- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/lib/backup/bloc/backup_cubit.dart b/lib/backup/bloc/backup_cubit.dart index 511212d3e..e3042527c 100644 --- a/lib/backup/bloc/backup_cubit.dart +++ b/lib/backup/bloc/backup_cubit.dart @@ -10,8 +10,6 @@ import 'package:bb_mobile/_pkg/wallet/labels.dart'; import 'package:bb_mobile/_pkg/wallet/repository/sensitive_storage.dart'; import 'package:bb_mobile/backup/bloc/backup_state.dart'; import 'package:bb_mobile/wallet/bloc/wallet_bloc.dart'; -import 'package:bdk_flutter/bdk_flutter.dart' as bdk; -import 'package:bip85/bip85.dart' as bip85; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:hex/hex.dart'; import 'package:intl/intl.dart'; @@ -79,17 +77,17 @@ class BackupCubit extends Cubit { Future<(String?, Err?)> writeEncryptedBackup() async { final backups = state.backups; - final firstMnemonic = backups.first.mnemonic; - final bdkMnemonic = await bdk.Mnemonic.fromString(firstMnemonic.join(' ')); - final xprv = bdk.DescriptorSecretKey.create( - network: bdk.Network.bitcoin, // TODO: handle testnet? - mnemonic: bdkMnemonic, - password: '', // TODO: which passphrase? - ).toString(); - - const derivation = "m/1608'/0'"; // TODO: key rotation ? - final derived = bip85.derive(xprv: xprv, path: derivation); - final backupKey = HEX.encode(derived); + // final firstMnemonic = backups.first.mnemonic; + // final bdkMnemonic = await bdk.Mnemonic.fromString(firstMnemonic.join(' ')); + // final xprv = bdk.DescriptorSecretKey.create( + // network: bdk.Network.bitcoin, // TODO: handle testnet? + // mnemonic: bdkMnemonic, + // password: '', // TODO: which passphrase? + // ).toString(); + + // const derivation = "m/1608'/0'"; // TODO: key rotation ? + // final derived = bip85.derive(xprv: xprv, path: derivation); + final backupKey = HEX.encode(Crypto.generateRandomBytes(32)); final plaintext = json.encode(backups.map((i) => i.toJson()).toList()); final ciphertext = Crypto.aesEncrypt(plaintext, backupKey); diff --git a/lib/main.dart b/lib/main.dart index 1765942e8..e4cffda86 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -20,7 +20,6 @@ import 'package:bb_mobile/settings/bloc/settings_cubit.dart'; import 'package:bb_mobile/styles.dart'; import 'package:bb_mobile/swap/listeners.dart'; import 'package:bb_mobile/swap/watcher_bloc/watchtxs_bloc.dart'; -import 'package:bip85/bip85.dart'; import 'package:boltz/boltz.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -41,7 +40,7 @@ Future main({bool fromTest = false}) async { await core.init(); await LibLwk.init(); await LibBoltz.init(); - await LibBip85.init(); + // await LibBip85.init(); await dotenv.load(isOptional: true); Bloc.observer = BBlocObserver(); // await FlutterWindowManager.addFlags(FlutterWindowManager.FLAG_SECURE); From a6b1198b3a51a9e24458f74eea01bd2c6c683e26 Mon Sep 17 00:00:00 2001 From: ethicnology Date: Thu, 21 Nov 2024 14:10:22 -0500 Subject: [PATCH 005/401] feat: keychain backup --- lib/_pkg/crypto.dart | 5 ++ lib/backup/backup_page.dart | 18 ++++-- lib/backup/bloc/backup_cubit.dart | 18 +++--- lib/backup/bloc/keychain_cubit.dart | 59 +++++++++++++++++ lib/backup/bloc/keychain_state.dart | 9 +++ lib/backup/keychain_page.dart | 98 +++++++++++++++++++++++++++++ lib/routes.dart | 8 +++ pubspec.yaml | 2 +- 8 files changed, 205 insertions(+), 12 deletions(-) create mode 100644 lib/backup/bloc/keychain_cubit.dart create mode 100644 lib/backup/bloc/keychain_state.dart create mode 100644 lib/backup/keychain_page.dart diff --git a/lib/_pkg/crypto.dart b/lib/_pkg/crypto.dart index b15370a68..9e66e6a79 100644 --- a/lib/_pkg/crypto.dart +++ b/lib/_pkg/crypto.dart @@ -4,6 +4,7 @@ import 'dart:typed_data'; import 'package:hex/hex.dart'; import 'package:pointycastle/api.dart'; +import 'package:pointycastle/digests/sha256.dart'; import 'package:pointycastle/export.dart' as pc; class Crypto { @@ -49,4 +50,8 @@ class Crypto { } return randomIV; } + + static List sha256(List input) { + return SHA256Digest().process(Uint8List.fromList(input)); + } } diff --git a/lib/backup/backup_page.dart b/lib/backup/backup_page.dart index 761133728..834cf479d 100644 --- a/lib/backup/backup_page.dart +++ b/lib/backup/backup_page.dart @@ -21,6 +21,7 @@ class TheBackupPage extends StatefulWidget { class _TheBackupPageState extends State { String backupKey = ''; + String backupId = ''; @override Widget build(BuildContext context) { @@ -75,6 +76,13 @@ class _TheBackupPageState extends State { style: const TextStyle(fontWeight: FontWeight.bold), ), + ElevatedButton( + onPressed: () => context.push( + '/keychain-backup', + extra: (backupKey, backupId), + ), + child: const Text('Keychain'), + ), ], ), ), @@ -82,15 +90,17 @@ class _TheBackupPageState extends State { if (backupKey.isEmpty) ElevatedButton( onPressed: () async { - final (secret, error) = + final (key, id) = await backupCubit.writeEncryptedBackup(); - if (error != null) { + if (key == null || id == null) { ScaffoldMessenger.of(context).showSnackBar( - context.showToast(error.toString()), + context.showToast('error'), ); + return; } - if (secret != null) backupKey = secret; + backupKey = key; + backupId = id; setState(() {}); }, child: const Text('Backup'), diff --git a/lib/backup/bloc/backup_cubit.dart b/lib/backup/bloc/backup_cubit.dart index e3042527c..6ffc4c301 100644 --- a/lib/backup/bloc/backup_cubit.dart +++ b/lib/backup/bloc/backup_cubit.dart @@ -4,7 +4,6 @@ import 'dart:io'; import 'package:bb_mobile/_model/backup.dart'; import 'package:bb_mobile/_model/seed.dart'; import 'package:bb_mobile/_pkg/crypto.dart'; -import 'package:bb_mobile/_pkg/error.dart'; import 'package:bb_mobile/_pkg/file_storage.dart'; import 'package:bb_mobile/_pkg/wallet/labels.dart'; import 'package:bb_mobile/_pkg/wallet/repository/sensitive_storage.dart'; @@ -74,7 +73,7 @@ class BackupCubit extends Cubit { return backups; } - Future<(String?, Err?)> writeEncryptedBackup() async { + Future<(String?, String?)> writeEncryptedBackup() async { final backups = state.backups; // final firstMnemonic = backups.first.mnemonic; @@ -88,22 +87,27 @@ class BackupCubit extends Cubit { // const derivation = "m/1608'/0'"; // TODO: key rotation ? // final derived = bip85.derive(xprv: xprv, path: derivation); final backupKey = HEX.encode(Crypto.generateRandomBytes(32)); + final backupId = HEX.encode(Crypto.generateRandomBytes(32)); + final plaintext = json.encode(backups.map((i) => i.toJson()).toList()); - final ciphertext = Crypto.aesEncrypt(plaintext, backupKey); + final ciphertext = + Crypto.aesEncrypt(plaintext, backupKey); // TODO : extract nonce? final now = DateTime.now(); final formattedDate = DateFormat('yyyy-MM-dd_HH-mm-ss').format(now); final filename = '${formattedDate}_backup.txt'; final (directory, errDir) = await _fileStorage.getDownloadDirectory(); - if (errDir != null) return (null, Err('Fail to get Download directory')); + if (errDir != null) return (null, null); // Fail to get Download directory + final file = File(directory! + '/' + filename); + final content = json.encode({'id': backupId, 'encrypted': ciphertext}); - final (f, errSave) = await _fileStorage.saveToFile(file, ciphertext); - if (errSave != null) return (null, Err('Fail to save backup')); + final (f, errSave) = await _fileStorage.saveToFile(file, content); + if (errSave != null) return (null, null); // Fail to save backup print(f?.path); - return (backupKey, null); + return (backupKey, backupId); } } diff --git a/lib/backup/bloc/keychain_cubit.dart b/lib/backup/bloc/keychain_cubit.dart new file mode 100644 index 000000000..2c0acc98e --- /dev/null +++ b/lib/backup/bloc/keychain_cubit.dart @@ -0,0 +1,59 @@ +import 'dart:convert'; + +import 'package:bb_mobile/_pkg/crypto.dart'; +import 'package:bb_mobile/_pkg/error.dart'; +import 'package:bb_mobile/backup/bloc/keychain_state.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:hex/hex.dart'; +import 'package:http/http.dart' as http; + +class KeychainCubit extends Cubit { + KeychainCubit() : super(KeychainState(secret: '', secretConfirmed: false)); + + void updateSecret(String value) { + emit(KeychainState(secret: value, secretConfirmed: false)); + } + + void confirmSecret(String value) { + emit( + KeychainState( + secret: state.secret, + secretConfirmed: state.secret == value, + ), + ); + } + + Future secureBackupKey( + String backupId, + String backupKey, + ) async { + if (state.secret.isEmpty || !state.secretConfirmed) { + return Err('confirm your secret'); + } + + final keychainUrl = dotenv.env['KEYCHAIN_URL']; + if (keychainUrl == null) return Err('KEYCHAIN_URL missing from .env'); + + final secretHashBytes = Crypto.sha256(utf8.encode(state.secret)); + final secretHashHex = HEX.encode(secretHashBytes); + + final response = await http.post( + Uri.parse('$keychainUrl/store_key'), + headers: {'Content-Type': 'application/json'}, + body: json.encode({ + 'backup_id': backupId, + 'backup_key': backupKey, + 'secret_hash': secretHashHex, + }), + ); + + if (response.statusCode == 201) { + return null; + } else if (response.statusCode == 403) { + return Err('Key already stored'); + } else { + return Err('Key not secured \n${response.statusCode}'); + } + } +} diff --git a/lib/backup/bloc/keychain_state.dart b/lib/backup/bloc/keychain_state.dart new file mode 100644 index 000000000..e25bae876 --- /dev/null +++ b/lib/backup/bloc/keychain_state.dart @@ -0,0 +1,9 @@ +class KeychainState { + KeychainState({ + required this.secret, + required this.secretConfirmed, + }); + + final String secret; + final bool secretConfirmed; +} diff --git a/lib/backup/keychain_page.dart b/lib/backup/keychain_page.dart new file mode 100644 index 000000000..a7520df47 --- /dev/null +++ b/lib/backup/keychain_page.dart @@ -0,0 +1,98 @@ +import 'package:bb_mobile/_ui/app_bar.dart'; +import 'package:bb_mobile/_ui/toast.dart'; +import 'package:bb_mobile/backup/bloc/keychain_cubit.dart'; +import 'package:bb_mobile/backup/bloc/keychain_state.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; + +class KeychainBackupPage extends StatelessWidget { + const KeychainBackupPage({ + super.key, + required this.backupKey, + required this.backupId, + }); + + final String backupKey; + final String backupId; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => KeychainCubit(), + child: Scaffold( + backgroundColor: Colors.amber, + appBar: AppBar( + backgroundColor: Colors.transparent, + elevation: 0, + flexibleSpace: BBAppBar( + text: 'Keychain Backup', + onBack: () => context.pop(), + ), + ), + body: BlocBuilder( + builder: (context, state) { + final cubit = context.read(); + return Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Column( + children: [ + SelectableText('Backup Key: $backupKey'), + SelectableText('Backup ID: $backupId'), + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + SizedBox( + width: 100, + child: TextFormField( + decoration: + const InputDecoration(labelText: 'Enter PIN'), + keyboardType: TextInputType.number, + maxLength: 6, + onChanged: (value) => cubit.updateSecret(value), + ), + ), + SizedBox( + width: 100, + child: TextFormField( + decoration: + const InputDecoration(labelText: 'Confirm PIN'), + keyboardType: TextInputType.number, + obscureText: true, + maxLength: 6, + onChanged: (value) => cubit.confirmSecret(value), + ), + ), + ], + ), + if (state.secretConfirmed) + ElevatedButton( + onPressed: () async { + final err = await cubit.secureBackupKey( + backupId, + backupKey, + ); + final message = err?.message ?? 'Key secured'; + + ScaffoldMessenger.of(context).showSnackBar( + context.showToast(message), + ); + }, + child: const Text('Secure my backup key'), + ), + if (!state.secretConfirmed) + const Text( + 'PINs do not match! Please confirm your PIN.', + style: TextStyle(color: Colors.red), + ), + ], + ); + }, + ), + ), + ); + } +} diff --git a/lib/routes.dart b/lib/routes.dart index a1c9d7217..15ea2946e 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -5,6 +5,7 @@ import 'package:bb_mobile/_model/transaction.dart'; import 'package:bb_mobile/_ui/logger_page.dart'; import 'package:bb_mobile/auth/page.dart'; import 'package:bb_mobile/backup/backup_page.dart'; +import 'package:bb_mobile/backup/keychain_page.dart'; import 'package:bb_mobile/create/page.dart'; import 'package:bb_mobile/home/home_page.dart'; import 'package:bb_mobile/home/market.dart'; @@ -202,6 +203,13 @@ GoRouter setupRouter() => GoRouter( return TheBackupPage(wallets: wallets); }, ), + GoRoute( + path: '/keychain-backup', + builder: (context, state) { + final (backupKey, backupId) = state.extra! as (String, String); + return KeychainBackupPage(backupKey: backupKey, backupId: backupId); + }, + ), // GoRoute( // path: '/wallet-settings/open-test-backup', // builder: (context, state) { diff --git a/pubspec.yaml b/pubspec.yaml index 58f89390c..f39e48fab 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -129,8 +129,8 @@ flutter: # - assets/arrow_down_white.png # - assets/request-payment.svg # - assets/new-address.svg - - assets/bip39_english.txt + - .env flutter_native_splash: background_image: "assets/splash.png" From 05da1ffc309e1a9ec5e85cc2fa05d778b38c0bb1 Mon Sep 17 00:00:00 2001 From: ethicnology Date: Fri, 22 Nov 2024 08:46:19 -0500 Subject: [PATCH 006/401] refactor: wip --- lib/backup/bloc/backup_cubit.dart | 24 +++++++++++++++--------- lib/main.dart | 3 ++- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/lib/backup/bloc/backup_cubit.dart b/lib/backup/bloc/backup_cubit.dart index 6ffc4c301..354a7acf4 100644 --- a/lib/backup/bloc/backup_cubit.dart +++ b/lib/backup/bloc/backup_cubit.dart @@ -9,6 +9,8 @@ import 'package:bb_mobile/_pkg/wallet/labels.dart'; import 'package:bb_mobile/_pkg/wallet/repository/sensitive_storage.dart'; import 'package:bb_mobile/backup/bloc/backup_state.dart'; import 'package:bb_mobile/wallet/bloc/wallet_bloc.dart'; +import 'package:bdk_flutter/bdk_flutter.dart' as bdk; +import 'package:bip85/bip85.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:hex/hex.dart'; import 'package:intl/intl.dart'; @@ -76,16 +78,20 @@ class BackupCubit extends Cubit { Future<(String?, String?)> writeEncryptedBackup() async { final backups = state.backups; - // final firstMnemonic = backups.first.mnemonic; - // final bdkMnemonic = await bdk.Mnemonic.fromString(firstMnemonic.join(' ')); - // final xprv = bdk.DescriptorSecretKey.create( - // network: bdk.Network.bitcoin, // TODO: handle testnet? - // mnemonic: bdkMnemonic, - // password: '', // TODO: which passphrase? - // ).toString(); + final firstMnemonic = backups.first.mnemonic; + final bdkMnemonic = await bdk.Mnemonic.fromString(firstMnemonic.join(' ')); + final xprv = await bdk.DescriptorSecretKey.create( + network: bdk.Network.bitcoin, // TODO: handle testnet? + mnemonic: bdkMnemonic, + password: '', // TODO: which passphrase? + ); + final rootXprv = xprv.toString().substring(0, 64); // remove /* + print('rootXprv: $rootXprv'); + + const derivation = "m/1608'/0'"; // TODO: key rotation ? + final derived = derive(xprv: rootXprv, path: derivation); + print('derived: $derived'); - // const derivation = "m/1608'/0'"; // TODO: key rotation ? - // final derived = bip85.derive(xprv: xprv, path: derivation); final backupKey = HEX.encode(Crypto.generateRandomBytes(32)); final backupId = HEX.encode(Crypto.generateRandomBytes(32)); diff --git a/lib/main.dart b/lib/main.dart index e4cffda86..1765942e8 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -20,6 +20,7 @@ import 'package:bb_mobile/settings/bloc/settings_cubit.dart'; import 'package:bb_mobile/styles.dart'; import 'package:bb_mobile/swap/listeners.dart'; import 'package:bb_mobile/swap/watcher_bloc/watchtxs_bloc.dart'; +import 'package:bip85/bip85.dart'; import 'package:boltz/boltz.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -40,7 +41,7 @@ Future main({bool fromTest = false}) async { await core.init(); await LibLwk.init(); await LibBoltz.init(); - // await LibBip85.init(); + await LibBip85.init(); await dotenv.load(isOptional: true); Bloc.observer = BBlocObserver(); // await FlutterWindowManager.addFlags(FlutterWindowManager.FLAG_SECURE); From 749f3d074e5d4b8aacedf095db0a9337b4f4335a Mon Sep 17 00:00:00 2001 From: ethicnology Date: Mon, 25 Nov 2024 09:46:14 -0500 Subject: [PATCH 007/401] refactor: catch inaccessible server --- lib/backup/bloc/keychain_cubit.dart | 36 ++++++++++++++++------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/lib/backup/bloc/keychain_cubit.dart b/lib/backup/bloc/keychain_cubit.dart index 2c0acc98e..4c710a1da 100644 --- a/lib/backup/bloc/keychain_cubit.dart +++ b/lib/backup/bloc/keychain_cubit.dart @@ -38,22 +38,26 @@ class KeychainCubit extends Cubit { final secretHashBytes = Crypto.sha256(utf8.encode(state.secret)); final secretHashHex = HEX.encode(secretHashBytes); - final response = await http.post( - Uri.parse('$keychainUrl/store_key'), - headers: {'Content-Type': 'application/json'}, - body: json.encode({ - 'backup_id': backupId, - 'backup_key': backupKey, - 'secret_hash': secretHashHex, - }), - ); - - if (response.statusCode == 201) { - return null; - } else if (response.statusCode == 403) { - return Err('Key already stored'); - } else { - return Err('Key not secured \n${response.statusCode}'); + try { + final response = await http.post( + Uri.parse('$keychainUrl/store_key'), + headers: {'Content-Type': 'application/json'}, + body: json.encode({ + 'backup_id': backupId, + 'backup_key': backupKey, + 'secret_hash': secretHashHex, + }), + ); + + if (response.statusCode == 201) { + return null; + } else if (response.statusCode == 403) { + return Err('Key already stored'); + } else { + return Err('Key not secured \n${response.statusCode}'); + } + } catch (e) { + return Err('Server Inaccessible'); } } } From c005bae08163ee6f33e6693914902462e8a9681e Mon Sep 17 00:00:00 2001 From: ethicnology Date: Mon, 25 Nov 2024 10:43:03 -0500 Subject: [PATCH 008/401] refactor: readability --- lib/backup/bloc/backup_cubit.dart | 34 ++++++++++++++----------------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/lib/backup/bloc/backup_cubit.dart b/lib/backup/bloc/backup_cubit.dart index 354a7acf4..3e2cea962 100644 --- a/lib/backup/bloc/backup_cubit.dart +++ b/lib/backup/bloc/backup_cubit.dart @@ -10,34 +10,30 @@ import 'package:bb_mobile/_pkg/wallet/repository/sensitive_storage.dart'; import 'package:bb_mobile/backup/bloc/backup_state.dart'; import 'package:bb_mobile/wallet/bloc/wallet_bloc.dart'; import 'package:bdk_flutter/bdk_flutter.dart' as bdk; -import 'package:bip85/bip85.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:hex/hex.dart'; import 'package:intl/intl.dart'; class BackupCubit extends Cubit { BackupCubit({ - required List wallets, - required WalletSensitiveStorageRepository walletSensitiveStorage, - required FileStorage fileStorage, - }) : _wallets = wallets, - _walletSensitiveStorage = walletSensitiveStorage, - _fileStorage = fileStorage, - super(BackupState(backups: [])); - - final FileStorage _fileStorage; - final List _wallets; - final WalletSensitiveStorageRepository _walletSensitiveStorage; + required this.wallets, + required this.walletSensitiveStorage, + required this.fileStorage, + }) : super(BackupState(backups: [])); + + final FileStorage fileStorage; + final List wallets; + final WalletSensitiveStorageRepository walletSensitiveStorage; Future> loadBackupData() async { emit(BackupState(loading: true, backups: [])); final backups = []; - for (final walletBloc in _wallets) { + for (final walletBloc in wallets) { final wallet = walletBloc.state.wallet!; - final (seed, error) = await _walletSensitiveStorage.readSeed( + final (seed, error) = await walletSensitiveStorage.readSeed( fingerprintIndex: wallet.getRelatedSeedStorageString(), ); final mnemonic = seed?.mnemonic.split(' ') ?? []; @@ -88,9 +84,9 @@ class BackupCubit extends Cubit { final rootXprv = xprv.toString().substring(0, 64); // remove /* print('rootXprv: $rootXprv'); - const derivation = "m/1608'/0'"; // TODO: key rotation ? - final derived = derive(xprv: rootXprv, path: derivation); - print('derived: $derived'); + // const derivation = "m/1608'/0'"; // TODO: key rotation ? + // final derived = derive(xprv: rootXprv, path: derivation); + // print('derived: $derived'); final backupKey = HEX.encode(Crypto.generateRandomBytes(32)); final backupId = HEX.encode(Crypto.generateRandomBytes(32)); @@ -103,13 +99,13 @@ class BackupCubit extends Cubit { final formattedDate = DateFormat('yyyy-MM-dd_HH-mm-ss').format(now); final filename = '${formattedDate}_backup.txt'; - final (directory, errDir) = await _fileStorage.getDownloadDirectory(); + final (directory, errDir) = await fileStorage.getDownloadDirectory(); if (errDir != null) return (null, null); // Fail to get Download directory final file = File(directory! + '/' + filename); final content = json.encode({'id': backupId, 'encrypted': ciphertext}); - final (f, errSave) = await _fileStorage.saveToFile(file, content); + final (f, errSave) = await fileStorage.saveToFile(file, content); if (errSave != null) return (null, null); // Fail to save backup print(f?.path); From 91485dc290a35e4ae6ff65c9594e9c7839866ee4 Mon Sep 17 00:00:00 2001 From: ethicnology Date: Mon, 25 Nov 2024 10:49:36 -0500 Subject: [PATCH 009/401] =?UTF-8?q?refactor:=20txt=20=E2=80=93>=20json?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/backup/bloc/backup_cubit.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/backup/bloc/backup_cubit.dart b/lib/backup/bloc/backup_cubit.dart index 3e2cea962..72735c93c 100644 --- a/lib/backup/bloc/backup_cubit.dart +++ b/lib/backup/bloc/backup_cubit.dart @@ -97,7 +97,7 @@ class BackupCubit extends Cubit { final now = DateTime.now(); final formattedDate = DateFormat('yyyy-MM-dd_HH-mm-ss').format(now); - final filename = '${formattedDate}_backup.txt'; + final filename = '${formattedDate}_backup.json'; final (directory, errDir) = await fileStorage.getDownloadDirectory(); if (errDir != null) return (null, null); // Fail to get Download directory From 67f1bffd50c25cf9123f94b3fb36e80d1d7ba883 Mon Sep 17 00:00:00 2001 From: ethicnology Date: Mon, 25 Nov 2024 19:27:12 -0500 Subject: [PATCH 010/401] feat: recovery --- lib/_model/backup.dart | 9 +- lib/_model/backup.freezed.dart | 159 ++++++++++++++--- lib/_model/backup.g.dart | 17 +- lib/backup/backup_page.dart | 164 +++++++++--------- lib/backup/bloc/backup_cubit.dart | 61 ++++--- lib/backup/bloc/backup_state.dart | 15 +- lib/backup/bloc/keychain_cubit.dart | 40 ++--- lib/backup/bloc/keychain_state.dart | 18 +- lib/backup/keychain_page.dart | 131 +++++++------- lib/recover/bloc/manual_cubit.dart | 178 ++++++++++++++++++++ lib/recover/bloc/manual_state.dart | 12 ++ lib/recover/manual_page.dart | 96 +++++++++++ lib/routes.dart | 10 +- lib/settings/core_wallet_settings_page.dart | 29 +++- 14 files changed, 702 insertions(+), 237 deletions(-) create mode 100644 lib/recover/bloc/manual_cubit.dart create mode 100644 lib/recover/bloc/manual_state.dart create mode 100644 lib/recover/manual_page.dart diff --git a/lib/_model/backup.dart b/lib/_model/backup.dart index 4799837d3..7e7d4c0f9 100644 --- a/lib/_model/backup.dart +++ b/lib/_model/backup.dart @@ -8,8 +8,13 @@ part 'backup.g.dart'; class Backup with _$Backup { const factory Backup({ @Default(1) int version, + @Default('') String name, + @Default('') String layer, + @Default('') String network, + @Default('') String script, + @Default('') String type, @Default([]) List mnemonic, - @Default([]) List passphrases, + @Default('') String passphrase, @Default([]) List labels, @Default([]) List descriptors, }) = _Backup; @@ -20,7 +25,7 @@ class Backup with _$Backup { bool get isEmpty => mnemonic.isEmpty && - passphrases.isEmpty && + passphrase.isEmpty && labels.isEmpty && descriptors.isEmpty; } diff --git a/lib/_model/backup.freezed.dart b/lib/_model/backup.freezed.dart index cf8e3cea7..6d95b2ada 100644 --- a/lib/_model/backup.freezed.dart +++ b/lib/_model/backup.freezed.dart @@ -21,8 +21,13 @@ Backup _$BackupFromJson(Map json) { /// @nodoc mixin _$Backup { int get version => throw _privateConstructorUsedError; + String get name => throw _privateConstructorUsedError; + String get layer => throw _privateConstructorUsedError; + String get network => throw _privateConstructorUsedError; + String get script => throw _privateConstructorUsedError; + String get type => throw _privateConstructorUsedError; List get mnemonic => throw _privateConstructorUsedError; - List get passphrases => throw _privateConstructorUsedError; + String get passphrase => throw _privateConstructorUsedError; List get labels => throw _privateConstructorUsedError; List get descriptors => throw _privateConstructorUsedError; @@ -42,8 +47,13 @@ abstract class $BackupCopyWith<$Res> { @useResult $Res call( {int version, + String name, + String layer, + String network, + String script, + String type, List mnemonic, - List passphrases, + String passphrase, List labels, List descriptors}); } @@ -64,8 +74,13 @@ class _$BackupCopyWithImpl<$Res, $Val extends Backup> @override $Res call({ Object? version = null, + Object? name = null, + Object? layer = null, + Object? network = null, + Object? script = null, + Object? type = null, Object? mnemonic = null, - Object? passphrases = null, + Object? passphrase = null, Object? labels = null, Object? descriptors = null, }) { @@ -74,14 +89,34 @@ class _$BackupCopyWithImpl<$Res, $Val extends Backup> ? _value.version : version // ignore: cast_nullable_to_non_nullable as int, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + layer: null == layer + ? _value.layer + : layer // ignore: cast_nullable_to_non_nullable + as String, + network: null == network + ? _value.network + : network // ignore: cast_nullable_to_non_nullable + as String, + script: null == script + ? _value.script + : script // ignore: cast_nullable_to_non_nullable + as String, + type: null == type + ? _value.type + : type // ignore: cast_nullable_to_non_nullable + as String, mnemonic: null == mnemonic ? _value.mnemonic : mnemonic // ignore: cast_nullable_to_non_nullable as List, - passphrases: null == passphrases - ? _value.passphrases - : passphrases // ignore: cast_nullable_to_non_nullable - as List, + passphrase: null == passphrase + ? _value.passphrase + : passphrase // ignore: cast_nullable_to_non_nullable + as String, labels: null == labels ? _value.labels : labels // ignore: cast_nullable_to_non_nullable @@ -103,8 +138,13 @@ abstract class _$$BackupImplCopyWith<$Res> implements $BackupCopyWith<$Res> { @useResult $Res call( {int version, + String name, + String layer, + String network, + String script, + String type, List mnemonic, - List passphrases, + String passphrase, List labels, List descriptors}); } @@ -123,8 +163,13 @@ class __$$BackupImplCopyWithImpl<$Res> @override $Res call({ Object? version = null, + Object? name = null, + Object? layer = null, + Object? network = null, + Object? script = null, + Object? type = null, Object? mnemonic = null, - Object? passphrases = null, + Object? passphrase = null, Object? labels = null, Object? descriptors = null, }) { @@ -133,14 +178,34 @@ class __$$BackupImplCopyWithImpl<$Res> ? _value.version : version // ignore: cast_nullable_to_non_nullable as int, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + layer: null == layer + ? _value.layer + : layer // ignore: cast_nullable_to_non_nullable + as String, + network: null == network + ? _value.network + : network // ignore: cast_nullable_to_non_nullable + as String, + script: null == script + ? _value.script + : script // ignore: cast_nullable_to_non_nullable + as String, + type: null == type + ? _value.type + : type // ignore: cast_nullable_to_non_nullable + as String, mnemonic: null == mnemonic ? _value._mnemonic : mnemonic // ignore: cast_nullable_to_non_nullable as List, - passphrases: null == passphrases - ? _value._passphrases - : passphrases // ignore: cast_nullable_to_non_nullable - as List, + passphrase: null == passphrase + ? _value.passphrase + : passphrase // ignore: cast_nullable_to_non_nullable + as String, labels: null == labels ? _value._labels : labels // ignore: cast_nullable_to_non_nullable @@ -158,12 +223,16 @@ class __$$BackupImplCopyWithImpl<$Res> class _$BackupImpl extends _Backup { const _$BackupImpl( {this.version = 1, + this.name = '', + this.layer = '', + this.network = '', + this.script = '', + this.type = '', final List mnemonic = const [], - final List passphrases = const [], + this.passphrase = '', final List labels = const [], final List descriptors = const []}) : _mnemonic = mnemonic, - _passphrases = passphrases, _labels = labels, _descriptors = descriptors, super._(); @@ -174,6 +243,21 @@ class _$BackupImpl extends _Backup { @override @JsonKey() final int version; + @override + @JsonKey() + final String name; + @override + @JsonKey() + final String layer; + @override + @JsonKey() + final String network; + @override + @JsonKey() + final String script; + @override + @JsonKey() + final String type; final List _mnemonic; @override @JsonKey() @@ -183,15 +267,9 @@ class _$BackupImpl extends _Backup { return EqualUnmodifiableListView(_mnemonic); } - final List _passphrases; @override @JsonKey() - List get passphrases { - if (_passphrases is EqualUnmodifiableListView) return _passphrases; - // ignore: implicit_dynamic_type - return EqualUnmodifiableListView(_passphrases); - } - + final String passphrase; final List _labels; @override @JsonKey() @@ -212,7 +290,7 @@ class _$BackupImpl extends _Backup { @override String toString() { - return 'Backup(version: $version, mnemonic: $mnemonic, passphrases: $passphrases, labels: $labels, descriptors: $descriptors)'; + return 'Backup(version: $version, name: $name, layer: $layer, network: $network, script: $script, type: $type, mnemonic: $mnemonic, passphrase: $passphrase, labels: $labels, descriptors: $descriptors)'; } @override @@ -221,9 +299,14 @@ class _$BackupImpl extends _Backup { (other.runtimeType == runtimeType && other is _$BackupImpl && (identical(other.version, version) || other.version == version) && + (identical(other.name, name) || other.name == name) && + (identical(other.layer, layer) || other.layer == layer) && + (identical(other.network, network) || other.network == network) && + (identical(other.script, script) || other.script == script) && + (identical(other.type, type) || other.type == type) && const DeepCollectionEquality().equals(other._mnemonic, _mnemonic) && - const DeepCollectionEquality() - .equals(other._passphrases, _passphrases) && + (identical(other.passphrase, passphrase) || + other.passphrase == passphrase) && const DeepCollectionEquality().equals(other._labels, _labels) && const DeepCollectionEquality() .equals(other._descriptors, _descriptors)); @@ -234,8 +317,13 @@ class _$BackupImpl extends _Backup { int get hashCode => Object.hash( runtimeType, version, + name, + layer, + network, + script, + type, const DeepCollectionEquality().hash(_mnemonic), - const DeepCollectionEquality().hash(_passphrases), + passphrase, const DeepCollectionEquality().hash(_labels), const DeepCollectionEquality().hash(_descriptors)); @@ -258,8 +346,13 @@ class _$BackupImpl extends _Backup { abstract class _Backup extends Backup { const factory _Backup( {final int version, + final String name, + final String layer, + final String network, + final String script, + final String type, final List mnemonic, - final List passphrases, + final String passphrase, final List labels, final List descriptors}) = _$BackupImpl; const _Backup._() : super._(); @@ -269,9 +362,19 @@ abstract class _Backup extends Backup { @override int get version; @override + String get name; + @override + String get layer; + @override + String get network; + @override + String get script; + @override + String get type; + @override List get mnemonic; @override - List get passphrases; + String get passphrase; @override List get labels; @override diff --git a/lib/_model/backup.g.dart b/lib/_model/backup.g.dart index d6e23d062..21bc4be6e 100644 --- a/lib/_model/backup.g.dart +++ b/lib/_model/backup.g.dart @@ -8,14 +8,16 @@ part of 'backup.dart'; _$BackupImpl _$$BackupImplFromJson(Map json) => _$BackupImpl( version: (json['version'] as num?)?.toInt() ?? 1, + name: json['name'] as String? ?? '', + layer: json['layer'] as String? ?? '', + network: json['network'] as String? ?? '', + script: json['script'] as String? ?? '', + type: json['type'] as String? ?? '', mnemonic: (json['mnemonic'] as List?) ?.map((e) => e as String) .toList() ?? const [], - passphrases: (json['passphrases'] as List?) - ?.map((e) => e as String) - .toList() ?? - const [], + passphrase: json['passphrase'] as String? ?? '', labels: (json['labels'] as List?) ?.map((e) => Bip329Label.fromJson(e as Map)) .toList() ?? @@ -29,8 +31,13 @@ _$BackupImpl _$$BackupImplFromJson(Map json) => _$BackupImpl( Map _$$BackupImplToJson(_$BackupImpl instance) => { 'version': instance.version, + 'name': instance.name, + 'layer': instance.layer, + 'network': instance.network, + 'script': instance.script, + 'type': instance.type, 'mnemonic': instance.mnemonic, - 'passphrases': instance.passphrases, + 'passphrase': instance.passphrase, 'labels': instance.labels, 'descriptors': instance.descriptors, }; diff --git a/lib/backup/backup_page.dart b/lib/backup/backup_page.dart index 834cf479d..f99ef1d70 100644 --- a/lib/backup/backup_page.dart +++ b/lib/backup/backup_page.dart @@ -1,9 +1,8 @@ -import 'package:bb_mobile/_model/backup.dart'; import 'package:bb_mobile/_pkg/file_storage.dart'; import 'package:bb_mobile/_pkg/wallet/repository/sensitive_storage.dart'; import 'package:bb_mobile/_ui/app_bar.dart'; -import 'package:bb_mobile/_ui/toast.dart'; import 'package:bb_mobile/backup/bloc/backup_cubit.dart'; +import 'package:bb_mobile/backup/bloc/backup_state.dart'; import 'package:bb_mobile/locator.dart'; import 'package:bb_mobile/wallet/bloc/wallet_bloc.dart'; import 'package:flutter/material.dart'; @@ -20,96 +19,101 @@ class TheBackupPage extends StatefulWidget { } class _TheBackupPageState extends State { - String backupKey = ''; - String backupId = ''; - @override Widget build(BuildContext context) { - final backupCubit = BackupCubit( - wallets: widget.wallets, - walletSensitiveStorage: locator(), - fileStorage: locator(), - ); - - return MultiBlocProvider( - providers: [BlocProvider.value(value: backupCubit)], + return BlocProvider( + create: (_) => BackupCubit( + wallets: widget.wallets, + walletSensitiveStorage: locator(), + fileStorage: locator(), + )..loadBackupData(), child: Scaffold( + backgroundColor: Colors.amber, appBar: AppBar( - automaticallyImplyLeading: false, flexibleSpace: BBAppBar( - text: 'Backup', + text: 'Recover Backup', onBack: () => context.pop(), ), ), - body: FutureBuilder>( - future: backupCubit.loadBackupData(), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const Center(child: CircularProgressIndicator()); - } else if (snapshot.hasError) { - return Center(child: Text('Error: ${snapshot.error}')); - } else { - final backup = snapshot.data!; - - return Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (backupKey.isNotEmpty) - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - border: Border.all(color: Colors.amber, width: 2), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'Backup Key:', - style: TextStyle(fontWeight: FontWeight.bold), - ), - const SizedBox(height: 10), - SelectableText( - backupKey, - style: - const TextStyle(fontWeight: FontWeight.bold), - ), - ElevatedButton( - onPressed: () => context.push( - '/keychain-backup', - extra: (backupKey, backupId), - ), - child: const Text('Keychain'), - ), - ], - ), - ), - const SizedBox(height: 20), - if (backupKey.isEmpty) - ElevatedButton( - onPressed: () async { - final (key, id) = - await backupCubit.writeEncryptedBackup(); - if (key == null || id == null) { - ScaffoldMessenger.of(context).showSnackBar( - context.showToast('error'), - ); - return; - } - - backupKey = key; - backupId = id; - setState(() {}); - }, - child: const Text('Backup'), - ), - ], + body: BlocListener( + listener: (context, state) { + if (state.error.isNotEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.error), + backgroundColor: Colors.red, + ), + ); + context.read().clearError(); + } + if (state.backupId.isNotEmpty && state.backupKey.isNotEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Backup completed'), + backgroundColor: Colors.green, ), ); } }, + child: BlocBuilder( + builder: (context, state) { + return state.loading + ? const Center(child: CircularProgressIndicator()) + : Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (state.backupKey.isNotEmpty) + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + border: + Border.all(color: Colors.amber, width: 2), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Backup Key:', + style: + TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 10), + SelectableText( + state.backupKey, + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + if (state.backupId.isNotEmpty) + ElevatedButton( + onPressed: () => context.push( + '/keychain-backup', + extra: ( + state.backupKey, + state.backupId + ), + ), + child: const Text('Keychain'), + ), + ], + ), + ), + const SizedBox(height: 20), + if (state.backupKey.isEmpty) + ElevatedButton( + onPressed: context + .read() + .writeEncryptedBackup, + child: const Text('Backup'), + ), + ], + ), + ); + }, + ), ), ), ); diff --git a/lib/backup/bloc/backup_cubit.dart b/lib/backup/bloc/backup_cubit.dart index 72735c93c..2cc88354e 100644 --- a/lib/backup/bloc/backup_cubit.dart +++ b/lib/backup/bloc/backup_cubit.dart @@ -2,7 +2,6 @@ import 'dart:convert'; import 'dart:io'; import 'package:bb_mobile/_model/backup.dart'; -import 'package:bb_mobile/_model/seed.dart'; import 'package:bb_mobile/_pkg/crypto.dart'; import 'package:bb_mobile/_pkg/file_storage.dart'; import 'package:bb_mobile/_pkg/wallet/labels.dart'; @@ -19,15 +18,13 @@ class BackupCubit extends Cubit { required this.wallets, required this.walletSensitiveStorage, required this.fileStorage, - }) : super(BackupState(backups: [])); + }) : super(const BackupState()); final FileStorage fileStorage; final List wallets; final WalletSensitiveStorageRepository walletSensitiveStorage; - Future> loadBackupData() async { - emit(BackupState(loading: true, backups: [])); - + Future loadBackupData() async { final backups = []; for (final walletBloc in wallets) { @@ -38,10 +35,13 @@ class BackupCubit extends Cubit { ); final mnemonic = seed?.mnemonic.split(' ') ?? []; - final passphrases = []; - for (final Passphrase passphrase in seed?.passphrases ?? []) { - passphrases.add(passphrase.passphrase); - } + final passphrase = wallet.hasPassphrase() + ? seed!.passphrases + .firstWhere( + (e) => e.sourceFingerprint == wallet.sourceFingerprint, + ) + .passphrase + : ''; final descriptors = [wallet.getDescriptorCombined()]; @@ -59,19 +59,23 @@ class BackupCubit extends Cubit { backups.add( Backup( + name: wallet.name ?? '', + network: wallet.network.name.toLowerCase(), + layer: wallet.baseWalletType.name.toLowerCase(), + script: wallet.scriptType.name.toLowerCase(), + type: wallet.type.name.toLowerCase(), mnemonic: mnemonic, - passphrases: passphrases, + passphrase: passphrase, descriptors: descriptors, labels: labels, ), ); } - emit(BackupState(backups: backups)); - return backups; + emit(state.copyWith(backups: backups, loading: false)); } - Future<(String?, String?)> writeEncryptedBackup() async { + Future writeEncryptedBackup() async { final backups = state.backups; final firstMnemonic = backups.first.mnemonic; @@ -88,28 +92,37 @@ class BackupCubit extends Cubit { // final derived = derive(xprv: rootXprv, path: derivation); // print('derived: $derived'); - final backupKey = HEX.encode(Crypto.generateRandomBytes(32)); + // final backupKey = HEX.encode(Crypto.generateRandomBytes(32)); + // TODO: replace by BIP85 + const backupKey = + '23fe885eb43961829d0951f1f7eb251890512c504f6ea88034e6369491f256dd'; final backupId = HEX.encode(Crypto.generateRandomBytes(32)); final plaintext = json.encode(backups.map((i) => i.toJson()).toList()); - final ciphertext = - Crypto.aesEncrypt(plaintext, backupKey); // TODO : extract nonce? + final ciphertext = Crypto.aesEncrypt(plaintext, backupKey); + // TODO : extract nonce? final now = DateTime.now(); final formattedDate = DateFormat('yyyy-MM-dd_HH-mm-ss').format(now); - final filename = '${formattedDate}_backup.json'; + final filename = '$formattedDate.json'; - final (directory, errDir) = await fileStorage.getDownloadDirectory(); - if (errDir != null) return (null, null); // Fail to get Download directory + final (appDir, errDir) = await fileStorage.getAppDirectory(); + if (errDir != null) { + emit(state.copyWith(error: 'Fail to get Download directory')); + } - final file = File(directory! + '/' + filename); + final backupDir = + await Directory(appDir! + '/backups/').create(recursive: true); + final file = File(backupDir.path + filename); final content = json.encode({'id': backupId, 'encrypted': ciphertext}); final (f, errSave) = await fileStorage.saveToFile(file, content); - if (errSave != null) return (null, null); // Fail to save backup - - print(f?.path); + if (errSave != null) { + emit(state.copyWith(error: 'Fail to save backup')); + } - return (backupKey, backupId); + emit(state.copyWith(backupId: backupId, backupKey: backupKey)); } + + void clearError() => emit(state.copyWith(error: '')); } diff --git a/lib/backup/bloc/backup_state.dart b/lib/backup/bloc/backup_state.dart index 42178650f..894bf8181 100644 --- a/lib/backup/bloc/backup_state.dart +++ b/lib/backup/bloc/backup_state.dart @@ -1,8 +1,15 @@ import 'package:bb_mobile/_model/backup.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; -class BackupState { - BackupState({this.loading = false, required this.backups}); +part 'backup_state.freezed.dart'; - final bool loading; - final List backups; +@freezed +class BackupState with _$BackupState { + const factory BackupState({ + @Default(true) bool loading, + @Default([]) List backups, + @Default('') String backupId, + @Default('') String backupKey, + @Default('') String error, + }) = _BackupState; } diff --git a/lib/backup/bloc/keychain_cubit.dart b/lib/backup/bloc/keychain_cubit.dart index 4c710a1da..de076345e 100644 --- a/lib/backup/bloc/keychain_cubit.dart +++ b/lib/backup/bloc/keychain_cubit.dart @@ -1,7 +1,6 @@ import 'dart:convert'; import 'package:bb_mobile/_pkg/crypto.dart'; -import 'package:bb_mobile/_pkg/error.dart'; import 'package:bb_mobile/backup/bloc/keychain_state.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; @@ -9,31 +8,26 @@ import 'package:hex/hex.dart'; import 'package:http/http.dart' as http; class KeychainCubit extends Cubit { - KeychainCubit() : super(KeychainState(secret: '', secretConfirmed: false)); + KeychainCubit() : super(const KeychainState()); - void updateSecret(String value) { - emit(KeychainState(secret: value, secretConfirmed: false)); - } + void clearError() => state.copyWith(error: ''); - void confirmSecret(String value) { - emit( - KeychainState( - secret: state.secret, - secretConfirmed: state.secret == value, - ), - ); - } + void updateSecret(String value) => emit(state.copyWith(secret: value)); - Future secureBackupKey( - String backupId, - String backupKey, - ) async { + void confirmSecret(String value) => + emit(state.copyWith(secretConfirmed: state.secret == value)); + + Future secureBackupKey(String backupId, String backupKey) async { if (state.secret.isEmpty || !state.secretConfirmed) { - return Err('confirm your secret'); + emit(state.copyWith(error: 'confirm your secret')); + return; } final keychainUrl = dotenv.env['KEYCHAIN_URL']; - if (keychainUrl == null) return Err('KEYCHAIN_URL missing from .env'); + if (keychainUrl == null) { + emit(state.copyWith(error: 'KEYCHAIN_URL missing from .env')); + return; + } final secretHashBytes = Crypto.sha256(utf8.encode(state.secret)); final secretHashHex = HEX.encode(secretHashBytes); @@ -50,14 +44,14 @@ class KeychainCubit extends Cubit { ); if (response.statusCode == 201) { - return null; + emit(state.copyWith(completed: true)); } else if (response.statusCode == 403) { - return Err('Key already stored'); + emit(state.copyWith(error: 'Key already stored')); } else { - return Err('Key not secured \n${response.statusCode}'); + emit(state.copyWith(error: 'Key not secured \n${response.statusCode}')); } } catch (e) { - return Err('Server Inaccessible'); + emit(state.copyWith(error: 'Server Inaccessible')); } } } diff --git a/lib/backup/bloc/keychain_state.dart b/lib/backup/bloc/keychain_state.dart index e25bae876..02a4384b0 100644 --- a/lib/backup/bloc/keychain_state.dart +++ b/lib/backup/bloc/keychain_state.dart @@ -1,9 +1,13 @@ -class KeychainState { - KeychainState({ - required this.secret, - required this.secretConfirmed, - }); +import 'package:freezed_annotation/freezed_annotation.dart'; - final String secret; - final bool secretConfirmed; +part 'keychain_state.freezed.dart'; + +@freezed +class KeychainState with _$KeychainState { + const factory KeychainState({ + @Default(false) bool completed, + @Default('') String secret, + @Default(false) bool secretConfirmed, + @Default('') String error, + }) = _KeychainState; } diff --git a/lib/backup/keychain_page.dart b/lib/backup/keychain_page.dart index a7520df47..116b3566d 100644 --- a/lib/backup/keychain_page.dart +++ b/lib/backup/keychain_page.dart @@ -1,7 +1,8 @@ import 'package:bb_mobile/_ui/app_bar.dart'; -import 'package:bb_mobile/_ui/toast.dart'; import 'package:bb_mobile/backup/bloc/keychain_cubit.dart'; import 'package:bb_mobile/backup/bloc/keychain_state.dart'; +import 'package:bb_mobile/home/bloc/home_cubit.dart'; +import 'package:bb_mobile/locator.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; @@ -30,67 +31,81 @@ class KeychainBackupPage extends StatelessWidget { onBack: () => context.pop(), ), ), - body: BlocBuilder( - builder: (context, state) { - final cubit = context.read(); - return Column( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Column( - children: [ - SelectableText('Backup Key: $backupKey'), - SelectableText('Backup ID: $backupId'), - ], + body: BlocListener( + listener: (context, state) { + if (state.error.isNotEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.error), + backgroundColor: Colors.red, ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - SizedBox( - width: 100, - child: TextFormField( - decoration: - const InputDecoration(labelText: 'Enter PIN'), - keyboardType: TextInputType.number, - maxLength: 6, - onChanged: (value) => cubit.updateSecret(value), - ), - ), - SizedBox( - width: 100, - child: TextFormField( - decoration: - const InputDecoration(labelText: 'Confirm PIN'), - keyboardType: TextInputType.number, - obscureText: true, - maxLength: 6, - onChanged: (value) => cubit.confirmSecret(value), - ), - ), - ], + ); + context.read().clearError(); + } + if (state.completed) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Keychain completed'), + backgroundColor: Colors.green, ), - if (state.secretConfirmed) - ElevatedButton( - onPressed: () async { - final err = await cubit.secureBackupKey( - backupId, - backupKey, - ); - final message = err?.message ?? 'Key secured'; - - ScaffoldMessenger.of(context).showSnackBar( - context.showToast(message), - ); - }, - child: const Text('Secure my backup key'), + ); + locator().getWalletsFromStorage(); + context.go('/home'); + } + }, + child: BlocBuilder( + builder: (context, state) { + final cubit = context.read(); + return Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Column( + children: [ + SelectableText('Backup Key: $backupKey'), + SelectableText('Backup ID: $backupId'), + ], ), - if (!state.secretConfirmed) - const Text( - 'PINs do not match! Please confirm your PIN.', - style: TextStyle(color: Colors.red), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + SizedBox( + width: 100, + child: TextFormField( + decoration: + const InputDecoration(labelText: 'Enter PIN'), + keyboardType: TextInputType.number, + maxLength: 6, + onChanged: (value) => cubit.updateSecret(value), + ), + ), + SizedBox( + width: 100, + child: TextFormField( + decoration: + const InputDecoration(labelText: 'Confirm PIN'), + keyboardType: TextInputType.number, + obscureText: true, + maxLength: 6, + onChanged: (value) => cubit.confirmSecret(value), + ), + ), + ], ), - ], - ); - }, + if (state.secretConfirmed) + ElevatedButton( + onPressed: () => + cubit.secureBackupKey(backupId, backupKey), + child: const Text('Secure my backup key'), + ), + if (!state.secretConfirmed) + const Text( + 'PINs do not match! Please confirm your PIN.', + style: TextStyle(color: Colors.red), + ), + ], + ); + }, + ), ), ), ); diff --git a/lib/recover/bloc/manual_cubit.dart b/lib/recover/bloc/manual_cubit.dart new file mode 100644 index 000000000..1e57cd229 --- /dev/null +++ b/lib/recover/bloc/manual_cubit.dart @@ -0,0 +1,178 @@ +import 'dart:convert'; + +import 'package:bb_mobile/_model/backup.dart'; +import 'package:bb_mobile/_model/wallet.dart'; +import 'package:bb_mobile/_pkg/crypto.dart'; +import 'package:bb_mobile/_pkg/file_picker.dart'; +import 'package:bb_mobile/_pkg/wallet/bdk/sensitive_create.dart'; +import 'package:bb_mobile/_pkg/wallet/create.dart'; +import 'package:bb_mobile/_pkg/wallet/create_sensitive.dart'; +import 'package:bb_mobile/_pkg/wallet/lwk/sensitive_create.dart'; +import 'package:bb_mobile/_pkg/wallet/repository/sensitive_storage.dart'; +import 'package:bb_mobile/_pkg/wallet/repository/storage.dart'; +import 'package:bb_mobile/recover/bloc/manual_state.dart'; +import 'package:bb_mobile/wallet/bloc/wallet_bloc.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class ManualCubit extends Cubit { + ManualCubit({ + required this.bdkSensitiveCreate, + required this.lwkSensitiveCreate, + required this.walletSensitiveCreate, + required this.walletsStorageRepository, + required this.walletCreate, + required this.wallets, + required this.walletSensitiveStorage, + required this.filePicker, + }) : super(const ManualState()); + + final FilePick filePicker; + final List wallets; + final WalletSensitiveStorageRepository walletSensitiveStorage; + final WalletsStorageRepository walletsStorageRepository; + final WalletSensitiveCreate walletSensitiveCreate; + final BDKSensitiveCreate bdkSensitiveCreate; + final WalletCreate walletCreate; + final LWKSensitiveCreate lwkSensitiveCreate; + + Future selectFile() async { + final (file, error) = await filePicker.pickFile(); + + if (error != null) { + emit(state.copyWith(error: error.toString())); + return; + } + + if (file == null || file.isEmpty) { + emit(state.copyWith(error: 'Empty file')); + return; + } + + final json = jsonDecode(file); + if (json['encrypted'] == null || json['encrypted'] is! String) { + emit(state.copyWith(error: 'Invalid backup')); + return; + } + + final recovered = await _recoverBackup(json); + if (recovered) { + emit(state.copyWith(recovered: true)); + } + } + + Future _recoverBackup(json) async { + if (state.backupKey.length != 64) { + emit(state.copyWith(error: 'Backup key should be 64 chars')); + return false; + } + + final ciphertext = json['encrypted'] as String; + try { + final plaintext = Crypto.aesDecrypt(ciphertext, state.backupKey); + final decodedJson = jsonDecode(plaintext) as List; + + final backups = decodedJson + .map((item) => Backup.fromJson(item as Map)) + .toList(); + + for (final backup in backups) { + final network = switch (backup.network.toLowerCase()) { + 'mainnet' => BBNetwork.Mainnet, + 'testnet' => BBNetwork.Testnet, + _ => null + }; + + final layer = switch (backup.layer.toLowerCase()) { + 'bitcoin' => BaseWalletType.Bitcoin, + 'liquid' => BaseWalletType.Liquid, + _ => null + }; + + final script = switch (backup.script.toLowerCase()) { + 'bip44' => ScriptType.bip44, + 'bip49' => ScriptType.bip49, + 'bip84' => ScriptType.bip84, + _ => null + }; + + final type = switch (backup.type.toLowerCase()) { + 'main' => BBWalletType.main, + 'xpub' => BBWalletType.xpub, + 'words' => BBWalletType.words, + 'descriptors' => BBWalletType.descriptors, + 'coldcard' => BBWalletType.coldcard, + _ => null + }; + + if (network == null || + layer == null || + script == null || + type == null) { + return false; + } + + if (backup.mnemonic.isNotEmpty) { + _addWallet( + backup.mnemonic.join(' '), + backup.passphrase, + network, + layer, + script, + type, + ); + } + } + + return true; + } catch (e) { + emit(state.copyWith(error: 'Invalid backup key or file')); + return false; + } + } + + void _addWallet( + String mnemonic, + String passphrase, + BBNetwork network, + BaseWalletType layer, + ScriptType script, + BBWalletType type, + ) async { + final (seed, error) = + await walletSensitiveCreate.mnemonicSeed(mnemonic, network); + + await walletSensitiveStorage.newSeed(seed: seed!); + + Wallet? wallet; + switch (layer) { + case BaseWalletType.Bitcoin: + final (btcWallet, btcError) = await bdkSensitiveCreate.oneFromBIP39( + seed: seed, + passphrase: passphrase, + scriptType: script, + network: network, + walletType: type, + walletCreate: walletCreate, + ); + wallet = btcWallet; + + case BaseWalletType.Liquid: + final (liqWallet, liqError) = + await lwkSensitiveCreate.oneLiquidFromBIP39( + seed: seed, + passphrase: passphrase, + scriptType: script, + network: network, + walletType: type, + walletCreate: walletCreate, + ); + wallet = liqWallet; + } + + await walletsStorageRepository.newWallet(wallet!); + } + + void updateBackupKey(String value) => emit(state.copyWith(backupKey: value)); + + void errorDisplayed() => emit(state.copyWith(error: '')); +} diff --git a/lib/recover/bloc/manual_state.dart b/lib/recover/bloc/manual_state.dart new file mode 100644 index 000000000..c18ede489 --- /dev/null +++ b/lib/recover/bloc/manual_state.dart @@ -0,0 +1,12 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'manual_state.freezed.dart'; + +@freezed +class ManualState with _$ManualState { + const factory ManualState({ + @Default('') String error, + @Default(false) bool recovered, + @Default('') String backupKey, + }) = _ManualState; +} diff --git a/lib/recover/manual_page.dart b/lib/recover/manual_page.dart new file mode 100644 index 000000000..275586c93 --- /dev/null +++ b/lib/recover/manual_page.dart @@ -0,0 +1,96 @@ +import 'package:bb_mobile/_pkg/file_picker.dart'; +import 'package:bb_mobile/_pkg/wallet/bdk/sensitive_create.dart'; +import 'package:bb_mobile/_pkg/wallet/create.dart'; +import 'package:bb_mobile/_pkg/wallet/create_sensitive.dart'; +import 'package:bb_mobile/_pkg/wallet/lwk/sensitive_create.dart'; +import 'package:bb_mobile/_pkg/wallet/repository/sensitive_storage.dart'; +import 'package:bb_mobile/_pkg/wallet/repository/storage.dart'; +import 'package:bb_mobile/_ui/app_bar.dart'; +import 'package:bb_mobile/_ui/components/button.dart'; +import 'package:bb_mobile/home/bloc/home_cubit.dart'; +import 'package:bb_mobile/locator.dart'; +import 'package:bb_mobile/recover/bloc/manual_cubit.dart'; +import 'package:bb_mobile/recover/bloc/manual_state.dart'; +import 'package:bb_mobile/wallet/bloc/wallet_bloc.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; + +class RecoverManualPage extends StatelessWidget { + const RecoverManualPage({super.key, required this.wallets}); + + final List wallets; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => ManualCubit( + filePicker: locator(), + walletCreate: locator(), + walletSensitiveCreate: locator(), + walletsStorageRepository: locator(), + wallets: wallets, + walletSensitiveStorage: locator(), + bdkSensitiveCreate: locator(), + lwkSensitiveCreate: locator(), + ), + child: Scaffold( + backgroundColor: Colors.amber, + appBar: AppBar( + flexibleSpace: BBAppBar( + text: 'Recover Backup', + onBack: () => context.pop(), + ), + ), + body: BlocListener( + listener: (context, state) { + if (state.error.isNotEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.error), + backgroundColor: Colors.red, + ), + ); + context.read().errorDisplayed(); + } + if (state.recovered) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Recovery completed'), + backgroundColor: Colors.green, + ), + ); + locator().getWalletsFromStorage(); + context.go('/home'); + } + }, + child: BlocBuilder( + builder: (context, state) { + final cubit = context.read(); + + return Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + if (!state.recovered) + TextFormField( + decoration: + const InputDecoration(labelText: 'Backup Key'), + maxLength: 64, + onChanged: (value) => cubit.updateBackupKey(value), + ), + if (state.backupKey.length == 64 && !state.recovered) + BBButton.big( + label: 'Recover Backup', + center: true, + onPressed: () => cubit.selectFile(), + ), + if (state.recovered) const Text('Successfully recovered'), + ], + ); + }, + ), + ), + ), + ); + } +} diff --git a/lib/routes.dart b/lib/routes.dart index 15ea2946e..224b22f45 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -14,6 +14,7 @@ import 'package:bb_mobile/import/hardware_page.dart'; import 'package:bb_mobile/import/page.dart'; import 'package:bb_mobile/locator.dart'; import 'package:bb_mobile/receive/receive_page.dart'; +import 'package:bb_mobile/recover/manual_page.dart'; import 'package:bb_mobile/send/bloc/send_cubit.dart'; // import 'package:bb_mobile/seeds/seeds_page.dart'; import 'package:bb_mobile/send/send_page.dart'; @@ -197,7 +198,7 @@ GoRouter setupRouter() => GoRouter( }, ), GoRoute( - path: '/backup', + path: '/backupbull', builder: (context, state) { final wallets = state.extra! as List; return TheBackupPage(wallets: wallets); @@ -210,6 +211,13 @@ GoRouter setupRouter() => GoRouter( return KeychainBackupPage(backupKey: backupKey, backupId: backupId); }, ), + GoRoute( + path: '/recoverbull', + builder: (context, state) { + final wallets = state.extra! as List; + return RecoverManualPage(wallets: wallets); + }, + ), // GoRoute( // path: '/wallet-settings/open-test-backup', // builder: (context, state) { diff --git a/lib/settings/core_wallet_settings_page.dart b/lib/settings/core_wallet_settings_page.dart index 1d120269c..8e25a8315 100644 --- a/lib/settings/core_wallet_settings_page.dart +++ b/lib/settings/core_wallet_settings_page.dart @@ -46,7 +46,9 @@ class _Screen extends StatelessWidget { Gap(8), InstantPaymentsWallet(), Gap(8), - BackupButton(), + BackupBullButton(), + Gap(8), + RecoverBullButton(), Gap(8), _ButtonList(), ], @@ -113,18 +115,35 @@ class InstantPaymentsWallet extends StatelessWidget { } } -class BackupButton extends StatelessWidget { - const BackupButton({super.key}); +class BackupBullButton extends StatelessWidget { + const BackupBullButton({super.key}); + + @override + Widget build(BuildContext context) { + return BBButton.textWithStatusAndRightArrow( + label: 'BackupBull', + onPressed: () { + final network = context.read().state.getBBNetwork(); + final wallets = + context.read().state.walletBlocsFromNetwork(network); + context.push('/backupbull', extra: wallets); + }, + ); + } +} + +class RecoverBullButton extends StatelessWidget { + const RecoverBullButton({super.key}); @override Widget build(BuildContext context) { return BBButton.textWithStatusAndRightArrow( - label: 'Backup', + label: 'RecoverBull', onPressed: () { final network = context.read().state.getBBNetwork(); final wallets = context.read().state.walletBlocsFromNetwork(network); - context.push('/backup', extra: wallets); + context.push('/recoverbull', extra: wallets); }, ); } From 2bd223cc5552c56477026983cf576ace683d383d Mon Sep 17 00:00:00 2001 From: ethicnology Date: Wed, 27 Nov 2024 14:41:55 -0500 Subject: [PATCH 011/401] feat: keychain recovery --- lib/_pkg/consts/configs.dart | 2 +- lib/backup/backup_page.dart | 70 ++++++++++------------ lib/backup/bloc/keychain_cubit.dart | 15 +++-- lib/backup/keychain_page.dart | 5 +- lib/recover/bloc/keychain_cubit.dart | 60 +++++++++++++++++++ lib/recover/bloc/keychain_state.dart | 13 ++++ lib/recover/bloc/manual_cubit.dart | 30 +++++----- lib/recover/bloc/manual_state.dart | 2 + lib/recover/keychain_page.dart | 89 ++++++++++++++++++++++++++++ lib/recover/manual_page.dart | 36 ++++++++--- lib/routes.dart | 12 +++- lib/settings/settings_page.dart | 40 +++++++++++++ 12 files changed, 299 insertions(+), 75 deletions(-) create mode 100644 lib/recover/bloc/keychain_cubit.dart create mode 100644 lib/recover/bloc/keychain_state.dart create mode 100644 lib/recover/keychain_page.dart diff --git a/lib/_pkg/consts/configs.dart b/lib/_pkg/consts/configs.dart index 6e7c479f9..3a8f36c8b 100644 --- a/lib/_pkg/consts/configs.dart +++ b/lib/_pkg/consts/configs.dart @@ -2,7 +2,7 @@ import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:lwk/lwk.dart' as lwk; void setupConfigs() {} - +final keychainapi = dotenv.env['KEYCHAIN_API'] ?? ''; final bbmempoolapi = dotenv.env['BB_MEMPOOL_API'] ?? 'mempool.bullbitcoin.com'; final openmempoolapi = dotenv.env['MEMPOOL_API'] ?? 'mempool.space'; final bbexchangeapi = dotenv.env['BB_API'] ?? 'api.bullbitcoin.com/price'; diff --git a/lib/backup/backup_page.dart b/lib/backup/backup_page.dart index f99ef1d70..77c1c961c 100644 --- a/lib/backup/backup_page.dart +++ b/lib/backup/backup_page.dart @@ -9,8 +9,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; -class TheBackupPage extends StatefulWidget { - const TheBackupPage({super.key, required this.wallets}); +class ManualBackupPage extends StatefulWidget { + const ManualBackupPage({super.key, required this.wallets}); final List wallets; @@ -18,7 +18,7 @@ class TheBackupPage extends StatefulWidget { _TheBackupPageState createState() => _TheBackupPageState(); } -class _TheBackupPageState extends State { +class _TheBackupPageState extends State { @override Widget build(BuildContext context) { return BlocProvider( @@ -30,6 +30,7 @@ class _TheBackupPageState extends State { child: Scaffold( backgroundColor: Colors.amber, appBar: AppBar( + automaticallyImplyLeading: false, flexibleSpace: BBAppBar( text: 'Recover Backup', onBack: () => context.pop(), @@ -65,49 +66,38 @@ class _TheBackupPageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ if (state.backupKey.isNotEmpty) - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - border: - Border.all(color: Colors.amber, width: 2), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'Backup Key:', - style: - TextStyle(fontWeight: FontWeight.bold), + Column( + children: [ + const Text( + 'Backup Key:', + style: TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 10), + SelectableText( + state.backupKey, + style: const TextStyle( + fontWeight: FontWeight.bold, ), - const SizedBox(height: 10), - SelectableText( - state.backupKey, - style: const TextStyle( - fontWeight: FontWeight.bold, + ), + if (state.backupId.isNotEmpty) + ElevatedButton( + onPressed: () => context.push( + '/keychain-backup', + extra: (state.backupKey, state.backupId), ), + child: const Text('Keychain'), ), - if (state.backupId.isNotEmpty) - ElevatedButton( - onPressed: () => context.push( - '/keychain-backup', - extra: ( - state.backupKey, - state.backupId - ), - ), - child: const Text('Keychain'), - ), - ], - ), + ], ), const SizedBox(height: 20), if (state.backupKey.isEmpty) - ElevatedButton( - onPressed: context - .read() - .writeEncryptedBackup, - child: const Text('Backup'), + Center( + child: ElevatedButton( + onPressed: context + .read() + .writeEncryptedBackup, + child: const Text('Generate'), + ), ), ], ), diff --git a/lib/backup/bloc/keychain_cubit.dart b/lib/backup/bloc/keychain_cubit.dart index de076345e..144c1f11e 100644 --- a/lib/backup/bloc/keychain_cubit.dart +++ b/lib/backup/bloc/keychain_cubit.dart @@ -1,9 +1,9 @@ import 'dart:convert'; +import 'package:bb_mobile/_pkg/consts/configs.dart'; import 'package:bb_mobile/_pkg/crypto.dart'; import 'package:bb_mobile/backup/bloc/keychain_state.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:hex/hex.dart'; import 'package:http/http.dart' as http; @@ -17,24 +17,27 @@ class KeychainCubit extends Cubit { void confirmSecret(String value) => emit(state.copyWith(secretConfirmed: state.secret == value)); - Future secureBackupKey(String backupId, String backupKey) async { + Future clickSecureKey(String backupId, String backupKey) async { if (state.secret.isEmpty || !state.secretConfirmed) { emit(state.copyWith(error: 'confirm your secret')); return; } - final keychainUrl = dotenv.env['KEYCHAIN_URL']; - if (keychainUrl == null) { - emit(state.copyWith(error: 'KEYCHAIN_URL missing from .env')); + if (keychainapi.isEmpty) { + emit(state.copyWith(error: 'keychain api is not set')); return; } + await _storeBackupKey(backupId, backupKey); + } + + Future _storeBackupKey(String backupId, String backupKey) async { final secretHashBytes = Crypto.sha256(utf8.encode(state.secret)); final secretHashHex = HEX.encode(secretHashBytes); try { final response = await http.post( - Uri.parse('$keychainUrl/store_key'), + Uri.parse('$keychainapi/store_key'), headers: {'Content-Type': 'application/json'}, body: json.encode({ 'backup_id': backupId, diff --git a/lib/backup/keychain_page.dart b/lib/backup/keychain_page.dart index 116b3566d..7c1ee17c1 100644 --- a/lib/backup/keychain_page.dart +++ b/lib/backup/keychain_page.dart @@ -24,8 +24,7 @@ class KeychainBackupPage extends StatelessWidget { child: Scaffold( backgroundColor: Colors.amber, appBar: AppBar( - backgroundColor: Colors.transparent, - elevation: 0, + automaticallyImplyLeading: false, flexibleSpace: BBAppBar( text: 'Keychain Backup', onBack: () => context.pop(), @@ -94,7 +93,7 @@ class KeychainBackupPage extends StatelessWidget { if (state.secretConfirmed) ElevatedButton( onPressed: () => - cubit.secureBackupKey(backupId, backupKey), + cubit.clickSecureKey(backupId, backupKey), child: const Text('Secure my backup key'), ), if (!state.secretConfirmed) diff --git a/lib/recover/bloc/keychain_cubit.dart b/lib/recover/bloc/keychain_cubit.dart new file mode 100644 index 000000000..a797dab0d --- /dev/null +++ b/lib/recover/bloc/keychain_cubit.dart @@ -0,0 +1,60 @@ +import 'dart:convert'; + +import 'package:bb_mobile/_pkg/consts/configs.dart'; +import 'package:bb_mobile/_pkg/crypto.dart'; +import 'package:bb_mobile/_pkg/file_picker.dart'; +import 'package:bb_mobile/recover/bloc/keychain_state.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:hex/hex.dart'; +import 'package:http/http.dart' as http; + +class KeychainCubit extends Cubit { + KeychainCubit({required this.filePicker}) : super(const KeychainState()); + + final FilePick filePicker; + + void clearError() => emit(state.copyWith(error: '')); + void updateSecret(String value) => emit(state.copyWith(secret: value)); + + void clickRecoverKey() async { + if (state.backupId.isEmpty) { + emit(state.copyWith(error: 'backup id is missing')); + return; + } + + if (state.secret.length != 6) { + emit(state.copyWith(error: 'pin should be 6 digits long')); + return; + } + + _recoverBackupKey(state.secret, state.backupId); + } + + void _recoverBackupKey(String secret, String backupId) async { + final secretHash = Crypto.sha256(utf8.encode(secret)); + + if (keychainapi.isEmpty) { + emit(state.copyWith(error: 'keychain api is not set')); + return; + } + + final response = await http.post( + Uri.parse(keychainapi + '/recover_key'), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode({ + 'backup_id': state.backupId, + 'secret_hash': HEX.encode(secretHash), + }), + ); + final body = jsonDecode(response.body); + final backupKey = body['backup_key']; + + if (response.statusCode == 200 && + backupKey != null && + backupKey is String) { + emit(state.copyWith(backupKey: backupKey)); + } else { + emit(state.copyWith(error: '${response.statusCode} | ${response.body}')); + } + } +} diff --git a/lib/recover/bloc/keychain_state.dart b/lib/recover/bloc/keychain_state.dart new file mode 100644 index 000000000..81fd6e7b0 --- /dev/null +++ b/lib/recover/bloc/keychain_state.dart @@ -0,0 +1,13 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'keychain_state.freezed.dart'; + +@freezed +class KeychainState with _$KeychainState { + const factory KeychainState({ + @Default('') String error, + @Default('') String backupKey, + @Default('') String backupId, + @Default('') String secret, + }) = _KeychainState; +} diff --git a/lib/recover/bloc/manual_cubit.dart b/lib/recover/bloc/manual_cubit.dart index 1e57cd229..2570fe56e 100644 --- a/lib/recover/bloc/manual_cubit.dart +++ b/lib/recover/bloc/manual_cubit.dart @@ -35,6 +35,9 @@ class ManualCubit extends Cubit { final WalletCreate walletCreate; final LWKSensitiveCreate lwkSensitiveCreate; + void updateBackupKey(String value) => emit(state.copyWith(backupKey: value)); + void clearError() => emit(state.copyWith(error: '')); + Future selectFile() async { final (file, error) = await filePicker.pickFile(); @@ -49,26 +52,29 @@ class ManualCubit extends Cubit { } final json = jsonDecode(file); - if (json['encrypted'] == null || json['encrypted'] is! String) { + final id = json['id']?.toString() ?? ''; + final encrypted = json['encrypted']?.toString() ?? ''; + if (encrypted.isEmpty || id.isEmpty) { emit(state.copyWith(error: 'Invalid backup')); return; } - final recovered = await _recoverBackup(json); - if (recovered) { - emit(state.copyWith(recovered: true)); - } + emit(state.copyWith(backupId: id, encrypted: encrypted)); + } + + Future clickRecover() async { + final recovered = await _recoverBackup(); + if (recovered) emit(state.copyWith(recovered: true)); } - Future _recoverBackup(json) async { + Future _recoverBackup() async { if (state.backupKey.length != 64) { emit(state.copyWith(error: 'Backup key should be 64 chars')); return false; } - final ciphertext = json['encrypted'] as String; try { - final plaintext = Crypto.aesDecrypt(ciphertext, state.backupKey); + final plaintext = Crypto.aesDecrypt(state.encrypted, state.backupKey); final decodedJson = jsonDecode(plaintext) as List; final backups = decodedJson @@ -112,7 +118,7 @@ class ManualCubit extends Cubit { } if (backup.mnemonic.isNotEmpty) { - _addWallet( + await _addWallet( backup.mnemonic.join(' '), backup.passphrase, network, @@ -130,7 +136,7 @@ class ManualCubit extends Cubit { } } - void _addWallet( + Future _addWallet( String mnemonic, String passphrase, BBNetwork network, @@ -171,8 +177,4 @@ class ManualCubit extends Cubit { await walletsStorageRepository.newWallet(wallet!); } - - void updateBackupKey(String value) => emit(state.copyWith(backupKey: value)); - - void errorDisplayed() => emit(state.copyWith(error: '')); } diff --git a/lib/recover/bloc/manual_state.dart b/lib/recover/bloc/manual_state.dart index c18ede489..e559db9e1 100644 --- a/lib/recover/bloc/manual_state.dart +++ b/lib/recover/bloc/manual_state.dart @@ -8,5 +8,7 @@ class ManualState with _$ManualState { @Default('') String error, @Default(false) bool recovered, @Default('') String backupKey, + @Default('') String backupId, + @Default('') String encrypted, }) = _ManualState; } diff --git a/lib/recover/keychain_page.dart b/lib/recover/keychain_page.dart new file mode 100644 index 000000000..e25a6c5d3 --- /dev/null +++ b/lib/recover/keychain_page.dart @@ -0,0 +1,89 @@ +import 'package:bb_mobile/_pkg/file_picker.dart'; +import 'package:bb_mobile/_ui/app_bar.dart'; +import 'package:bb_mobile/_ui/components/button.dart'; +import 'package:bb_mobile/locator.dart'; +import 'package:bb_mobile/recover/bloc/keychain_cubit.dart'; +import 'package:bb_mobile/recover/bloc/keychain_state.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; + +class KeychainRecoverPage extends StatelessWidget { + const KeychainRecoverPage({super.key, required this.backupId}); + + final String backupId; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => KeychainCubit(filePicker: locator()), + child: Scaffold( + backgroundColor: Colors.amber, + appBar: AppBar( + automaticallyImplyLeading: false, + flexibleSpace: BBAppBar( + text: 'Recover Backup', + onBack: () => context.pop(), + ), + ), + body: BlocListener( + listener: (context, state) { + if (state.error.isNotEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.error), + backgroundColor: Colors.red, + ), + ); + context.read().clearError(); + } + if (state.backupKey.isNotEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Backup Key recovered'), + backgroundColor: Colors.green, + ), + ); + } + }, + child: BlocBuilder( + builder: (context, state) { + final cubit = context.read(); + + final backupKey = state.backupKey; + final secret = state.secret; + + return Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + if (backupKey.isEmpty && backupId.isNotEmpty) + Center( + child: SizedBox( + width: 100, + child: TextFormField( + decoration: + const InputDecoration(labelText: 'Enter PIN'), + keyboardType: TextInputType.number, + maxLength: 6, + onChanged: (value) => cubit.updateSecret(value), + ), + ), + ), + if (backupKey.isEmpty && + backupId.isNotEmpty && + secret.length == 6) + BBButton.big( + label: 'Recover Backup Key', + center: true, + onPressed: () => cubit.clickRecoverKey(), + ), + if (backupKey.isNotEmpty) SelectableText(backupKey), + ], + ); + }, + ), + ), + ), + ); + } +} diff --git a/lib/recover/manual_page.dart b/lib/recover/manual_page.dart index 275586c93..3fc68dc4d 100644 --- a/lib/recover/manual_page.dart +++ b/lib/recover/manual_page.dart @@ -1,3 +1,4 @@ +import 'package:bb_mobile/_pkg/consts/keys.dart'; import 'package:bb_mobile/_pkg/file_picker.dart'; import 'package:bb_mobile/_pkg/wallet/bdk/sensitive_create.dart'; import 'package:bb_mobile/_pkg/wallet/create.dart'; @@ -16,8 +17,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; -class RecoverManualPage extends StatelessWidget { - const RecoverManualPage({super.key, required this.wallets}); +class ManualRecoverPage extends StatelessWidget { + const ManualRecoverPage({super.key, required this.wallets}); final List wallets; @@ -37,13 +38,15 @@ class RecoverManualPage extends StatelessWidget { child: Scaffold( backgroundColor: Colors.amber, appBar: AppBar( + automaticallyImplyLeading: false, flexibleSpace: BBAppBar( text: 'Recover Backup', onBack: () => context.pop(), + buttonKey: UIKeys.settingsBackButton, ), ), body: BlocListener( - listener: (context, state) { + listener: (context, state) async { if (state.error.isNotEmpty) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -51,7 +54,7 @@ class RecoverManualPage extends StatelessWidget { backgroundColor: Colors.red, ), ); - context.read().errorDisplayed(); + context.read().clearError(); } if (state.recovered) { ScaffoldMessenger.of(context).showSnackBar( @@ -60,7 +63,7 @@ class RecoverManualPage extends StatelessWidget { backgroundColor: Colors.green, ), ); - locator().getWalletsFromStorage(); + await locator().getWalletsFromStorage(); context.go('/home'); } }, @@ -71,20 +74,35 @@ class RecoverManualPage extends StatelessWidget { return Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - if (!state.recovered) + if (!state.recovered && + state.backupId.isNotEmpty && + state.backupKey.isEmpty) + ElevatedButton( + onPressed: () => context.push( + '/keychain-recover', + extra: state.backupId, + ), + child: const Text('Keychain'), + ), + if (!state.recovered && state.backupId.isNotEmpty) TextFormField( decoration: const InputDecoration(labelText: 'Backup Key'), maxLength: 64, onChanged: (value) => cubit.updateBackupKey(value), ), - if (state.backupKey.length == 64 && !state.recovered) + if (!state.recovered && state.backupKey.isEmpty) BBButton.big( - label: 'Recover Backup', + label: 'Select File', center: true, onPressed: () => cubit.selectFile(), ), - if (state.recovered) const Text('Successfully recovered'), + if (!state.recovered && state.backupKey.isNotEmpty) + BBButton.big( + label: 'Recover', + center: true, + onPressed: () => cubit.clickRecover(), + ), ], ); }, diff --git a/lib/routes.dart b/lib/routes.dart index 224b22f45..baffa8e7b 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -14,6 +14,7 @@ import 'package:bb_mobile/import/hardware_page.dart'; import 'package:bb_mobile/import/page.dart'; import 'package:bb_mobile/locator.dart'; import 'package:bb_mobile/receive/receive_page.dart'; +import 'package:bb_mobile/recover/keychain_page.dart'; import 'package:bb_mobile/recover/manual_page.dart'; import 'package:bb_mobile/send/bloc/send_cubit.dart'; // import 'package:bb_mobile/seeds/seeds_page.dart'; @@ -201,7 +202,7 @@ GoRouter setupRouter() => GoRouter( path: '/backupbull', builder: (context, state) { final wallets = state.extra! as List; - return TheBackupPage(wallets: wallets); + return ManualBackupPage(wallets: wallets); }, ), GoRoute( @@ -215,7 +216,14 @@ GoRouter setupRouter() => GoRouter( path: '/recoverbull', builder: (context, state) { final wallets = state.extra! as List; - return RecoverManualPage(wallets: wallets); + return ManualRecoverPage(wallets: wallets); + }, + ), + GoRoute( + path: '/keychain-recover', + builder: (context, state) { + final backupId = state.extra! as String; + return KeychainRecoverPage(backupId: backupId); }, ), // GoRoute( diff --git a/lib/settings/settings_page.dart b/lib/settings/settings_page.dart index 1dae6f00b..0ea1206e8 100644 --- a/lib/settings/settings_page.dart +++ b/lib/settings/settings_page.dart @@ -3,7 +3,9 @@ import 'package:bb_mobile/_pkg/launcher.dart'; import 'package:bb_mobile/_ui/app_bar.dart'; import 'package:bb_mobile/_ui/components/button.dart'; import 'package:bb_mobile/_ui/components/text.dart'; +import 'package:bb_mobile/home/bloc/home_cubit.dart'; import 'package:bb_mobile/locator.dart'; +import 'package:bb_mobile/network/bloc/network_cubit.dart'; import 'package:bb_mobile/settings/bloc/settings_cubit.dart'; import 'package:bb_mobile/styles.dart'; import 'package:flutter/material.dart'; @@ -48,6 +50,10 @@ class _Screen extends StatelessWidget { const BitcoinSettingsButton(), const Gap(8), const ApplicationSettingsButton(), + const Gap(8), + const BackupBullButton(), + const Gap(8), + const RecoverBullButton(), const Gap(24), const Center( @@ -157,6 +163,40 @@ class SwapHistoryButton extends StatelessWidget { } } +class BackupBullButton extends StatelessWidget { + const BackupBullButton({super.key}); + + @override + Widget build(BuildContext context) { + return BBButton.textWithStatusAndRightArrow( + label: 'BackupBull', + onPressed: () { + final network = context.read().state.getBBNetwork(); + final wallets = + context.read().state.walletBlocsFromNetwork(network); + context.push('/backupbull', extra: wallets); + }, + ); + } +} + +class RecoverBullButton extends StatelessWidget { + const RecoverBullButton({super.key}); + + @override + Widget build(BuildContext context) { + return BBButton.textWithStatusAndRightArrow( + label: 'RecoverBull', + onPressed: () { + final network = context.read().state.getBBNetwork(); + final wallets = + context.read().state.walletBlocsFromNetwork(network); + context.push('/recoverbull', extra: wallets); + }, + ); + } +} + class ApplicationSettingsButton extends StatelessWidget { const ApplicationSettingsButton({super.key}); From 9f2d9deb6b768124c3aa3b4b0c7db4711fbc965e Mon Sep 17 00:00:00 2001 From: ethicnology Date: Mon, 2 Dec 2024 09:50:46 -0500 Subject: [PATCH 012/401] refactor: re-enable bip85 backupKey derivation --- lib/backup/bloc/backup_cubit.dart | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/lib/backup/bloc/backup_cubit.dart b/lib/backup/bloc/backup_cubit.dart index 2cc88354e..7991cc0c1 100644 --- a/lib/backup/bloc/backup_cubit.dart +++ b/lib/backup/bloc/backup_cubit.dart @@ -9,6 +9,7 @@ import 'package:bb_mobile/_pkg/wallet/repository/sensitive_storage.dart'; import 'package:bb_mobile/backup/bloc/backup_state.dart'; import 'package:bb_mobile/wallet/bloc/wallet_bloc.dart'; import 'package:bdk_flutter/bdk_flutter.dart' as bdk; +import 'package:bip85/bip85.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:hex/hex.dart'; import 'package:intl/intl.dart'; @@ -85,17 +86,10 @@ class BackupCubit extends Cubit { mnemonic: bdkMnemonic, password: '', // TODO: which passphrase? ); - final rootXprv = xprv.toString().substring(0, 64); // remove /* - print('rootXprv: $rootXprv'); + final rootXprv = xprv.toString().substring(0, 111); // remove /* - // const derivation = "m/1608'/0'"; // TODO: key rotation ? - // final derived = derive(xprv: rootXprv, path: derivation); - // print('derived: $derived'); - - // final backupKey = HEX.encode(Crypto.generateRandomBytes(32)); - // TODO: replace by BIP85 - const backupKey = - '23fe885eb43961829d0951f1f7eb251890512c504f6ea88034e6369491f256dd'; + final derived = derive(xprv: rootXprv, path: "m/1608'/0'"); + final backupKey = HEX.encode(derived.sublist(0, 32)); final backupId = HEX.encode(Crypto.generateRandomBytes(32)); final plaintext = json.encode(backups.map((i) => i.toJson()).toList()); From bbcf6b4e64bf56b18bfb3987b776e6a6f1afcc91 Mon Sep 17 00:00:00 2001 From: ethicnology Date: Fri, 6 Dec 2024 10:28:18 -0500 Subject: [PATCH 013/401] feat: social backup --- ios/Podfile.lock | 6 + lib/_pkg/consts/configs.dart | 3 +- lib/backup/bloc/backup_state.freezed.dart | 247 +++++++++++++++++ lib/backup/bloc/keychain_state.freezed.dart | 208 ++++++++++++++ lib/backup/bloc/social_cubit.dart | 220 +++++++++++++++ lib/backup/bloc/social_setting_state.dart | 15 + .../bloc/social_setting_state.freezed.dart | 259 ++++++++++++++++++ lib/backup/bloc/social_settings_cubit.dart | 71 +++++ lib/backup/bloc/social_state.dart | 15 + lib/backup/bloc/social_state.freezed.dart | 254 +++++++++++++++++ lib/backup/social_page.dart | 129 +++++++++ lib/backup/social_settings.dart | 107 ++++++++ lib/backup/tweet_widget.dart | 94 +++++++ lib/main.dart | 2 + lib/recover/bloc/keychain_state.freezed.dart | 206 ++++++++++++++ lib/recover/bloc/manual_state.freezed.dart | 237 ++++++++++++++++ lib/routes.dart | 16 ++ lib/settings/core_wallet_settings_page.dart | 21 -- lib/settings/settings_page.dart | 16 ++ linux/flutter/generated_plugins.cmake | 1 + pubspec.lock | 35 ++- pubspec.yaml | 7 + 22 files changed, 2146 insertions(+), 23 deletions(-) create mode 100644 lib/backup/bloc/backup_state.freezed.dart create mode 100644 lib/backup/bloc/keychain_state.freezed.dart create mode 100644 lib/backup/bloc/social_cubit.dart create mode 100644 lib/backup/bloc/social_setting_state.dart create mode 100644 lib/backup/bloc/social_setting_state.freezed.dart create mode 100644 lib/backup/bloc/social_settings_cubit.dart create mode 100644 lib/backup/bloc/social_state.dart create mode 100644 lib/backup/bloc/social_state.freezed.dart create mode 100644 lib/backup/social_page.dart create mode 100644 lib/backup/social_settings.dart create mode 100644 lib/backup/tweet_widget.dart create mode 100644 lib/recover/bloc/keychain_state.freezed.dart create mode 100644 lib/recover/bloc/manual_state.freezed.dart diff --git a/ios/Podfile.lock b/ios/Podfile.lock index bdf6f4aa5..8b8c0e7fd 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,6 +1,8 @@ PODS: - bdk_flutter (0.31.2): - Flutter + - bip85 (0.0.1): + - Flutter - boltz (0.1.6) - DKImagePickerController/Core (4.3.9): - DKImagePickerController/ImageDataManager @@ -73,6 +75,7 @@ PODS: DEPENDENCIES: - bdk_flutter (from `.symlinks/plugins/bdk_flutter/ios`) + - bip85 (from `.symlinks/plugins/bip85/ios`) - boltz (from `.symlinks/plugins/boltz/ios`) - document_file_save_plus (from `.symlinks/plugins/document_file_save_plus/ios`) - file_picker (from `.symlinks/plugins/file_picker/ios`) @@ -102,6 +105,8 @@ SPEC REPOS: EXTERNAL SOURCES: bdk_flutter: :path: ".symlinks/plugins/bdk_flutter/ios" + bip85: + :path: ".symlinks/plugins/bip85/ios" boltz: :path: ".symlinks/plugins/boltz/ios" document_file_save_plus: @@ -139,6 +144,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: bdk_flutter: fb57a7400a7f3f181c5977bcdc2a5ef347ae4e7f + bip85: f656a7e6b23afda4960efb11c87d51d68e8be3db boltz: 6388ec2412f3753b63a9e65c97f87ea26f43bddc DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 diff --git a/lib/_pkg/consts/configs.dart b/lib/_pkg/consts/configs.dart index 3a8f36c8b..5aed4f491 100644 --- a/lib/_pkg/consts/configs.dart +++ b/lib/_pkg/consts/configs.dart @@ -2,7 +2,8 @@ import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:lwk/lwk.dart' as lwk; void setupConfigs() {} -final keychainapi = dotenv.env['KEYCHAIN_API'] ?? ''; +final keychainapi = dotenv.env['KEYCHAIN_API'] ?? 'http://localhost:3000'; +final socialrelay = dotenv.env['SOCIAL_RELAY'] ?? 'ws://localhost:7000'; final bbmempoolapi = dotenv.env['BB_MEMPOOL_API'] ?? 'mempool.bullbitcoin.com'; final openmempoolapi = dotenv.env['MEMPOOL_API'] ?? 'mempool.space'; final bbexchangeapi = dotenv.env['BB_API'] ?? 'api.bullbitcoin.com/price'; diff --git a/lib/backup/bloc/backup_state.freezed.dart b/lib/backup/bloc/backup_state.freezed.dart new file mode 100644 index 000000000..2a249665a --- /dev/null +++ b/lib/backup/bloc/backup_state.freezed.dart @@ -0,0 +1,247 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'backup_state.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$BackupState { + bool get loading => throw _privateConstructorUsedError; + List get backups => throw _privateConstructorUsedError; + String get backupId => throw _privateConstructorUsedError; + String get backupKey => throw _privateConstructorUsedError; + String get error => throw _privateConstructorUsedError; + + /// Create a copy of BackupState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $BackupStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $BackupStateCopyWith<$Res> { + factory $BackupStateCopyWith( + BackupState value, $Res Function(BackupState) then) = + _$BackupStateCopyWithImpl<$Res, BackupState>; + @useResult + $Res call( + {bool loading, + List backups, + String backupId, + String backupKey, + String error}); +} + +/// @nodoc +class _$BackupStateCopyWithImpl<$Res, $Val extends BackupState> + implements $BackupStateCopyWith<$Res> { + _$BackupStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of BackupState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? loading = null, + Object? backups = null, + Object? backupId = null, + Object? backupKey = null, + Object? error = null, + }) { + return _then(_value.copyWith( + loading: null == loading + ? _value.loading + : loading // ignore: cast_nullable_to_non_nullable + as bool, + backups: null == backups + ? _value.backups + : backups // ignore: cast_nullable_to_non_nullable + as List, + backupId: null == backupId + ? _value.backupId + : backupId // ignore: cast_nullable_to_non_nullable + as String, + backupKey: null == backupKey + ? _value.backupKey + : backupKey // ignore: cast_nullable_to_non_nullable + as String, + error: null == error + ? _value.error + : error // ignore: cast_nullable_to_non_nullable + as String, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$BackupStateImplCopyWith<$Res> + implements $BackupStateCopyWith<$Res> { + factory _$$BackupStateImplCopyWith( + _$BackupStateImpl value, $Res Function(_$BackupStateImpl) then) = + __$$BackupStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {bool loading, + List backups, + String backupId, + String backupKey, + String error}); +} + +/// @nodoc +class __$$BackupStateImplCopyWithImpl<$Res> + extends _$BackupStateCopyWithImpl<$Res, _$BackupStateImpl> + implements _$$BackupStateImplCopyWith<$Res> { + __$$BackupStateImplCopyWithImpl( + _$BackupStateImpl _value, $Res Function(_$BackupStateImpl) _then) + : super(_value, _then); + + /// Create a copy of BackupState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? loading = null, + Object? backups = null, + Object? backupId = null, + Object? backupKey = null, + Object? error = null, + }) { + return _then(_$BackupStateImpl( + loading: null == loading + ? _value.loading + : loading // ignore: cast_nullable_to_non_nullable + as bool, + backups: null == backups + ? _value._backups + : backups // ignore: cast_nullable_to_non_nullable + as List, + backupId: null == backupId + ? _value.backupId + : backupId // ignore: cast_nullable_to_non_nullable + as String, + backupKey: null == backupKey + ? _value.backupKey + : backupKey // ignore: cast_nullable_to_non_nullable + as String, + error: null == error + ? _value.error + : error // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc + +class _$BackupStateImpl implements _BackupState { + const _$BackupStateImpl( + {this.loading = true, + final List backups = const [], + this.backupId = '', + this.backupKey = '', + this.error = ''}) + : _backups = backups; + + @override + @JsonKey() + final bool loading; + final List _backups; + @override + @JsonKey() + List get backups { + if (_backups is EqualUnmodifiableListView) return _backups; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_backups); + } + + @override + @JsonKey() + final String backupId; + @override + @JsonKey() + final String backupKey; + @override + @JsonKey() + final String error; + + @override + String toString() { + return 'BackupState(loading: $loading, backups: $backups, backupId: $backupId, backupKey: $backupKey, error: $error)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$BackupStateImpl && + (identical(other.loading, loading) || other.loading == loading) && + const DeepCollectionEquality().equals(other._backups, _backups) && + (identical(other.backupId, backupId) || + other.backupId == backupId) && + (identical(other.backupKey, backupKey) || + other.backupKey == backupKey) && + (identical(other.error, error) || other.error == error)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + loading, + const DeepCollectionEquality().hash(_backups), + backupId, + backupKey, + error); + + /// Create a copy of BackupState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$BackupStateImplCopyWith<_$BackupStateImpl> get copyWith => + __$$BackupStateImplCopyWithImpl<_$BackupStateImpl>(this, _$identity); +} + +abstract class _BackupState implements BackupState { + const factory _BackupState( + {final bool loading, + final List backups, + final String backupId, + final String backupKey, + final String error}) = _$BackupStateImpl; + + @override + bool get loading; + @override + List get backups; + @override + String get backupId; + @override + String get backupKey; + @override + String get error; + + /// Create a copy of BackupState + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$BackupStateImplCopyWith<_$BackupStateImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/backup/bloc/keychain_state.freezed.dart b/lib/backup/bloc/keychain_state.freezed.dart new file mode 100644 index 000000000..5c749cb1c --- /dev/null +++ b/lib/backup/bloc/keychain_state.freezed.dart @@ -0,0 +1,208 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'keychain_state.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$KeychainState { + bool get completed => throw _privateConstructorUsedError; + String get secret => throw _privateConstructorUsedError; + bool get secretConfirmed => throw _privateConstructorUsedError; + String get error => throw _privateConstructorUsedError; + + /// Create a copy of KeychainState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $KeychainStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $KeychainStateCopyWith<$Res> { + factory $KeychainStateCopyWith( + KeychainState value, $Res Function(KeychainState) then) = + _$KeychainStateCopyWithImpl<$Res, KeychainState>; + @useResult + $Res call( + {bool completed, String secret, bool secretConfirmed, String error}); +} + +/// @nodoc +class _$KeychainStateCopyWithImpl<$Res, $Val extends KeychainState> + implements $KeychainStateCopyWith<$Res> { + _$KeychainStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of KeychainState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? completed = null, + Object? secret = null, + Object? secretConfirmed = null, + Object? error = null, + }) { + return _then(_value.copyWith( + completed: null == completed + ? _value.completed + : completed // ignore: cast_nullable_to_non_nullable + as bool, + secret: null == secret + ? _value.secret + : secret // ignore: cast_nullable_to_non_nullable + as String, + secretConfirmed: null == secretConfirmed + ? _value.secretConfirmed + : secretConfirmed // ignore: cast_nullable_to_non_nullable + as bool, + error: null == error + ? _value.error + : error // ignore: cast_nullable_to_non_nullable + as String, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$KeychainStateImplCopyWith<$Res> + implements $KeychainStateCopyWith<$Res> { + factory _$$KeychainStateImplCopyWith( + _$KeychainStateImpl value, $Res Function(_$KeychainStateImpl) then) = + __$$KeychainStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {bool completed, String secret, bool secretConfirmed, String error}); +} + +/// @nodoc +class __$$KeychainStateImplCopyWithImpl<$Res> + extends _$KeychainStateCopyWithImpl<$Res, _$KeychainStateImpl> + implements _$$KeychainStateImplCopyWith<$Res> { + __$$KeychainStateImplCopyWithImpl( + _$KeychainStateImpl _value, $Res Function(_$KeychainStateImpl) _then) + : super(_value, _then); + + /// Create a copy of KeychainState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? completed = null, + Object? secret = null, + Object? secretConfirmed = null, + Object? error = null, + }) { + return _then(_$KeychainStateImpl( + completed: null == completed + ? _value.completed + : completed // ignore: cast_nullable_to_non_nullable + as bool, + secret: null == secret + ? _value.secret + : secret // ignore: cast_nullable_to_non_nullable + as String, + secretConfirmed: null == secretConfirmed + ? _value.secretConfirmed + : secretConfirmed // ignore: cast_nullable_to_non_nullable + as bool, + error: null == error + ? _value.error + : error // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc + +class _$KeychainStateImpl implements _KeychainState { + const _$KeychainStateImpl( + {this.completed = false, + this.secret = '', + this.secretConfirmed = false, + this.error = ''}); + + @override + @JsonKey() + final bool completed; + @override + @JsonKey() + final String secret; + @override + @JsonKey() + final bool secretConfirmed; + @override + @JsonKey() + final String error; + + @override + String toString() { + return 'KeychainState(completed: $completed, secret: $secret, secretConfirmed: $secretConfirmed, error: $error)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$KeychainStateImpl && + (identical(other.completed, completed) || + other.completed == completed) && + (identical(other.secret, secret) || other.secret == secret) && + (identical(other.secretConfirmed, secretConfirmed) || + other.secretConfirmed == secretConfirmed) && + (identical(other.error, error) || other.error == error)); + } + + @override + int get hashCode => + Object.hash(runtimeType, completed, secret, secretConfirmed, error); + + /// Create a copy of KeychainState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$KeychainStateImplCopyWith<_$KeychainStateImpl> get copyWith => + __$$KeychainStateImplCopyWithImpl<_$KeychainStateImpl>(this, _$identity); +} + +abstract class _KeychainState implements KeychainState { + const factory _KeychainState( + {final bool completed, + final String secret, + final bool secretConfirmed, + final String error}) = _$KeychainStateImpl; + + @override + bool get completed; + @override + String get secret; + @override + bool get secretConfirmed; + @override + String get error; + + /// Create a copy of KeychainState + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$KeychainStateImplCopyWith<_$KeychainStateImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/backup/bloc/social_cubit.dart b/lib/backup/bloc/social_cubit.dart new file mode 100644 index 000000000..3f1d47bb4 --- /dev/null +++ b/lib/backup/bloc/social_cubit.dart @@ -0,0 +1,220 @@ +import 'dart:async'; +import 'dart:collection'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:bb_mobile/_pkg/crypto.dart'; +import 'package:bb_mobile/_pkg/file_storage.dart'; +import 'package:bb_mobile/backup/bloc/social_state.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:hex/hex.dart'; +import 'package:intl/intl.dart'; +import 'package:nostr/nostr.dart'; +import 'package:nostr_sdk/nostr_sdk.dart'; +import 'package:web_socket_channel/web_socket_channel.dart'; + +class SocialCubit extends Cubit { + SocialCubit({ + required this.fileStorage, + required this.relay, + required this.senderPublic, + required this.senderSecret, + required this.friendPublic, + required this.backupKey, + }) : channel = WebSocketChannel.connect(Uri.parse(relay)), + super(const SocialState()) { + _initializeListener(); + _sendInitialRequest(); + } + + final FileStorage fileStorage; + final WebSocketChannel channel; + final String senderPublic; + final String senderSecret; + final String friendPublic; + final String relay; + final String backupKey; + StreamSubscription? _subscription; + + void clearToast() => state.copyWith(toast: ''); + + void _initializeListener() { + _subscription = channel.stream.listen( + (data) async { + try { + var event = Event.deserialize(json.decode(data as String)); + if (event.kind == 1059) { + try { + final unwrapped = await receiveNip17( + receiverSecretKey: senderSecret, + eventJson: json.encode(event.toJson()), + ); + if (unwrapped != null) { + final x = json.decode(unwrapped) as Map; + event = Event.partial( + id: x['id'], + pubkey: x['pubkey'], + createdAt: x['created_at'], + content: x['content'], + ); + + print('is friend: ${event.pubkey == friendPublic}'); + + if (event.pubkey == friendPublic) { + try { + final socialPayload = + json.decode(event.content) as Map; + final type = socialPayload['type'] as String; + final friendBackupKey = + socialPayload['backup_key'] as String; + final friendBackupKeySignature = + socialPayload['backup_key_sig'] as String; + switch (type) { + case 'backup_request': + emit( + state.copyWith( + friendBackupKey: friendBackupKey, + friendBackupKeySignature: friendBackupKeySignature, + ), + ); + default: + } + } catch (e) { + print(e); + } + } + } + } catch (e) { + print(e); + } + } + print('deserialized: ${event.id.substring(0, 6)}'); + emit( + state.copyWith(messages: List.from(state.messages)..add(event)), + ); + } catch (e) { + print(e); + } + }, + onError: (toast) => print(toast), + onDone: () => print('closed'), + ); + } + + void _sendInitialRequest() { + final request = Request(generate64RandomHexChars(), [ + Filter(limit: 0), + ]).serialize(); + channel.sink.add(request); + } + + Future sendPM(String message) async { + final id = await sendNip17( + senderSecretKey: senderSecret, + receiverPublicKey: friendPublic, + relay: relay, + message: message, + ); + + // fake event to display unencrypted locally + final fake = Event.partial( + content: message, + pubkey: senderPublic, + createdAt: currentUnixTimestampSeconds(), + ); + + emit( + state.copyWith( + filter: HashMap.from(state.filter)..[id] = fake, + ), + ); + + print('sent: ${id.substring(0, 7)}'); + return id; + } + + Future backupRequest() async { + final backupKeySig = sign( + signerSecretKey: HEX.decode(senderSecret), + message: HEX.decode(backupKey), + ); + + final payload = json.encode({ + 'type': 'backup_request', + 'backup_key': backupKey, + 'backup_key_sig': HEX.encode(backupKeySig), + }); + + return await sendPM(payload); + } + + @override + Future close() { + _subscription?.cancel(); + channel.sink.close(); + return super.close(); + } + + void backupConfirm() { + final friendBackupKey = state.friendBackupKey; + final friendBackupKeySignature = state.friendBackupKeySignature; + final signerKey = friendPublic; + + final isVerified = verify( + signerPublicKey: HEX.decode(signerKey), + message: HEX.decode(friendBackupKey), + signature: HEX.decode(friendBackupKeySignature), + ); + + if (isVerified == false) { + emit( + state.copyWith( + toast: 'The friendBackupKeySignature does not match the backup key', + ), + ); + return; + } + + final confirmSig = sign( + signerSecretKey: HEX.decode(senderPublic), + message: HEX.decode(friendBackupKey), + ); + + final encrypted = Crypto.aesEncrypt(friendBackupKey, senderSecret); + fileSave(name: friendPublic.substring(0, 6), content: encrypted); + // TODO: encrypt the key –> derivate a new BIP85 key? use nostr keys? + + final payload = json.encode({ + 'type': 'backup_key_downloaded', + 'backup_key_sig': HEX.encode(confirmSig), + }); + + sendPM(payload); + } + + void fileSave({ + required String name, + required String content, + String ext = 'txt', + }) async { + final now = DateTime.now(); + final formattedDate = DateFormat('yyyy-MM-dd_HH-mm-ss').format(now); + final filename = '${name}_$formattedDate.$ext'; + + final (appDir, errDir) = await fileStorage.getAppDirectory(); + if (errDir != null) { + emit(state.copyWith(toast: 'Fail to get Download directory')); + } + + final backupDir = + await Directory(appDir! + '/backups/').create(recursive: true); + final file = File(backupDir.path + filename); + + final (f, errSave) = await fileStorage.saveToFile(file, content); + if (errSave != null) { + emit(state.copyWith(toast: 'Fail to save backup')); + } + + print(file.path); + } +} diff --git a/lib/backup/bloc/social_setting_state.dart b/lib/backup/bloc/social_setting_state.dart new file mode 100644 index 000000000..0f69df00d --- /dev/null +++ b/lib/backup/bloc/social_setting_state.dart @@ -0,0 +1,15 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'social_setting_state.freezed.dart'; + +@freezed +class SocialSettingState with _$SocialSettingState { + const factory SocialSettingState({ + @Default('') String secretKey, + @Default('') String publicKey, + @Default('') String receiverPublicKey, + @Default('') String backupKey, + @Default('') String relay, + @Default('') String error, + }) = _SocialSettingState; +} diff --git a/lib/backup/bloc/social_setting_state.freezed.dart b/lib/backup/bloc/social_setting_state.freezed.dart new file mode 100644 index 000000000..66f3905f5 --- /dev/null +++ b/lib/backup/bloc/social_setting_state.freezed.dart @@ -0,0 +1,259 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'social_setting_state.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$SocialSettingState { + String get secretKey => throw _privateConstructorUsedError; + String get publicKey => throw _privateConstructorUsedError; + String get receiverPublicKey => throw _privateConstructorUsedError; + String get backupKey => throw _privateConstructorUsedError; + String get relay => throw _privateConstructorUsedError; + String get error => throw _privateConstructorUsedError; + + /// Create a copy of SocialSettingState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $SocialSettingStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $SocialSettingStateCopyWith<$Res> { + factory $SocialSettingStateCopyWith( + SocialSettingState value, $Res Function(SocialSettingState) then) = + _$SocialSettingStateCopyWithImpl<$Res, SocialSettingState>; + @useResult + $Res call( + {String secretKey, + String publicKey, + String receiverPublicKey, + String backupKey, + String relay, + String error}); +} + +/// @nodoc +class _$SocialSettingStateCopyWithImpl<$Res, $Val extends SocialSettingState> + implements $SocialSettingStateCopyWith<$Res> { + _$SocialSettingStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of SocialSettingState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? secretKey = null, + Object? publicKey = null, + Object? receiverPublicKey = null, + Object? backupKey = null, + Object? relay = null, + Object? error = null, + }) { + return _then(_value.copyWith( + secretKey: null == secretKey + ? _value.secretKey + : secretKey // ignore: cast_nullable_to_non_nullable + as String, + publicKey: null == publicKey + ? _value.publicKey + : publicKey // ignore: cast_nullable_to_non_nullable + as String, + receiverPublicKey: null == receiverPublicKey + ? _value.receiverPublicKey + : receiverPublicKey // ignore: cast_nullable_to_non_nullable + as String, + backupKey: null == backupKey + ? _value.backupKey + : backupKey // ignore: cast_nullable_to_non_nullable + as String, + relay: null == relay + ? _value.relay + : relay // ignore: cast_nullable_to_non_nullable + as String, + error: null == error + ? _value.error + : error // ignore: cast_nullable_to_non_nullable + as String, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$SocialSettingStateImplCopyWith<$Res> + implements $SocialSettingStateCopyWith<$Res> { + factory _$$SocialSettingStateImplCopyWith(_$SocialSettingStateImpl value, + $Res Function(_$SocialSettingStateImpl) then) = + __$$SocialSettingStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {String secretKey, + String publicKey, + String receiverPublicKey, + String backupKey, + String relay, + String error}); +} + +/// @nodoc +class __$$SocialSettingStateImplCopyWithImpl<$Res> + extends _$SocialSettingStateCopyWithImpl<$Res, _$SocialSettingStateImpl> + implements _$$SocialSettingStateImplCopyWith<$Res> { + __$$SocialSettingStateImplCopyWithImpl(_$SocialSettingStateImpl _value, + $Res Function(_$SocialSettingStateImpl) _then) + : super(_value, _then); + + /// Create a copy of SocialSettingState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? secretKey = null, + Object? publicKey = null, + Object? receiverPublicKey = null, + Object? backupKey = null, + Object? relay = null, + Object? error = null, + }) { + return _then(_$SocialSettingStateImpl( + secretKey: null == secretKey + ? _value.secretKey + : secretKey // ignore: cast_nullable_to_non_nullable + as String, + publicKey: null == publicKey + ? _value.publicKey + : publicKey // ignore: cast_nullable_to_non_nullable + as String, + receiverPublicKey: null == receiverPublicKey + ? _value.receiverPublicKey + : receiverPublicKey // ignore: cast_nullable_to_non_nullable + as String, + backupKey: null == backupKey + ? _value.backupKey + : backupKey // ignore: cast_nullable_to_non_nullable + as String, + relay: null == relay + ? _value.relay + : relay // ignore: cast_nullable_to_non_nullable + as String, + error: null == error + ? _value.error + : error // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc + +class _$SocialSettingStateImpl implements _SocialSettingState { + const _$SocialSettingStateImpl( + {this.secretKey = '', + this.publicKey = '', + this.receiverPublicKey = '', + this.backupKey = '', + this.relay = '', + this.error = ''}); + + @override + @JsonKey() + final String secretKey; + @override + @JsonKey() + final String publicKey; + @override + @JsonKey() + final String receiverPublicKey; + @override + @JsonKey() + final String backupKey; + @override + @JsonKey() + final String relay; + @override + @JsonKey() + final String error; + + @override + String toString() { + return 'SocialSettingState(secretKey: $secretKey, publicKey: $publicKey, receiverPublicKey: $receiverPublicKey, backupKey: $backupKey, relay: $relay, error: $error)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$SocialSettingStateImpl && + (identical(other.secretKey, secretKey) || + other.secretKey == secretKey) && + (identical(other.publicKey, publicKey) || + other.publicKey == publicKey) && + (identical(other.receiverPublicKey, receiverPublicKey) || + other.receiverPublicKey == receiverPublicKey) && + (identical(other.backupKey, backupKey) || + other.backupKey == backupKey) && + (identical(other.relay, relay) || other.relay == relay) && + (identical(other.error, error) || other.error == error)); + } + + @override + int get hashCode => Object.hash(runtimeType, secretKey, publicKey, + receiverPublicKey, backupKey, relay, error); + + /// Create a copy of SocialSettingState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$SocialSettingStateImplCopyWith<_$SocialSettingStateImpl> get copyWith => + __$$SocialSettingStateImplCopyWithImpl<_$SocialSettingStateImpl>( + this, _$identity); +} + +abstract class _SocialSettingState implements SocialSettingState { + const factory _SocialSettingState( + {final String secretKey, + final String publicKey, + final String receiverPublicKey, + final String backupKey, + final String relay, + final String error}) = _$SocialSettingStateImpl; + + @override + String get secretKey; + @override + String get publicKey; + @override + String get receiverPublicKey; + @override + String get backupKey; + @override + String get relay; + @override + String get error; + + /// Create a copy of SocialSettingState + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$SocialSettingStateImplCopyWith<_$SocialSettingStateImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/backup/bloc/social_settings_cubit.dart b/lib/backup/bloc/social_settings_cubit.dart new file mode 100644 index 000000000..1be69c71c --- /dev/null +++ b/lib/backup/bloc/social_settings_cubit.dart @@ -0,0 +1,71 @@ +import 'package:bb_mobile/_pkg/consts/configs.dart'; +import 'package:bb_mobile/backup/bloc/social_setting_state.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:nostr/nostr.dart'; +import 'package:nostr_sdk/nostr_sdk.dart'; + +class SocialSettingsCubit extends Cubit { + SocialSettingsCubit() : super(const SocialSettingState()); + + final form = GlobalKey(); + + void init() { + final random = generate64RandomHexChars(); + final pair = keys(hex: random); + final secret = pair.$1; + final public = pair.$2; + + if (socialrelay.isEmpty) { + emit(state.copyWith(error: 'social nostr relay is not set')); + return; + } + + emit( + state.copyWith( + secretKey: secret, + publicKey: public, + relay: socialrelay, + ), + ); + } + + void clearError() => state.copyWith(error: ''); + + void updateRelay(String value) => emit(state.copyWith(relay: value)); + + void updateBackupKey(String v) => emit(state.copyWith(backupKey: v)); + + void updateSecretKey(String value) { + if (value.length == 64) { + final pair = keys(hex: value); + final secret = pair.$1; + final public = pair.$2; + + emit( + state.copyWith( + secretKey: secret, + publicKey: public, + ), + ); + } else { + emit(state.copyWith(secretKey: value, publicKey: 'N/A')); + } + } + + void updateReceiverPublicKey(String value) => + emit(state.copyWith(receiverPublicKey: value)); + + String? hexValidator(String? value) { + if (value == null || value.isEmpty) { + return 'Input cannot be empty'; + } + + final hexPattern = RegExp(r'^[0-9a-fA-F]+$'); + if (!hexPattern.hasMatch(value)) { + return 'Only hexadecimal characters are allowed'; + } + + return null; + } +} diff --git a/lib/backup/bloc/social_state.dart b/lib/backup/bloc/social_state.dart new file mode 100644 index 000000000..668d833ce --- /dev/null +++ b/lib/backup/bloc/social_state.dart @@ -0,0 +1,15 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:nostr/nostr.dart'; + +part 'social_state.freezed.dart'; + +@freezed +class SocialState with _$SocialState { + const factory SocialState({ + @Default('') String toast, + @Default('') String friendBackupKey, + @Default('') String friendBackupKeySignature, + @Default([]) List messages, + @Default({}) Map filter, + }) = _SocialState; +} diff --git a/lib/backup/bloc/social_state.freezed.dart b/lib/backup/bloc/social_state.freezed.dart new file mode 100644 index 000000000..8043fd8a5 --- /dev/null +++ b/lib/backup/bloc/social_state.freezed.dart @@ -0,0 +1,254 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'social_state.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$SocialState { + String get toast => throw _privateConstructorUsedError; + String get friendBackupKey => throw _privateConstructorUsedError; + String get friendBackupKeySignature => throw _privateConstructorUsedError; + List get messages => throw _privateConstructorUsedError; + Map get filter => throw _privateConstructorUsedError; + + /// Create a copy of SocialState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $SocialStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $SocialStateCopyWith<$Res> { + factory $SocialStateCopyWith( + SocialState value, $Res Function(SocialState) then) = + _$SocialStateCopyWithImpl<$Res, SocialState>; + @useResult + $Res call( + {String toast, + String friendBackupKey, + String friendBackupKeySignature, + List messages, + Map filter}); +} + +/// @nodoc +class _$SocialStateCopyWithImpl<$Res, $Val extends SocialState> + implements $SocialStateCopyWith<$Res> { + _$SocialStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of SocialState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? toast = null, + Object? friendBackupKey = null, + Object? friendBackupKeySignature = null, + Object? messages = null, + Object? filter = null, + }) { + return _then(_value.copyWith( + toast: null == toast + ? _value.toast + : toast // ignore: cast_nullable_to_non_nullable + as String, + friendBackupKey: null == friendBackupKey + ? _value.friendBackupKey + : friendBackupKey // ignore: cast_nullable_to_non_nullable + as String, + friendBackupKeySignature: null == friendBackupKeySignature + ? _value.friendBackupKeySignature + : friendBackupKeySignature // ignore: cast_nullable_to_non_nullable + as String, + messages: null == messages + ? _value.messages + : messages // ignore: cast_nullable_to_non_nullable + as List, + filter: null == filter + ? _value.filter + : filter // ignore: cast_nullable_to_non_nullable + as Map, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$SocialStateImplCopyWith<$Res> + implements $SocialStateCopyWith<$Res> { + factory _$$SocialStateImplCopyWith( + _$SocialStateImpl value, $Res Function(_$SocialStateImpl) then) = + __$$SocialStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {String toast, + String friendBackupKey, + String friendBackupKeySignature, + List messages, + Map filter}); +} + +/// @nodoc +class __$$SocialStateImplCopyWithImpl<$Res> + extends _$SocialStateCopyWithImpl<$Res, _$SocialStateImpl> + implements _$$SocialStateImplCopyWith<$Res> { + __$$SocialStateImplCopyWithImpl( + _$SocialStateImpl _value, $Res Function(_$SocialStateImpl) _then) + : super(_value, _then); + + /// Create a copy of SocialState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? toast = null, + Object? friendBackupKey = null, + Object? friendBackupKeySignature = null, + Object? messages = null, + Object? filter = null, + }) { + return _then(_$SocialStateImpl( + toast: null == toast + ? _value.toast + : toast // ignore: cast_nullable_to_non_nullable + as String, + friendBackupKey: null == friendBackupKey + ? _value.friendBackupKey + : friendBackupKey // ignore: cast_nullable_to_non_nullable + as String, + friendBackupKeySignature: null == friendBackupKeySignature + ? _value.friendBackupKeySignature + : friendBackupKeySignature // ignore: cast_nullable_to_non_nullable + as String, + messages: null == messages + ? _value._messages + : messages // ignore: cast_nullable_to_non_nullable + as List, + filter: null == filter + ? _value._filter + : filter // ignore: cast_nullable_to_non_nullable + as Map, + )); + } +} + +/// @nodoc + +class _$SocialStateImpl implements _SocialState { + const _$SocialStateImpl( + {this.toast = '', + this.friendBackupKey = '', + this.friendBackupKeySignature = '', + final List messages = const [], + final Map filter = const {}}) + : _messages = messages, + _filter = filter; + + @override + @JsonKey() + final String toast; + @override + @JsonKey() + final String friendBackupKey; + @override + @JsonKey() + final String friendBackupKeySignature; + final List _messages; + @override + @JsonKey() + List get messages { + if (_messages is EqualUnmodifiableListView) return _messages; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_messages); + } + + final Map _filter; + @override + @JsonKey() + Map get filter { + if (_filter is EqualUnmodifiableMapView) return _filter; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(_filter); + } + + @override + String toString() { + return 'SocialState(toast: $toast, friendBackupKey: $friendBackupKey, friendBackupKeySignature: $friendBackupKeySignature, messages: $messages, filter: $filter)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$SocialStateImpl && + (identical(other.toast, toast) || other.toast == toast) && + (identical(other.friendBackupKey, friendBackupKey) || + other.friendBackupKey == friendBackupKey) && + (identical( + other.friendBackupKeySignature, friendBackupKeySignature) || + other.friendBackupKeySignature == friendBackupKeySignature) && + const DeepCollectionEquality().equals(other._messages, _messages) && + const DeepCollectionEquality().equals(other._filter, _filter)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + toast, + friendBackupKey, + friendBackupKeySignature, + const DeepCollectionEquality().hash(_messages), + const DeepCollectionEquality().hash(_filter)); + + /// Create a copy of SocialState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$SocialStateImplCopyWith<_$SocialStateImpl> get copyWith => + __$$SocialStateImplCopyWithImpl<_$SocialStateImpl>(this, _$identity); +} + +abstract class _SocialState implements SocialState { + const factory _SocialState( + {final String toast, + final String friendBackupKey, + final String friendBackupKeySignature, + final List messages, + final Map filter}) = _$SocialStateImpl; + + @override + String get toast; + @override + String get friendBackupKey; + @override + String get friendBackupKeySignature; + @override + List get messages; + @override + Map get filter; + + /// Create a copy of SocialState + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$SocialStateImplCopyWith<_$SocialStateImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/backup/social_page.dart b/lib/backup/social_page.dart new file mode 100644 index 000000000..0b4291728 --- /dev/null +++ b/lib/backup/social_page.dart @@ -0,0 +1,129 @@ +import 'package:bb_mobile/_pkg/file_storage.dart'; +import 'package:bb_mobile/_ui/app_bar.dart'; +import 'package:bb_mobile/backup/bloc/social_cubit.dart'; +import 'package:bb_mobile/backup/bloc/social_setting_state.dart'; +import 'package:bb_mobile/backup/bloc/social_state.dart'; +import 'package:bb_mobile/backup/tweet_widget.dart'; +import 'package:bb_mobile/locator.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; + +class SocialPage extends StatelessWidget { + const SocialPage({super.key, required this.settings}); + final SocialSettingState settings; + + @override + Widget build(BuildContext context) { + final message = TextEditingController(); + + return BlocProvider( + create: (_) => SocialCubit( + fileStorage: locator(), + relay: settings.relay, + senderSecret: settings.secretKey, + senderPublic: settings.publicKey, + friendPublic: settings.receiverPublicKey, + backupKey: settings.backupKey, + ), + child: Scaffold( + backgroundColor: Colors.amber, + appBar: AppBar( + automaticallyImplyLeading: false, + flexibleSpace: BBAppBar( + text: 'Social', + onBack: () => context.pop(), + ), + ), + body: BlocListener( + listener: (context, state) { + if (state.toast.isNotEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.toast), + backgroundColor: Colors.red, + ), + ); + context.read().clearToast(); + } + }, + child: BlocBuilder( + builder: (context, state) { + final cubit = context.read(); + + return Column( + children: [ + Expanded( + child: ListView.builder( + itemCount: state.messages.length, + itemBuilder: (context, index) { + final pubkey = state.messages[index].pubkey; + final timestamp = state.messages[index].createdAt; + final content = state.messages[index].content; + final id = state.messages[index].id; + + if (state.filter.containsKey(id)) { + final fake = state.filter[id]!; + return TweetWidget( + pubkey: fake.pubkey, + timestamp: fake.createdAt, + text: fake.content, + ); + } else { + return TweetWidget( + pubkey: pubkey, + timestamp: timestamp, + text: content, + ); + } + }, + ), + ), + Row( + children: [ + ElevatedButton.icon( + icon: const Icon(Icons.backup), + label: const Text('Request Backup'), + onPressed: () async => await cubit.backupRequest(), + ), + if (state.friendBackupKey.isNotEmpty) + ElevatedButton.icon( + icon: const Icon(Icons.cloud_download), + label: const Text('Download Backup Key'), + onPressed: () async => cubit.backupConfirm(), + ), + ], + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + Expanded( + child: TextFormField( + controller: message, + decoration: const InputDecoration( + border: OutlineInputBorder(), + labelText: 'Write Message', + ), + ), + ), + const SizedBox(width: 8), + IconButton( + icon: const Icon(Icons.send), + onPressed: () async { + await cubit.sendPM(message.text); + message.clear(); + }, + ), + ], + ), + ), + ], + ); + }, + ), + ), + ), + ); + } +} diff --git a/lib/backup/social_settings.dart b/lib/backup/social_settings.dart new file mode 100644 index 000000000..dfad95ecb --- /dev/null +++ b/lib/backup/social_settings.dart @@ -0,0 +1,107 @@ +import 'package:bb_mobile/_ui/app_bar.dart'; +import 'package:bb_mobile/_ui/components/button.dart'; +import 'package:bb_mobile/backup/bloc/social_setting_state.dart'; +import 'package:bb_mobile/backup/bloc/social_settings_cubit.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; + +class SocialSettingsPage extends StatefulWidget { + const SocialSettingsPage({super.key}); + + @override + _SocialSettingsPageState createState() => _SocialSettingsPageState(); +} + +class _SocialSettingsPageState extends State { + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => SocialSettingsCubit()..init(), + child: Scaffold( + backgroundColor: Colors.amber, + appBar: AppBar( + automaticallyImplyLeading: false, + flexibleSpace: BBAppBar( + text: 'Social', + onBack: () => context.pop(), + ), + ), + body: BlocListener( + listener: (context, state) { + if (state.error.isNotEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.error), + backgroundColor: Colors.red, + ), + ); + context.read().clearError(); + } + }, + child: BlocBuilder( + builder: (context, state) { + final cubit = context.read(); + + return Form( + key: cubit.form, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + TextFormField( + initialValue: state.relay, + decoration: const InputDecoration(labelText: 'Relay'), + onChanged: (v) => cubit.updateRelay(v), + ), + const SizedBox(height: 16), + TextFormField( + initialValue: state.secretKey, + decoration: + const InputDecoration(labelText: 'Your secret'), + onChanged: (v) => cubit.updateSecretKey(v), + validator: cubit.hexValidator, + maxLength: 64, + ), + SelectableText('Pubkey: ${state.publicKey}'), + const SizedBox(height: 16), + TextFormField( + initialValue: state.receiverPublicKey, + decoration: + const InputDecoration(labelText: 'Friend public'), + onChanged: (v) => cubit.updateReceiverPublicKey(v), + validator: cubit.hexValidator, + maxLength: 64, + ), + const SizedBox(height: 16), + TextFormField( + initialValue: state.backupKey, + decoration: + const InputDecoration(labelText: 'Backup Key'), + onChanged: (v) => cubit.updateBackupKey(v), + validator: cubit.hexValidator, + maxLength: 64, + ), + const SizedBox(height: 16), + if (state.secretKey.length == 64 && + state.publicKey.length == 64) + BBButton.textWithStatusAndRightArrow( + label: 'Social', + onPressed: () { + if (cubit.form.currentState!.validate()) { + context.push('/social', extra: state); + } + }, + ), + ], + ), + ), + ); + }, + ), + ), + ), + ); + } +} diff --git a/lib/backup/tweet_widget.dart b/lib/backup/tweet_widget.dart new file mode 100644 index 000000000..168987af1 --- /dev/null +++ b/lib/backup/tweet_widget.dart @@ -0,0 +1,94 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:oktoast/oktoast.dart'; + +class TweetWidget extends StatelessWidget { + const TweetWidget({ + super.key, + required this.pubkey, + required this.timestamp, + required this.text, + }); + + final String pubkey; + final int timestamp; + final String text; + + String formatDate(int secondsUnixTimestamp) { + final date = DateTime.fromMillisecondsSinceEpoch( + secondsUnixTimestamp * 1000, + isUtc: true, + ).toLocal(); + return "${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')} ${date.hour.toString().padLeft(2, '0')}:${date.minute.toString().padLeft(2, '0')}"; + } + + Color generateColor() { + final hexColor = pubkey.substring(0, 6).padRight(6, '0'); + return Color(int.parse('0xFF$hexColor')); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(5.0), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: Colors.black, + ), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 5), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ClipOval( + child: Container( + width: 46, + height: 46, + color: generateColor(), + child: Image.network( + 'https://robohash.org/$pubkey.png', + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => const SizedBox.shrink(), + ), + ), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + GestureDetector( + onTap: () { + Clipboard.setData(ClipboardData(text: pubkey)); + showToast('Copied to clipboard: $pubkey'); + }, + child: Text( + pubkey.substring(0, 8), + style: const TextStyle(color: Colors.white), + ), + ), + Text( + formatDate(timestamp), + style: const TextStyle(color: Colors.white70), + ), + ], + ), + const SizedBox(height: 10), + SelectableText( + text, + style: const TextStyle(color: Colors.white), + ), + ], + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/main.dart b/lib/main.dart index 1765942e8..16ee02c9e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -29,6 +29,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:go_router/go_router.dart'; import 'package:lwk/lwk.dart'; +import 'package:nostr_sdk/nostr_sdk.dart'; import 'package:oktoast/oktoast.dart'; import 'package:payjoin_flutter/src/generated/frb_generated.dart'; @@ -42,6 +43,7 @@ Future main({bool fromTest = false}) async { await LibLwk.init(); await LibBoltz.init(); await LibBip85.init(); + await Nip17.init(); await dotenv.load(isOptional: true); Bloc.observer = BBlocObserver(); // await FlutterWindowManager.addFlags(FlutterWindowManager.FLAG_SECURE); diff --git a/lib/recover/bloc/keychain_state.freezed.dart b/lib/recover/bloc/keychain_state.freezed.dart new file mode 100644 index 000000000..ff8234347 --- /dev/null +++ b/lib/recover/bloc/keychain_state.freezed.dart @@ -0,0 +1,206 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'keychain_state.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$KeychainState { + String get error => throw _privateConstructorUsedError; + String get backupKey => throw _privateConstructorUsedError; + String get backupId => throw _privateConstructorUsedError; + String get secret => throw _privateConstructorUsedError; + + /// Create a copy of KeychainState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $KeychainStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $KeychainStateCopyWith<$Res> { + factory $KeychainStateCopyWith( + KeychainState value, $Res Function(KeychainState) then) = + _$KeychainStateCopyWithImpl<$Res, KeychainState>; + @useResult + $Res call({String error, String backupKey, String backupId, String secret}); +} + +/// @nodoc +class _$KeychainStateCopyWithImpl<$Res, $Val extends KeychainState> + implements $KeychainStateCopyWith<$Res> { + _$KeychainStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of KeychainState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? error = null, + Object? backupKey = null, + Object? backupId = null, + Object? secret = null, + }) { + return _then(_value.copyWith( + error: null == error + ? _value.error + : error // ignore: cast_nullable_to_non_nullable + as String, + backupKey: null == backupKey + ? _value.backupKey + : backupKey // ignore: cast_nullable_to_non_nullable + as String, + backupId: null == backupId + ? _value.backupId + : backupId // ignore: cast_nullable_to_non_nullable + as String, + secret: null == secret + ? _value.secret + : secret // ignore: cast_nullable_to_non_nullable + as String, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$KeychainStateImplCopyWith<$Res> + implements $KeychainStateCopyWith<$Res> { + factory _$$KeychainStateImplCopyWith( + _$KeychainStateImpl value, $Res Function(_$KeychainStateImpl) then) = + __$$KeychainStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({String error, String backupKey, String backupId, String secret}); +} + +/// @nodoc +class __$$KeychainStateImplCopyWithImpl<$Res> + extends _$KeychainStateCopyWithImpl<$Res, _$KeychainStateImpl> + implements _$$KeychainStateImplCopyWith<$Res> { + __$$KeychainStateImplCopyWithImpl( + _$KeychainStateImpl _value, $Res Function(_$KeychainStateImpl) _then) + : super(_value, _then); + + /// Create a copy of KeychainState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? error = null, + Object? backupKey = null, + Object? backupId = null, + Object? secret = null, + }) { + return _then(_$KeychainStateImpl( + error: null == error + ? _value.error + : error // ignore: cast_nullable_to_non_nullable + as String, + backupKey: null == backupKey + ? _value.backupKey + : backupKey // ignore: cast_nullable_to_non_nullable + as String, + backupId: null == backupId + ? _value.backupId + : backupId // ignore: cast_nullable_to_non_nullable + as String, + secret: null == secret + ? _value.secret + : secret // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc + +class _$KeychainStateImpl implements _KeychainState { + const _$KeychainStateImpl( + {this.error = '', + this.backupKey = '', + this.backupId = '', + this.secret = ''}); + + @override + @JsonKey() + final String error; + @override + @JsonKey() + final String backupKey; + @override + @JsonKey() + final String backupId; + @override + @JsonKey() + final String secret; + + @override + String toString() { + return 'KeychainState(error: $error, backupKey: $backupKey, backupId: $backupId, secret: $secret)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$KeychainStateImpl && + (identical(other.error, error) || other.error == error) && + (identical(other.backupKey, backupKey) || + other.backupKey == backupKey) && + (identical(other.backupId, backupId) || + other.backupId == backupId) && + (identical(other.secret, secret) || other.secret == secret)); + } + + @override + int get hashCode => + Object.hash(runtimeType, error, backupKey, backupId, secret); + + /// Create a copy of KeychainState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$KeychainStateImplCopyWith<_$KeychainStateImpl> get copyWith => + __$$KeychainStateImplCopyWithImpl<_$KeychainStateImpl>(this, _$identity); +} + +abstract class _KeychainState implements KeychainState { + const factory _KeychainState( + {final String error, + final String backupKey, + final String backupId, + final String secret}) = _$KeychainStateImpl; + + @override + String get error; + @override + String get backupKey; + @override + String get backupId; + @override + String get secret; + + /// Create a copy of KeychainState + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$KeychainStateImplCopyWith<_$KeychainStateImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/recover/bloc/manual_state.freezed.dart b/lib/recover/bloc/manual_state.freezed.dart new file mode 100644 index 000000000..317a242fa --- /dev/null +++ b/lib/recover/bloc/manual_state.freezed.dart @@ -0,0 +1,237 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'manual_state.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$ManualState { + String get error => throw _privateConstructorUsedError; + bool get recovered => throw _privateConstructorUsedError; + String get backupKey => throw _privateConstructorUsedError; + String get backupId => throw _privateConstructorUsedError; + String get encrypted => throw _privateConstructorUsedError; + + /// Create a copy of ManualState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $ManualStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ManualStateCopyWith<$Res> { + factory $ManualStateCopyWith( + ManualState value, $Res Function(ManualState) then) = + _$ManualStateCopyWithImpl<$Res, ManualState>; + @useResult + $Res call( + {String error, + bool recovered, + String backupKey, + String backupId, + String encrypted}); +} + +/// @nodoc +class _$ManualStateCopyWithImpl<$Res, $Val extends ManualState> + implements $ManualStateCopyWith<$Res> { + _$ManualStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of ManualState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? error = null, + Object? recovered = null, + Object? backupKey = null, + Object? backupId = null, + Object? encrypted = null, + }) { + return _then(_value.copyWith( + error: null == error + ? _value.error + : error // ignore: cast_nullable_to_non_nullable + as String, + recovered: null == recovered + ? _value.recovered + : recovered // ignore: cast_nullable_to_non_nullable + as bool, + backupKey: null == backupKey + ? _value.backupKey + : backupKey // ignore: cast_nullable_to_non_nullable + as String, + backupId: null == backupId + ? _value.backupId + : backupId // ignore: cast_nullable_to_non_nullable + as String, + encrypted: null == encrypted + ? _value.encrypted + : encrypted // ignore: cast_nullable_to_non_nullable + as String, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$ManualStateImplCopyWith<$Res> + implements $ManualStateCopyWith<$Res> { + factory _$$ManualStateImplCopyWith( + _$ManualStateImpl value, $Res Function(_$ManualStateImpl) then) = + __$$ManualStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {String error, + bool recovered, + String backupKey, + String backupId, + String encrypted}); +} + +/// @nodoc +class __$$ManualStateImplCopyWithImpl<$Res> + extends _$ManualStateCopyWithImpl<$Res, _$ManualStateImpl> + implements _$$ManualStateImplCopyWith<$Res> { + __$$ManualStateImplCopyWithImpl( + _$ManualStateImpl _value, $Res Function(_$ManualStateImpl) _then) + : super(_value, _then); + + /// Create a copy of ManualState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? error = null, + Object? recovered = null, + Object? backupKey = null, + Object? backupId = null, + Object? encrypted = null, + }) { + return _then(_$ManualStateImpl( + error: null == error + ? _value.error + : error // ignore: cast_nullable_to_non_nullable + as String, + recovered: null == recovered + ? _value.recovered + : recovered // ignore: cast_nullable_to_non_nullable + as bool, + backupKey: null == backupKey + ? _value.backupKey + : backupKey // ignore: cast_nullable_to_non_nullable + as String, + backupId: null == backupId + ? _value.backupId + : backupId // ignore: cast_nullable_to_non_nullable + as String, + encrypted: null == encrypted + ? _value.encrypted + : encrypted // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc + +class _$ManualStateImpl implements _ManualState { + const _$ManualStateImpl( + {this.error = '', + this.recovered = false, + this.backupKey = '', + this.backupId = '', + this.encrypted = ''}); + + @override + @JsonKey() + final String error; + @override + @JsonKey() + final bool recovered; + @override + @JsonKey() + final String backupKey; + @override + @JsonKey() + final String backupId; + @override + @JsonKey() + final String encrypted; + + @override + String toString() { + return 'ManualState(error: $error, recovered: $recovered, backupKey: $backupKey, backupId: $backupId, encrypted: $encrypted)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ManualStateImpl && + (identical(other.error, error) || other.error == error) && + (identical(other.recovered, recovered) || + other.recovered == recovered) && + (identical(other.backupKey, backupKey) || + other.backupKey == backupKey) && + (identical(other.backupId, backupId) || + other.backupId == backupId) && + (identical(other.encrypted, encrypted) || + other.encrypted == encrypted)); + } + + @override + int get hashCode => Object.hash( + runtimeType, error, recovered, backupKey, backupId, encrypted); + + /// Create a copy of ManualState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$ManualStateImplCopyWith<_$ManualStateImpl> get copyWith => + __$$ManualStateImplCopyWithImpl<_$ManualStateImpl>(this, _$identity); +} + +abstract class _ManualState implements ManualState { + const factory _ManualState( + {final String error, + final bool recovered, + final String backupKey, + final String backupId, + final String encrypted}) = _$ManualStateImpl; + + @override + String get error; + @override + bool get recovered; + @override + String get backupKey; + @override + String get backupId; + @override + String get encrypted; + + /// Create a copy of ManualState + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$ManualStateImplCopyWith<_$ManualStateImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/routes.dart b/lib/routes.dart index baffa8e7b..160139e9e 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -5,7 +5,10 @@ import 'package:bb_mobile/_model/transaction.dart'; import 'package:bb_mobile/_ui/logger_page.dart'; import 'package:bb_mobile/auth/page.dart'; import 'package:bb_mobile/backup/backup_page.dart'; +import 'package:bb_mobile/backup/bloc/social_setting_state.dart'; import 'package:bb_mobile/backup/keychain_page.dart'; +import 'package:bb_mobile/backup/social_page.dart'; +import 'package:bb_mobile/backup/social_settings.dart'; import 'package:bb_mobile/create/page.dart'; import 'package:bb_mobile/home/home_page.dart'; import 'package:bb_mobile/home/market.dart'; @@ -219,6 +222,19 @@ GoRouter setupRouter() => GoRouter( return ManualRecoverPage(wallets: wallets); }, ), + GoRoute( + path: '/social-settings', + builder: (context, state) { + return const SocialSettingsPage(); + }, + ), + GoRoute( + path: '/social', + builder: (context, state) { + final settings = state.extra! as SocialSettingState; + return SocialPage(settings: settings); + }, + ), GoRoute( path: '/keychain-recover', builder: (context, state) { diff --git a/lib/settings/core_wallet_settings_page.dart b/lib/settings/core_wallet_settings_page.dart index 8e25a8315..8539636b8 100644 --- a/lib/settings/core_wallet_settings_page.dart +++ b/lib/settings/core_wallet_settings_page.dart @@ -46,10 +46,6 @@ class _Screen extends StatelessWidget { Gap(8), InstantPaymentsWallet(), Gap(8), - BackupBullButton(), - Gap(8), - RecoverBullButton(), - Gap(8), _ButtonList(), ], ), @@ -118,23 +114,6 @@ class InstantPaymentsWallet extends StatelessWidget { class BackupBullButton extends StatelessWidget { const BackupBullButton({super.key}); - @override - Widget build(BuildContext context) { - return BBButton.textWithStatusAndRightArrow( - label: 'BackupBull', - onPressed: () { - final network = context.read().state.getBBNetwork(); - final wallets = - context.read().state.walletBlocsFromNetwork(network); - context.push('/backupbull', extra: wallets); - }, - ); - } -} - -class RecoverBullButton extends StatelessWidget { - const RecoverBullButton({super.key}); - @override Widget build(BuildContext context) { return BBButton.textWithStatusAndRightArrow( diff --git a/lib/settings/settings_page.dart b/lib/settings/settings_page.dart index 0ea1206e8..4b35741bd 100644 --- a/lib/settings/settings_page.dart +++ b/lib/settings/settings_page.dart @@ -54,6 +54,8 @@ class _Screen extends StatelessWidget { const BackupBullButton(), const Gap(8), const RecoverBullButton(), + const Gap(8), + const SocialButton(), const Gap(24), const Center( @@ -197,6 +199,20 @@ class RecoverBullButton extends StatelessWidget { } } +class SocialButton extends StatelessWidget { + const SocialButton({super.key}); + + @override + Widget build(BuildContext context) { + return BBButton.textWithStatusAndRightArrow( + label: 'Social Backup', + onPressed: () { + context.push('/social-settings'); + }, + ); + } +} + class ApplicationSettingsButton extends StatelessWidget { const ApplicationSettingsButton({super.key}); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 407c9d317..008e559d3 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -10,6 +10,7 @@ list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST bip85 lwk + nostr_sdk ) set(PLUGIN_BUNDLED_LIBRARIES) diff --git a/pubspec.lock b/pubspec.lock index 85256ced6..3341ec9e9 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -70,6 +70,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.31.2" + bech32: + dependency: transitive + description: + name: bech32 + sha256: "156cbace936f7720c79a79d16a03efad343b1ef17106716e04b8b8e39f99f7f7" + url: "https://pub.dev" + source: hosted + version: "0.2.2" + bip340: + dependency: transitive + description: + name: bip340 + sha256: "2a92f6ed68959f75d67c9a304c17928b9c9449587d4f75ee68f34152f7f69e87" + url: "https://pub.dev" + source: hosted + version: "0.2.0" bip85: dependency: "direct main" description: @@ -982,6 +998,23 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.2" + nostr: + dependency: "direct main" + description: + name: nostr + sha256: a99942e4eedd5823d16f42e6df96240488028666a329ee1047552f79db564123 + url: "https://pub.dev" + source: hosted + version: "1.5.0" + nostr_sdk: + dependency: "direct main" + description: + path: "." + ref: main + resolved-ref: cae2399dcba0a3ed03c0284db26236f9e5e31be6 + url: "https://github.com/ethicnology/dart-nip17" + source: git + version: "0.0.1" oktoast: dependency: "direct main" description: @@ -1581,7 +1614,7 @@ packages: source: hosted version: "0.1.6" web_socket_channel: - dependency: transitive + dependency: "direct main" description: name: web_socket_channel sha256: "0b8e2457400d8a859b7b2030786835a28a8e80836ef64402abef392ff4f1d0e5" diff --git a/pubspec.yaml b/pubspec.yaml index f39e48fab..37a012f4d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -99,6 +99,13 @@ dependencies: url: https://github.com/ethicnology/rust-bip85.git path: bindings/dart-bip85 ref: master + nostr_sdk: + git: + url: https://github.com/ethicnology/dart-nip17 + ref: main + web_socket_channel: ^2.4.5 + nostr: ^1.5.0 + dev_dependencies: build_runner: ^2.4.9 From 790e6dabf28e36067cfc0a6a42792fd589a80413 Mon Sep 17 00:00:00 2001 From: ethicnology Date: Tue, 17 Dec 2024 13:09:28 -0500 Subject: [PATCH 014/401] chore: delete generated files committed before the cleaning on main --- lib/_model/backup.freezed.dart | 389 ------------------ lib/_model/backup.g.dart | 43 -- lib/backup/bloc/backup_state.freezed.dart | 247 ----------- lib/backup/bloc/keychain_state.freezed.dart | 208 ---------- .../bloc/social_setting_state.freezed.dart | 259 ------------ lib/backup/bloc/social_state.freezed.dart | 254 ------------ lib/recover/bloc/keychain_state.freezed.dart | 206 ---------- lib/recover/bloc/manual_state.freezed.dart | 237 ----------- 8 files changed, 1843 deletions(-) delete mode 100644 lib/_model/backup.freezed.dart delete mode 100644 lib/_model/backup.g.dart delete mode 100644 lib/backup/bloc/backup_state.freezed.dart delete mode 100644 lib/backup/bloc/keychain_state.freezed.dart delete mode 100644 lib/backup/bloc/social_setting_state.freezed.dart delete mode 100644 lib/backup/bloc/social_state.freezed.dart delete mode 100644 lib/recover/bloc/keychain_state.freezed.dart delete mode 100644 lib/recover/bloc/manual_state.freezed.dart diff --git a/lib/_model/backup.freezed.dart b/lib/_model/backup.freezed.dart deleted file mode 100644 index 6d95b2ada..000000000 --- a/lib/_model/backup.freezed.dart +++ /dev/null @@ -1,389 +0,0 @@ -// coverage:ignore-file -// GENERATED CODE - DO NOT MODIFY BY HAND -// ignore_for_file: type=lint -// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark - -part of 'backup.dart'; - -// ************************************************************************** -// FreezedGenerator -// ************************************************************************** - -T _$identity(T value) => value; - -final _privateConstructorUsedError = UnsupportedError( - 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); - -Backup _$BackupFromJson(Map json) { - return _Backup.fromJson(json); -} - -/// @nodoc -mixin _$Backup { - int get version => throw _privateConstructorUsedError; - String get name => throw _privateConstructorUsedError; - String get layer => throw _privateConstructorUsedError; - String get network => throw _privateConstructorUsedError; - String get script => throw _privateConstructorUsedError; - String get type => throw _privateConstructorUsedError; - List get mnemonic => throw _privateConstructorUsedError; - String get passphrase => throw _privateConstructorUsedError; - List get labels => throw _privateConstructorUsedError; - List get descriptors => throw _privateConstructorUsedError; - - /// Serializes this Backup to a JSON map. - Map toJson() => throw _privateConstructorUsedError; - - /// Create a copy of Backup - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - $BackupCopyWith get copyWith => throw _privateConstructorUsedError; -} - -/// @nodoc -abstract class $BackupCopyWith<$Res> { - factory $BackupCopyWith(Backup value, $Res Function(Backup) then) = - _$BackupCopyWithImpl<$Res, Backup>; - @useResult - $Res call( - {int version, - String name, - String layer, - String network, - String script, - String type, - List mnemonic, - String passphrase, - List labels, - List descriptors}); -} - -/// @nodoc -class _$BackupCopyWithImpl<$Res, $Val extends Backup> - implements $BackupCopyWith<$Res> { - _$BackupCopyWithImpl(this._value, this._then); - - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; - - /// Create a copy of Backup - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? version = null, - Object? name = null, - Object? layer = null, - Object? network = null, - Object? script = null, - Object? type = null, - Object? mnemonic = null, - Object? passphrase = null, - Object? labels = null, - Object? descriptors = null, - }) { - return _then(_value.copyWith( - version: null == version - ? _value.version - : version // ignore: cast_nullable_to_non_nullable - as int, - name: null == name - ? _value.name - : name // ignore: cast_nullable_to_non_nullable - as String, - layer: null == layer - ? _value.layer - : layer // ignore: cast_nullable_to_non_nullable - as String, - network: null == network - ? _value.network - : network // ignore: cast_nullable_to_non_nullable - as String, - script: null == script - ? _value.script - : script // ignore: cast_nullable_to_non_nullable - as String, - type: null == type - ? _value.type - : type // ignore: cast_nullable_to_non_nullable - as String, - mnemonic: null == mnemonic - ? _value.mnemonic - : mnemonic // ignore: cast_nullable_to_non_nullable - as List, - passphrase: null == passphrase - ? _value.passphrase - : passphrase // ignore: cast_nullable_to_non_nullable - as String, - labels: null == labels - ? _value.labels - : labels // ignore: cast_nullable_to_non_nullable - as List, - descriptors: null == descriptors - ? _value.descriptors - : descriptors // ignore: cast_nullable_to_non_nullable - as List, - ) as $Val); - } -} - -/// @nodoc -abstract class _$$BackupImplCopyWith<$Res> implements $BackupCopyWith<$Res> { - factory _$$BackupImplCopyWith( - _$BackupImpl value, $Res Function(_$BackupImpl) then) = - __$$BackupImplCopyWithImpl<$Res>; - @override - @useResult - $Res call( - {int version, - String name, - String layer, - String network, - String script, - String type, - List mnemonic, - String passphrase, - List labels, - List descriptors}); -} - -/// @nodoc -class __$$BackupImplCopyWithImpl<$Res> - extends _$BackupCopyWithImpl<$Res, _$BackupImpl> - implements _$$BackupImplCopyWith<$Res> { - __$$BackupImplCopyWithImpl( - _$BackupImpl _value, $Res Function(_$BackupImpl) _then) - : super(_value, _then); - - /// Create a copy of Backup - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? version = null, - Object? name = null, - Object? layer = null, - Object? network = null, - Object? script = null, - Object? type = null, - Object? mnemonic = null, - Object? passphrase = null, - Object? labels = null, - Object? descriptors = null, - }) { - return _then(_$BackupImpl( - version: null == version - ? _value.version - : version // ignore: cast_nullable_to_non_nullable - as int, - name: null == name - ? _value.name - : name // ignore: cast_nullable_to_non_nullable - as String, - layer: null == layer - ? _value.layer - : layer // ignore: cast_nullable_to_non_nullable - as String, - network: null == network - ? _value.network - : network // ignore: cast_nullable_to_non_nullable - as String, - script: null == script - ? _value.script - : script // ignore: cast_nullable_to_non_nullable - as String, - type: null == type - ? _value.type - : type // ignore: cast_nullable_to_non_nullable - as String, - mnemonic: null == mnemonic - ? _value._mnemonic - : mnemonic // ignore: cast_nullable_to_non_nullable - as List, - passphrase: null == passphrase - ? _value.passphrase - : passphrase // ignore: cast_nullable_to_non_nullable - as String, - labels: null == labels - ? _value._labels - : labels // ignore: cast_nullable_to_non_nullable - as List, - descriptors: null == descriptors - ? _value._descriptors - : descriptors // ignore: cast_nullable_to_non_nullable - as List, - )); - } -} - -/// @nodoc -@JsonSerializable() -class _$BackupImpl extends _Backup { - const _$BackupImpl( - {this.version = 1, - this.name = '', - this.layer = '', - this.network = '', - this.script = '', - this.type = '', - final List mnemonic = const [], - this.passphrase = '', - final List labels = const [], - final List descriptors = const []}) - : _mnemonic = mnemonic, - _labels = labels, - _descriptors = descriptors, - super._(); - - factory _$BackupImpl.fromJson(Map json) => - _$$BackupImplFromJson(json); - - @override - @JsonKey() - final int version; - @override - @JsonKey() - final String name; - @override - @JsonKey() - final String layer; - @override - @JsonKey() - final String network; - @override - @JsonKey() - final String script; - @override - @JsonKey() - final String type; - final List _mnemonic; - @override - @JsonKey() - List get mnemonic { - if (_mnemonic is EqualUnmodifiableListView) return _mnemonic; - // ignore: implicit_dynamic_type - return EqualUnmodifiableListView(_mnemonic); - } - - @override - @JsonKey() - final String passphrase; - final List _labels; - @override - @JsonKey() - List get labels { - if (_labels is EqualUnmodifiableListView) return _labels; - // ignore: implicit_dynamic_type - return EqualUnmodifiableListView(_labels); - } - - final List _descriptors; - @override - @JsonKey() - List get descriptors { - if (_descriptors is EqualUnmodifiableListView) return _descriptors; - // ignore: implicit_dynamic_type - return EqualUnmodifiableListView(_descriptors); - } - - @override - String toString() { - return 'Backup(version: $version, name: $name, layer: $layer, network: $network, script: $script, type: $type, mnemonic: $mnemonic, passphrase: $passphrase, labels: $labels, descriptors: $descriptors)'; - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$BackupImpl && - (identical(other.version, version) || other.version == version) && - (identical(other.name, name) || other.name == name) && - (identical(other.layer, layer) || other.layer == layer) && - (identical(other.network, network) || other.network == network) && - (identical(other.script, script) || other.script == script) && - (identical(other.type, type) || other.type == type) && - const DeepCollectionEquality().equals(other._mnemonic, _mnemonic) && - (identical(other.passphrase, passphrase) || - other.passphrase == passphrase) && - const DeepCollectionEquality().equals(other._labels, _labels) && - const DeepCollectionEquality() - .equals(other._descriptors, _descriptors)); - } - - @JsonKey(includeFromJson: false, includeToJson: false) - @override - int get hashCode => Object.hash( - runtimeType, - version, - name, - layer, - network, - script, - type, - const DeepCollectionEquality().hash(_mnemonic), - passphrase, - const DeepCollectionEquality().hash(_labels), - const DeepCollectionEquality().hash(_descriptors)); - - /// Create a copy of Backup - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - @override - @pragma('vm:prefer-inline') - _$$BackupImplCopyWith<_$BackupImpl> get copyWith => - __$$BackupImplCopyWithImpl<_$BackupImpl>(this, _$identity); - - @override - Map toJson() { - return _$$BackupImplToJson( - this, - ); - } -} - -abstract class _Backup extends Backup { - const factory _Backup( - {final int version, - final String name, - final String layer, - final String network, - final String script, - final String type, - final List mnemonic, - final String passphrase, - final List labels, - final List descriptors}) = _$BackupImpl; - const _Backup._() : super._(); - - factory _Backup.fromJson(Map json) = _$BackupImpl.fromJson; - - @override - int get version; - @override - String get name; - @override - String get layer; - @override - String get network; - @override - String get script; - @override - String get type; - @override - List get mnemonic; - @override - String get passphrase; - @override - List get labels; - @override - List get descriptors; - - /// Create a copy of Backup - /// with the given fields replaced by the non-null parameter values. - @override - @JsonKey(includeFromJson: false, includeToJson: false) - _$$BackupImplCopyWith<_$BackupImpl> get copyWith => - throw _privateConstructorUsedError; -} diff --git a/lib/_model/backup.g.dart b/lib/_model/backup.g.dart deleted file mode 100644 index 21bc4be6e..000000000 --- a/lib/_model/backup.g.dart +++ /dev/null @@ -1,43 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'backup.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -_$BackupImpl _$$BackupImplFromJson(Map json) => _$BackupImpl( - version: (json['version'] as num?)?.toInt() ?? 1, - name: json['name'] as String? ?? '', - layer: json['layer'] as String? ?? '', - network: json['network'] as String? ?? '', - script: json['script'] as String? ?? '', - type: json['type'] as String? ?? '', - mnemonic: (json['mnemonic'] as List?) - ?.map((e) => e as String) - .toList() ?? - const [], - passphrase: json['passphrase'] as String? ?? '', - labels: (json['labels'] as List?) - ?.map((e) => Bip329Label.fromJson(e as Map)) - .toList() ?? - const [], - descriptors: (json['descriptors'] as List?) - ?.map((e) => e as String) - .toList() ?? - const [], - ); - -Map _$$BackupImplToJson(_$BackupImpl instance) => - { - 'version': instance.version, - 'name': instance.name, - 'layer': instance.layer, - 'network': instance.network, - 'script': instance.script, - 'type': instance.type, - 'mnemonic': instance.mnemonic, - 'passphrase': instance.passphrase, - 'labels': instance.labels, - 'descriptors': instance.descriptors, - }; diff --git a/lib/backup/bloc/backup_state.freezed.dart b/lib/backup/bloc/backup_state.freezed.dart deleted file mode 100644 index 2a249665a..000000000 --- a/lib/backup/bloc/backup_state.freezed.dart +++ /dev/null @@ -1,247 +0,0 @@ -// coverage:ignore-file -// GENERATED CODE - DO NOT MODIFY BY HAND -// ignore_for_file: type=lint -// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark - -part of 'backup_state.dart'; - -// ************************************************************************** -// FreezedGenerator -// ************************************************************************** - -T _$identity(T value) => value; - -final _privateConstructorUsedError = UnsupportedError( - 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); - -/// @nodoc -mixin _$BackupState { - bool get loading => throw _privateConstructorUsedError; - List get backups => throw _privateConstructorUsedError; - String get backupId => throw _privateConstructorUsedError; - String get backupKey => throw _privateConstructorUsedError; - String get error => throw _privateConstructorUsedError; - - /// Create a copy of BackupState - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - $BackupStateCopyWith get copyWith => - throw _privateConstructorUsedError; -} - -/// @nodoc -abstract class $BackupStateCopyWith<$Res> { - factory $BackupStateCopyWith( - BackupState value, $Res Function(BackupState) then) = - _$BackupStateCopyWithImpl<$Res, BackupState>; - @useResult - $Res call( - {bool loading, - List backups, - String backupId, - String backupKey, - String error}); -} - -/// @nodoc -class _$BackupStateCopyWithImpl<$Res, $Val extends BackupState> - implements $BackupStateCopyWith<$Res> { - _$BackupStateCopyWithImpl(this._value, this._then); - - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; - - /// Create a copy of BackupState - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? loading = null, - Object? backups = null, - Object? backupId = null, - Object? backupKey = null, - Object? error = null, - }) { - return _then(_value.copyWith( - loading: null == loading - ? _value.loading - : loading // ignore: cast_nullable_to_non_nullable - as bool, - backups: null == backups - ? _value.backups - : backups // ignore: cast_nullable_to_non_nullable - as List, - backupId: null == backupId - ? _value.backupId - : backupId // ignore: cast_nullable_to_non_nullable - as String, - backupKey: null == backupKey - ? _value.backupKey - : backupKey // ignore: cast_nullable_to_non_nullable - as String, - error: null == error - ? _value.error - : error // ignore: cast_nullable_to_non_nullable - as String, - ) as $Val); - } -} - -/// @nodoc -abstract class _$$BackupStateImplCopyWith<$Res> - implements $BackupStateCopyWith<$Res> { - factory _$$BackupStateImplCopyWith( - _$BackupStateImpl value, $Res Function(_$BackupStateImpl) then) = - __$$BackupStateImplCopyWithImpl<$Res>; - @override - @useResult - $Res call( - {bool loading, - List backups, - String backupId, - String backupKey, - String error}); -} - -/// @nodoc -class __$$BackupStateImplCopyWithImpl<$Res> - extends _$BackupStateCopyWithImpl<$Res, _$BackupStateImpl> - implements _$$BackupStateImplCopyWith<$Res> { - __$$BackupStateImplCopyWithImpl( - _$BackupStateImpl _value, $Res Function(_$BackupStateImpl) _then) - : super(_value, _then); - - /// Create a copy of BackupState - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? loading = null, - Object? backups = null, - Object? backupId = null, - Object? backupKey = null, - Object? error = null, - }) { - return _then(_$BackupStateImpl( - loading: null == loading - ? _value.loading - : loading // ignore: cast_nullable_to_non_nullable - as bool, - backups: null == backups - ? _value._backups - : backups // ignore: cast_nullable_to_non_nullable - as List, - backupId: null == backupId - ? _value.backupId - : backupId // ignore: cast_nullable_to_non_nullable - as String, - backupKey: null == backupKey - ? _value.backupKey - : backupKey // ignore: cast_nullable_to_non_nullable - as String, - error: null == error - ? _value.error - : error // ignore: cast_nullable_to_non_nullable - as String, - )); - } -} - -/// @nodoc - -class _$BackupStateImpl implements _BackupState { - const _$BackupStateImpl( - {this.loading = true, - final List backups = const [], - this.backupId = '', - this.backupKey = '', - this.error = ''}) - : _backups = backups; - - @override - @JsonKey() - final bool loading; - final List _backups; - @override - @JsonKey() - List get backups { - if (_backups is EqualUnmodifiableListView) return _backups; - // ignore: implicit_dynamic_type - return EqualUnmodifiableListView(_backups); - } - - @override - @JsonKey() - final String backupId; - @override - @JsonKey() - final String backupKey; - @override - @JsonKey() - final String error; - - @override - String toString() { - return 'BackupState(loading: $loading, backups: $backups, backupId: $backupId, backupKey: $backupKey, error: $error)'; - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$BackupStateImpl && - (identical(other.loading, loading) || other.loading == loading) && - const DeepCollectionEquality().equals(other._backups, _backups) && - (identical(other.backupId, backupId) || - other.backupId == backupId) && - (identical(other.backupKey, backupKey) || - other.backupKey == backupKey) && - (identical(other.error, error) || other.error == error)); - } - - @override - int get hashCode => Object.hash( - runtimeType, - loading, - const DeepCollectionEquality().hash(_backups), - backupId, - backupKey, - error); - - /// Create a copy of BackupState - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - @override - @pragma('vm:prefer-inline') - _$$BackupStateImplCopyWith<_$BackupStateImpl> get copyWith => - __$$BackupStateImplCopyWithImpl<_$BackupStateImpl>(this, _$identity); -} - -abstract class _BackupState implements BackupState { - const factory _BackupState( - {final bool loading, - final List backups, - final String backupId, - final String backupKey, - final String error}) = _$BackupStateImpl; - - @override - bool get loading; - @override - List get backups; - @override - String get backupId; - @override - String get backupKey; - @override - String get error; - - /// Create a copy of BackupState - /// with the given fields replaced by the non-null parameter values. - @override - @JsonKey(includeFromJson: false, includeToJson: false) - _$$BackupStateImplCopyWith<_$BackupStateImpl> get copyWith => - throw _privateConstructorUsedError; -} diff --git a/lib/backup/bloc/keychain_state.freezed.dart b/lib/backup/bloc/keychain_state.freezed.dart deleted file mode 100644 index 5c749cb1c..000000000 --- a/lib/backup/bloc/keychain_state.freezed.dart +++ /dev/null @@ -1,208 +0,0 @@ -// coverage:ignore-file -// GENERATED CODE - DO NOT MODIFY BY HAND -// ignore_for_file: type=lint -// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark - -part of 'keychain_state.dart'; - -// ************************************************************************** -// FreezedGenerator -// ************************************************************************** - -T _$identity(T value) => value; - -final _privateConstructorUsedError = UnsupportedError( - 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); - -/// @nodoc -mixin _$KeychainState { - bool get completed => throw _privateConstructorUsedError; - String get secret => throw _privateConstructorUsedError; - bool get secretConfirmed => throw _privateConstructorUsedError; - String get error => throw _privateConstructorUsedError; - - /// Create a copy of KeychainState - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - $KeychainStateCopyWith get copyWith => - throw _privateConstructorUsedError; -} - -/// @nodoc -abstract class $KeychainStateCopyWith<$Res> { - factory $KeychainStateCopyWith( - KeychainState value, $Res Function(KeychainState) then) = - _$KeychainStateCopyWithImpl<$Res, KeychainState>; - @useResult - $Res call( - {bool completed, String secret, bool secretConfirmed, String error}); -} - -/// @nodoc -class _$KeychainStateCopyWithImpl<$Res, $Val extends KeychainState> - implements $KeychainStateCopyWith<$Res> { - _$KeychainStateCopyWithImpl(this._value, this._then); - - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; - - /// Create a copy of KeychainState - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? completed = null, - Object? secret = null, - Object? secretConfirmed = null, - Object? error = null, - }) { - return _then(_value.copyWith( - completed: null == completed - ? _value.completed - : completed // ignore: cast_nullable_to_non_nullable - as bool, - secret: null == secret - ? _value.secret - : secret // ignore: cast_nullable_to_non_nullable - as String, - secretConfirmed: null == secretConfirmed - ? _value.secretConfirmed - : secretConfirmed // ignore: cast_nullable_to_non_nullable - as bool, - error: null == error - ? _value.error - : error // ignore: cast_nullable_to_non_nullable - as String, - ) as $Val); - } -} - -/// @nodoc -abstract class _$$KeychainStateImplCopyWith<$Res> - implements $KeychainStateCopyWith<$Res> { - factory _$$KeychainStateImplCopyWith( - _$KeychainStateImpl value, $Res Function(_$KeychainStateImpl) then) = - __$$KeychainStateImplCopyWithImpl<$Res>; - @override - @useResult - $Res call( - {bool completed, String secret, bool secretConfirmed, String error}); -} - -/// @nodoc -class __$$KeychainStateImplCopyWithImpl<$Res> - extends _$KeychainStateCopyWithImpl<$Res, _$KeychainStateImpl> - implements _$$KeychainStateImplCopyWith<$Res> { - __$$KeychainStateImplCopyWithImpl( - _$KeychainStateImpl _value, $Res Function(_$KeychainStateImpl) _then) - : super(_value, _then); - - /// Create a copy of KeychainState - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? completed = null, - Object? secret = null, - Object? secretConfirmed = null, - Object? error = null, - }) { - return _then(_$KeychainStateImpl( - completed: null == completed - ? _value.completed - : completed // ignore: cast_nullable_to_non_nullable - as bool, - secret: null == secret - ? _value.secret - : secret // ignore: cast_nullable_to_non_nullable - as String, - secretConfirmed: null == secretConfirmed - ? _value.secretConfirmed - : secretConfirmed // ignore: cast_nullable_to_non_nullable - as bool, - error: null == error - ? _value.error - : error // ignore: cast_nullable_to_non_nullable - as String, - )); - } -} - -/// @nodoc - -class _$KeychainStateImpl implements _KeychainState { - const _$KeychainStateImpl( - {this.completed = false, - this.secret = '', - this.secretConfirmed = false, - this.error = ''}); - - @override - @JsonKey() - final bool completed; - @override - @JsonKey() - final String secret; - @override - @JsonKey() - final bool secretConfirmed; - @override - @JsonKey() - final String error; - - @override - String toString() { - return 'KeychainState(completed: $completed, secret: $secret, secretConfirmed: $secretConfirmed, error: $error)'; - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$KeychainStateImpl && - (identical(other.completed, completed) || - other.completed == completed) && - (identical(other.secret, secret) || other.secret == secret) && - (identical(other.secretConfirmed, secretConfirmed) || - other.secretConfirmed == secretConfirmed) && - (identical(other.error, error) || other.error == error)); - } - - @override - int get hashCode => - Object.hash(runtimeType, completed, secret, secretConfirmed, error); - - /// Create a copy of KeychainState - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - @override - @pragma('vm:prefer-inline') - _$$KeychainStateImplCopyWith<_$KeychainStateImpl> get copyWith => - __$$KeychainStateImplCopyWithImpl<_$KeychainStateImpl>(this, _$identity); -} - -abstract class _KeychainState implements KeychainState { - const factory _KeychainState( - {final bool completed, - final String secret, - final bool secretConfirmed, - final String error}) = _$KeychainStateImpl; - - @override - bool get completed; - @override - String get secret; - @override - bool get secretConfirmed; - @override - String get error; - - /// Create a copy of KeychainState - /// with the given fields replaced by the non-null parameter values. - @override - @JsonKey(includeFromJson: false, includeToJson: false) - _$$KeychainStateImplCopyWith<_$KeychainStateImpl> get copyWith => - throw _privateConstructorUsedError; -} diff --git a/lib/backup/bloc/social_setting_state.freezed.dart b/lib/backup/bloc/social_setting_state.freezed.dart deleted file mode 100644 index 66f3905f5..000000000 --- a/lib/backup/bloc/social_setting_state.freezed.dart +++ /dev/null @@ -1,259 +0,0 @@ -// coverage:ignore-file -// GENERATED CODE - DO NOT MODIFY BY HAND -// ignore_for_file: type=lint -// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark - -part of 'social_setting_state.dart'; - -// ************************************************************************** -// FreezedGenerator -// ************************************************************************** - -T _$identity(T value) => value; - -final _privateConstructorUsedError = UnsupportedError( - 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); - -/// @nodoc -mixin _$SocialSettingState { - String get secretKey => throw _privateConstructorUsedError; - String get publicKey => throw _privateConstructorUsedError; - String get receiverPublicKey => throw _privateConstructorUsedError; - String get backupKey => throw _privateConstructorUsedError; - String get relay => throw _privateConstructorUsedError; - String get error => throw _privateConstructorUsedError; - - /// Create a copy of SocialSettingState - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - $SocialSettingStateCopyWith get copyWith => - throw _privateConstructorUsedError; -} - -/// @nodoc -abstract class $SocialSettingStateCopyWith<$Res> { - factory $SocialSettingStateCopyWith( - SocialSettingState value, $Res Function(SocialSettingState) then) = - _$SocialSettingStateCopyWithImpl<$Res, SocialSettingState>; - @useResult - $Res call( - {String secretKey, - String publicKey, - String receiverPublicKey, - String backupKey, - String relay, - String error}); -} - -/// @nodoc -class _$SocialSettingStateCopyWithImpl<$Res, $Val extends SocialSettingState> - implements $SocialSettingStateCopyWith<$Res> { - _$SocialSettingStateCopyWithImpl(this._value, this._then); - - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; - - /// Create a copy of SocialSettingState - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? secretKey = null, - Object? publicKey = null, - Object? receiverPublicKey = null, - Object? backupKey = null, - Object? relay = null, - Object? error = null, - }) { - return _then(_value.copyWith( - secretKey: null == secretKey - ? _value.secretKey - : secretKey // ignore: cast_nullable_to_non_nullable - as String, - publicKey: null == publicKey - ? _value.publicKey - : publicKey // ignore: cast_nullable_to_non_nullable - as String, - receiverPublicKey: null == receiverPublicKey - ? _value.receiverPublicKey - : receiverPublicKey // ignore: cast_nullable_to_non_nullable - as String, - backupKey: null == backupKey - ? _value.backupKey - : backupKey // ignore: cast_nullable_to_non_nullable - as String, - relay: null == relay - ? _value.relay - : relay // ignore: cast_nullable_to_non_nullable - as String, - error: null == error - ? _value.error - : error // ignore: cast_nullable_to_non_nullable - as String, - ) as $Val); - } -} - -/// @nodoc -abstract class _$$SocialSettingStateImplCopyWith<$Res> - implements $SocialSettingStateCopyWith<$Res> { - factory _$$SocialSettingStateImplCopyWith(_$SocialSettingStateImpl value, - $Res Function(_$SocialSettingStateImpl) then) = - __$$SocialSettingStateImplCopyWithImpl<$Res>; - @override - @useResult - $Res call( - {String secretKey, - String publicKey, - String receiverPublicKey, - String backupKey, - String relay, - String error}); -} - -/// @nodoc -class __$$SocialSettingStateImplCopyWithImpl<$Res> - extends _$SocialSettingStateCopyWithImpl<$Res, _$SocialSettingStateImpl> - implements _$$SocialSettingStateImplCopyWith<$Res> { - __$$SocialSettingStateImplCopyWithImpl(_$SocialSettingStateImpl _value, - $Res Function(_$SocialSettingStateImpl) _then) - : super(_value, _then); - - /// Create a copy of SocialSettingState - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? secretKey = null, - Object? publicKey = null, - Object? receiverPublicKey = null, - Object? backupKey = null, - Object? relay = null, - Object? error = null, - }) { - return _then(_$SocialSettingStateImpl( - secretKey: null == secretKey - ? _value.secretKey - : secretKey // ignore: cast_nullable_to_non_nullable - as String, - publicKey: null == publicKey - ? _value.publicKey - : publicKey // ignore: cast_nullable_to_non_nullable - as String, - receiverPublicKey: null == receiverPublicKey - ? _value.receiverPublicKey - : receiverPublicKey // ignore: cast_nullable_to_non_nullable - as String, - backupKey: null == backupKey - ? _value.backupKey - : backupKey // ignore: cast_nullable_to_non_nullable - as String, - relay: null == relay - ? _value.relay - : relay // ignore: cast_nullable_to_non_nullable - as String, - error: null == error - ? _value.error - : error // ignore: cast_nullable_to_non_nullable - as String, - )); - } -} - -/// @nodoc - -class _$SocialSettingStateImpl implements _SocialSettingState { - const _$SocialSettingStateImpl( - {this.secretKey = '', - this.publicKey = '', - this.receiverPublicKey = '', - this.backupKey = '', - this.relay = '', - this.error = ''}); - - @override - @JsonKey() - final String secretKey; - @override - @JsonKey() - final String publicKey; - @override - @JsonKey() - final String receiverPublicKey; - @override - @JsonKey() - final String backupKey; - @override - @JsonKey() - final String relay; - @override - @JsonKey() - final String error; - - @override - String toString() { - return 'SocialSettingState(secretKey: $secretKey, publicKey: $publicKey, receiverPublicKey: $receiverPublicKey, backupKey: $backupKey, relay: $relay, error: $error)'; - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$SocialSettingStateImpl && - (identical(other.secretKey, secretKey) || - other.secretKey == secretKey) && - (identical(other.publicKey, publicKey) || - other.publicKey == publicKey) && - (identical(other.receiverPublicKey, receiverPublicKey) || - other.receiverPublicKey == receiverPublicKey) && - (identical(other.backupKey, backupKey) || - other.backupKey == backupKey) && - (identical(other.relay, relay) || other.relay == relay) && - (identical(other.error, error) || other.error == error)); - } - - @override - int get hashCode => Object.hash(runtimeType, secretKey, publicKey, - receiverPublicKey, backupKey, relay, error); - - /// Create a copy of SocialSettingState - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - @override - @pragma('vm:prefer-inline') - _$$SocialSettingStateImplCopyWith<_$SocialSettingStateImpl> get copyWith => - __$$SocialSettingStateImplCopyWithImpl<_$SocialSettingStateImpl>( - this, _$identity); -} - -abstract class _SocialSettingState implements SocialSettingState { - const factory _SocialSettingState( - {final String secretKey, - final String publicKey, - final String receiverPublicKey, - final String backupKey, - final String relay, - final String error}) = _$SocialSettingStateImpl; - - @override - String get secretKey; - @override - String get publicKey; - @override - String get receiverPublicKey; - @override - String get backupKey; - @override - String get relay; - @override - String get error; - - /// Create a copy of SocialSettingState - /// with the given fields replaced by the non-null parameter values. - @override - @JsonKey(includeFromJson: false, includeToJson: false) - _$$SocialSettingStateImplCopyWith<_$SocialSettingStateImpl> get copyWith => - throw _privateConstructorUsedError; -} diff --git a/lib/backup/bloc/social_state.freezed.dart b/lib/backup/bloc/social_state.freezed.dart deleted file mode 100644 index 8043fd8a5..000000000 --- a/lib/backup/bloc/social_state.freezed.dart +++ /dev/null @@ -1,254 +0,0 @@ -// coverage:ignore-file -// GENERATED CODE - DO NOT MODIFY BY HAND -// ignore_for_file: type=lint -// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark - -part of 'social_state.dart'; - -// ************************************************************************** -// FreezedGenerator -// ************************************************************************** - -T _$identity(T value) => value; - -final _privateConstructorUsedError = UnsupportedError( - 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); - -/// @nodoc -mixin _$SocialState { - String get toast => throw _privateConstructorUsedError; - String get friendBackupKey => throw _privateConstructorUsedError; - String get friendBackupKeySignature => throw _privateConstructorUsedError; - List get messages => throw _privateConstructorUsedError; - Map get filter => throw _privateConstructorUsedError; - - /// Create a copy of SocialState - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - $SocialStateCopyWith get copyWith => - throw _privateConstructorUsedError; -} - -/// @nodoc -abstract class $SocialStateCopyWith<$Res> { - factory $SocialStateCopyWith( - SocialState value, $Res Function(SocialState) then) = - _$SocialStateCopyWithImpl<$Res, SocialState>; - @useResult - $Res call( - {String toast, - String friendBackupKey, - String friendBackupKeySignature, - List messages, - Map filter}); -} - -/// @nodoc -class _$SocialStateCopyWithImpl<$Res, $Val extends SocialState> - implements $SocialStateCopyWith<$Res> { - _$SocialStateCopyWithImpl(this._value, this._then); - - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; - - /// Create a copy of SocialState - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? toast = null, - Object? friendBackupKey = null, - Object? friendBackupKeySignature = null, - Object? messages = null, - Object? filter = null, - }) { - return _then(_value.copyWith( - toast: null == toast - ? _value.toast - : toast // ignore: cast_nullable_to_non_nullable - as String, - friendBackupKey: null == friendBackupKey - ? _value.friendBackupKey - : friendBackupKey // ignore: cast_nullable_to_non_nullable - as String, - friendBackupKeySignature: null == friendBackupKeySignature - ? _value.friendBackupKeySignature - : friendBackupKeySignature // ignore: cast_nullable_to_non_nullable - as String, - messages: null == messages - ? _value.messages - : messages // ignore: cast_nullable_to_non_nullable - as List, - filter: null == filter - ? _value.filter - : filter // ignore: cast_nullable_to_non_nullable - as Map, - ) as $Val); - } -} - -/// @nodoc -abstract class _$$SocialStateImplCopyWith<$Res> - implements $SocialStateCopyWith<$Res> { - factory _$$SocialStateImplCopyWith( - _$SocialStateImpl value, $Res Function(_$SocialStateImpl) then) = - __$$SocialStateImplCopyWithImpl<$Res>; - @override - @useResult - $Res call( - {String toast, - String friendBackupKey, - String friendBackupKeySignature, - List messages, - Map filter}); -} - -/// @nodoc -class __$$SocialStateImplCopyWithImpl<$Res> - extends _$SocialStateCopyWithImpl<$Res, _$SocialStateImpl> - implements _$$SocialStateImplCopyWith<$Res> { - __$$SocialStateImplCopyWithImpl( - _$SocialStateImpl _value, $Res Function(_$SocialStateImpl) _then) - : super(_value, _then); - - /// Create a copy of SocialState - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? toast = null, - Object? friendBackupKey = null, - Object? friendBackupKeySignature = null, - Object? messages = null, - Object? filter = null, - }) { - return _then(_$SocialStateImpl( - toast: null == toast - ? _value.toast - : toast // ignore: cast_nullable_to_non_nullable - as String, - friendBackupKey: null == friendBackupKey - ? _value.friendBackupKey - : friendBackupKey // ignore: cast_nullable_to_non_nullable - as String, - friendBackupKeySignature: null == friendBackupKeySignature - ? _value.friendBackupKeySignature - : friendBackupKeySignature // ignore: cast_nullable_to_non_nullable - as String, - messages: null == messages - ? _value._messages - : messages // ignore: cast_nullable_to_non_nullable - as List, - filter: null == filter - ? _value._filter - : filter // ignore: cast_nullable_to_non_nullable - as Map, - )); - } -} - -/// @nodoc - -class _$SocialStateImpl implements _SocialState { - const _$SocialStateImpl( - {this.toast = '', - this.friendBackupKey = '', - this.friendBackupKeySignature = '', - final List messages = const [], - final Map filter = const {}}) - : _messages = messages, - _filter = filter; - - @override - @JsonKey() - final String toast; - @override - @JsonKey() - final String friendBackupKey; - @override - @JsonKey() - final String friendBackupKeySignature; - final List _messages; - @override - @JsonKey() - List get messages { - if (_messages is EqualUnmodifiableListView) return _messages; - // ignore: implicit_dynamic_type - return EqualUnmodifiableListView(_messages); - } - - final Map _filter; - @override - @JsonKey() - Map get filter { - if (_filter is EqualUnmodifiableMapView) return _filter; - // ignore: implicit_dynamic_type - return EqualUnmodifiableMapView(_filter); - } - - @override - String toString() { - return 'SocialState(toast: $toast, friendBackupKey: $friendBackupKey, friendBackupKeySignature: $friendBackupKeySignature, messages: $messages, filter: $filter)'; - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$SocialStateImpl && - (identical(other.toast, toast) || other.toast == toast) && - (identical(other.friendBackupKey, friendBackupKey) || - other.friendBackupKey == friendBackupKey) && - (identical( - other.friendBackupKeySignature, friendBackupKeySignature) || - other.friendBackupKeySignature == friendBackupKeySignature) && - const DeepCollectionEquality().equals(other._messages, _messages) && - const DeepCollectionEquality().equals(other._filter, _filter)); - } - - @override - int get hashCode => Object.hash( - runtimeType, - toast, - friendBackupKey, - friendBackupKeySignature, - const DeepCollectionEquality().hash(_messages), - const DeepCollectionEquality().hash(_filter)); - - /// Create a copy of SocialState - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - @override - @pragma('vm:prefer-inline') - _$$SocialStateImplCopyWith<_$SocialStateImpl> get copyWith => - __$$SocialStateImplCopyWithImpl<_$SocialStateImpl>(this, _$identity); -} - -abstract class _SocialState implements SocialState { - const factory _SocialState( - {final String toast, - final String friendBackupKey, - final String friendBackupKeySignature, - final List messages, - final Map filter}) = _$SocialStateImpl; - - @override - String get toast; - @override - String get friendBackupKey; - @override - String get friendBackupKeySignature; - @override - List get messages; - @override - Map get filter; - - /// Create a copy of SocialState - /// with the given fields replaced by the non-null parameter values. - @override - @JsonKey(includeFromJson: false, includeToJson: false) - _$$SocialStateImplCopyWith<_$SocialStateImpl> get copyWith => - throw _privateConstructorUsedError; -} diff --git a/lib/recover/bloc/keychain_state.freezed.dart b/lib/recover/bloc/keychain_state.freezed.dart deleted file mode 100644 index ff8234347..000000000 --- a/lib/recover/bloc/keychain_state.freezed.dart +++ /dev/null @@ -1,206 +0,0 @@ -// coverage:ignore-file -// GENERATED CODE - DO NOT MODIFY BY HAND -// ignore_for_file: type=lint -// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark - -part of 'keychain_state.dart'; - -// ************************************************************************** -// FreezedGenerator -// ************************************************************************** - -T _$identity(T value) => value; - -final _privateConstructorUsedError = UnsupportedError( - 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); - -/// @nodoc -mixin _$KeychainState { - String get error => throw _privateConstructorUsedError; - String get backupKey => throw _privateConstructorUsedError; - String get backupId => throw _privateConstructorUsedError; - String get secret => throw _privateConstructorUsedError; - - /// Create a copy of KeychainState - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - $KeychainStateCopyWith get copyWith => - throw _privateConstructorUsedError; -} - -/// @nodoc -abstract class $KeychainStateCopyWith<$Res> { - factory $KeychainStateCopyWith( - KeychainState value, $Res Function(KeychainState) then) = - _$KeychainStateCopyWithImpl<$Res, KeychainState>; - @useResult - $Res call({String error, String backupKey, String backupId, String secret}); -} - -/// @nodoc -class _$KeychainStateCopyWithImpl<$Res, $Val extends KeychainState> - implements $KeychainStateCopyWith<$Res> { - _$KeychainStateCopyWithImpl(this._value, this._then); - - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; - - /// Create a copy of KeychainState - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? error = null, - Object? backupKey = null, - Object? backupId = null, - Object? secret = null, - }) { - return _then(_value.copyWith( - error: null == error - ? _value.error - : error // ignore: cast_nullable_to_non_nullable - as String, - backupKey: null == backupKey - ? _value.backupKey - : backupKey // ignore: cast_nullable_to_non_nullable - as String, - backupId: null == backupId - ? _value.backupId - : backupId // ignore: cast_nullable_to_non_nullable - as String, - secret: null == secret - ? _value.secret - : secret // ignore: cast_nullable_to_non_nullable - as String, - ) as $Val); - } -} - -/// @nodoc -abstract class _$$KeychainStateImplCopyWith<$Res> - implements $KeychainStateCopyWith<$Res> { - factory _$$KeychainStateImplCopyWith( - _$KeychainStateImpl value, $Res Function(_$KeychainStateImpl) then) = - __$$KeychainStateImplCopyWithImpl<$Res>; - @override - @useResult - $Res call({String error, String backupKey, String backupId, String secret}); -} - -/// @nodoc -class __$$KeychainStateImplCopyWithImpl<$Res> - extends _$KeychainStateCopyWithImpl<$Res, _$KeychainStateImpl> - implements _$$KeychainStateImplCopyWith<$Res> { - __$$KeychainStateImplCopyWithImpl( - _$KeychainStateImpl _value, $Res Function(_$KeychainStateImpl) _then) - : super(_value, _then); - - /// Create a copy of KeychainState - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? error = null, - Object? backupKey = null, - Object? backupId = null, - Object? secret = null, - }) { - return _then(_$KeychainStateImpl( - error: null == error - ? _value.error - : error // ignore: cast_nullable_to_non_nullable - as String, - backupKey: null == backupKey - ? _value.backupKey - : backupKey // ignore: cast_nullable_to_non_nullable - as String, - backupId: null == backupId - ? _value.backupId - : backupId // ignore: cast_nullable_to_non_nullable - as String, - secret: null == secret - ? _value.secret - : secret // ignore: cast_nullable_to_non_nullable - as String, - )); - } -} - -/// @nodoc - -class _$KeychainStateImpl implements _KeychainState { - const _$KeychainStateImpl( - {this.error = '', - this.backupKey = '', - this.backupId = '', - this.secret = ''}); - - @override - @JsonKey() - final String error; - @override - @JsonKey() - final String backupKey; - @override - @JsonKey() - final String backupId; - @override - @JsonKey() - final String secret; - - @override - String toString() { - return 'KeychainState(error: $error, backupKey: $backupKey, backupId: $backupId, secret: $secret)'; - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$KeychainStateImpl && - (identical(other.error, error) || other.error == error) && - (identical(other.backupKey, backupKey) || - other.backupKey == backupKey) && - (identical(other.backupId, backupId) || - other.backupId == backupId) && - (identical(other.secret, secret) || other.secret == secret)); - } - - @override - int get hashCode => - Object.hash(runtimeType, error, backupKey, backupId, secret); - - /// Create a copy of KeychainState - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - @override - @pragma('vm:prefer-inline') - _$$KeychainStateImplCopyWith<_$KeychainStateImpl> get copyWith => - __$$KeychainStateImplCopyWithImpl<_$KeychainStateImpl>(this, _$identity); -} - -abstract class _KeychainState implements KeychainState { - const factory _KeychainState( - {final String error, - final String backupKey, - final String backupId, - final String secret}) = _$KeychainStateImpl; - - @override - String get error; - @override - String get backupKey; - @override - String get backupId; - @override - String get secret; - - /// Create a copy of KeychainState - /// with the given fields replaced by the non-null parameter values. - @override - @JsonKey(includeFromJson: false, includeToJson: false) - _$$KeychainStateImplCopyWith<_$KeychainStateImpl> get copyWith => - throw _privateConstructorUsedError; -} diff --git a/lib/recover/bloc/manual_state.freezed.dart b/lib/recover/bloc/manual_state.freezed.dart deleted file mode 100644 index 317a242fa..000000000 --- a/lib/recover/bloc/manual_state.freezed.dart +++ /dev/null @@ -1,237 +0,0 @@ -// coverage:ignore-file -// GENERATED CODE - DO NOT MODIFY BY HAND -// ignore_for_file: type=lint -// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark - -part of 'manual_state.dart'; - -// ************************************************************************** -// FreezedGenerator -// ************************************************************************** - -T _$identity(T value) => value; - -final _privateConstructorUsedError = UnsupportedError( - 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); - -/// @nodoc -mixin _$ManualState { - String get error => throw _privateConstructorUsedError; - bool get recovered => throw _privateConstructorUsedError; - String get backupKey => throw _privateConstructorUsedError; - String get backupId => throw _privateConstructorUsedError; - String get encrypted => throw _privateConstructorUsedError; - - /// Create a copy of ManualState - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - $ManualStateCopyWith get copyWith => - throw _privateConstructorUsedError; -} - -/// @nodoc -abstract class $ManualStateCopyWith<$Res> { - factory $ManualStateCopyWith( - ManualState value, $Res Function(ManualState) then) = - _$ManualStateCopyWithImpl<$Res, ManualState>; - @useResult - $Res call( - {String error, - bool recovered, - String backupKey, - String backupId, - String encrypted}); -} - -/// @nodoc -class _$ManualStateCopyWithImpl<$Res, $Val extends ManualState> - implements $ManualStateCopyWith<$Res> { - _$ManualStateCopyWithImpl(this._value, this._then); - - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; - - /// Create a copy of ManualState - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? error = null, - Object? recovered = null, - Object? backupKey = null, - Object? backupId = null, - Object? encrypted = null, - }) { - return _then(_value.copyWith( - error: null == error - ? _value.error - : error // ignore: cast_nullable_to_non_nullable - as String, - recovered: null == recovered - ? _value.recovered - : recovered // ignore: cast_nullable_to_non_nullable - as bool, - backupKey: null == backupKey - ? _value.backupKey - : backupKey // ignore: cast_nullable_to_non_nullable - as String, - backupId: null == backupId - ? _value.backupId - : backupId // ignore: cast_nullable_to_non_nullable - as String, - encrypted: null == encrypted - ? _value.encrypted - : encrypted // ignore: cast_nullable_to_non_nullable - as String, - ) as $Val); - } -} - -/// @nodoc -abstract class _$$ManualStateImplCopyWith<$Res> - implements $ManualStateCopyWith<$Res> { - factory _$$ManualStateImplCopyWith( - _$ManualStateImpl value, $Res Function(_$ManualStateImpl) then) = - __$$ManualStateImplCopyWithImpl<$Res>; - @override - @useResult - $Res call( - {String error, - bool recovered, - String backupKey, - String backupId, - String encrypted}); -} - -/// @nodoc -class __$$ManualStateImplCopyWithImpl<$Res> - extends _$ManualStateCopyWithImpl<$Res, _$ManualStateImpl> - implements _$$ManualStateImplCopyWith<$Res> { - __$$ManualStateImplCopyWithImpl( - _$ManualStateImpl _value, $Res Function(_$ManualStateImpl) _then) - : super(_value, _then); - - /// Create a copy of ManualState - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? error = null, - Object? recovered = null, - Object? backupKey = null, - Object? backupId = null, - Object? encrypted = null, - }) { - return _then(_$ManualStateImpl( - error: null == error - ? _value.error - : error // ignore: cast_nullable_to_non_nullable - as String, - recovered: null == recovered - ? _value.recovered - : recovered // ignore: cast_nullable_to_non_nullable - as bool, - backupKey: null == backupKey - ? _value.backupKey - : backupKey // ignore: cast_nullable_to_non_nullable - as String, - backupId: null == backupId - ? _value.backupId - : backupId // ignore: cast_nullable_to_non_nullable - as String, - encrypted: null == encrypted - ? _value.encrypted - : encrypted // ignore: cast_nullable_to_non_nullable - as String, - )); - } -} - -/// @nodoc - -class _$ManualStateImpl implements _ManualState { - const _$ManualStateImpl( - {this.error = '', - this.recovered = false, - this.backupKey = '', - this.backupId = '', - this.encrypted = ''}); - - @override - @JsonKey() - final String error; - @override - @JsonKey() - final bool recovered; - @override - @JsonKey() - final String backupKey; - @override - @JsonKey() - final String backupId; - @override - @JsonKey() - final String encrypted; - - @override - String toString() { - return 'ManualState(error: $error, recovered: $recovered, backupKey: $backupKey, backupId: $backupId, encrypted: $encrypted)'; - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$ManualStateImpl && - (identical(other.error, error) || other.error == error) && - (identical(other.recovered, recovered) || - other.recovered == recovered) && - (identical(other.backupKey, backupKey) || - other.backupKey == backupKey) && - (identical(other.backupId, backupId) || - other.backupId == backupId) && - (identical(other.encrypted, encrypted) || - other.encrypted == encrypted)); - } - - @override - int get hashCode => Object.hash( - runtimeType, error, recovered, backupKey, backupId, encrypted); - - /// Create a copy of ManualState - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - @override - @pragma('vm:prefer-inline') - _$$ManualStateImplCopyWith<_$ManualStateImpl> get copyWith => - __$$ManualStateImplCopyWithImpl<_$ManualStateImpl>(this, _$identity); -} - -abstract class _ManualState implements ManualState { - const factory _ManualState( - {final String error, - final bool recovered, - final String backupKey, - final String backupId, - final String encrypted}) = _$ManualStateImpl; - - @override - String get error; - @override - bool get recovered; - @override - String get backupKey; - @override - String get backupId; - @override - String get encrypted; - - /// Create a copy of ManualState - /// with the given fields replaced by the non-null parameter values. - @override - @JsonKey(includeFromJson: false, includeToJson: false) - _$$ManualStateImplCopyWith<_$ManualStateImpl> get copyWith => - throw _privateConstructorUsedError; -} From 2fc348eda5e8070d4c1d73963e028a5d5d4186b0 Mon Sep 17 00:00:00 2001 From: ethicnology Date: Tue, 17 Dec 2024 13:09:54 -0500 Subject: [PATCH 015/401] chore: update lock files --- ios/Podfile.lock | 6 ++++++ pubspec.lock | 7 ++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 8b8c0e7fd..b80316c56 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -57,6 +57,8 @@ PODS: - "no_screenshot (0.0.1+4)": - Flutter - ScreenProtectorKit (~> 1.3.1) + - nostr_sdk (0.0.1): + - Flutter - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS @@ -88,6 +90,7 @@ DEPENDENCIES: - integration_test (from `.symlinks/plugins/integration_test/ios`) - lwk (from `.symlinks/plugins/lwk/ios`) - no_screenshot (from `.symlinks/plugins/no_screenshot/ios`) + - nostr_sdk (from `.symlinks/plugins/nostr_sdk/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - payjoin_flutter (from `.symlinks/plugins/payjoin_flutter/ios`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) @@ -131,6 +134,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/lwk/ios" no_screenshot: :path: ".symlinks/plugins/no_screenshot/ios" + nostr_sdk: + :path: ".symlinks/plugins/nostr_sdk/ios" path_provider_foundation: :path: ".symlinks/plugins/path_provider_foundation/darwin" payjoin_flutter: @@ -159,6 +164,7 @@ SPEC CHECKSUMS: integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573 lwk: 22e06bc5664247d6b2dac91cfe209b63b70dd580 no_screenshot: 67d110f12466f4913b488803d4e498d03ef2889e + nostr_sdk: ef4a055cafc285eb55c5e44057da60bfc96cf3d0 path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 payjoin_flutter: 6397d7b698cdad6453be4949ab6aca1863f6c5e5 permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 diff --git a/pubspec.lock b/pubspec.lock index 3341ec9e9..c45b81cde 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -91,7 +91,7 @@ packages: description: path: "bindings/dart-bip85" ref: master - resolved-ref: "2321b17f3e1c74f15bdb95638bb8c65bbbe2a2af" + resolved-ref: beee977ad2608987ceb350ef3bc3cc05c9e36455 url: "https://github.com/ethicnology/rust-bip85.git" source: git version: "1.0.2" @@ -1670,5 +1670,10 @@ packages: source: hosted version: "3.1.3" sdks: +<<<<<<< HEAD dart: ">=3.6.0 <4.0.0" flutter: ">=3.27.0" +======= + dart: ">=3.5.4 <4.0.0" + flutter: ">=3.24.0" +>>>>>>> 254ad28a (chore: update lock files) From 287b10ee205673aa1a81f0b678d00c61b8cdbf73 Mon Sep 17 00:00:00 2001 From: ethicnology Date: Tue, 17 Dec 2024 13:28:46 -0500 Subject: [PATCH 016/401] refactor: since rebase with cleaned main --- lib/_model/bip329_label.dart | 7 +++++-- lib/_pkg/crypto.dart | 2 +- lib/backup/bloc/backup_cubit.dart | 2 +- lib/backup/bloc/keychain_cubit.dart | 12 ++++++------ 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/lib/_model/bip329_label.dart b/lib/_model/bip329_label.dart index c73e91e58..dfa549295 100644 --- a/lib/_model/bip329_label.dart +++ b/lib/_model/bip329_label.dart @@ -2,11 +2,14 @@ import 'dart:convert'; import 'dart:io'; +import 'dart:math'; +import 'dart:typed_data'; -import 'package:bb_mobile/_pkg/crypto.dart'; import 'package:bb_mobile/_pkg/error.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:path_provider/path_provider.dart'; +import 'package:pointycastle/api.dart'; +import 'package:pointycastle/export.dart' as pc; part 'bip329_label.freezed.dart'; part 'bip329_label.g.dart'; @@ -69,7 +72,7 @@ extension Bip329LabelHelpers on Bip329Label { ); } final encryptedContents = await file.readAsString(); - final decryptedContents = Crypto.aesDecrypt(encryptedContents, key); + final decryptedContents = _decrypt(encryptedContents, key); final lines = LineSplitter.split(decryptedContents); return ( lines diff --git a/lib/_pkg/crypto.dart b/lib/_pkg/crypto.dart index 9e66e6a79..d2c78fca2 100644 --- a/lib/_pkg/crypto.dart +++ b/lib/_pkg/crypto.dart @@ -21,7 +21,7 @@ class Crypto { final input = Uint8List.fromList(utf8.encode(plainText)); final encrypted = paddedBlockCipher.process(input); - return base64Encode(iv) + ',' + base64Encode(encrypted); + return '${base64Encode(iv)},${base64Encode(encrypted)}'; } static String aesDecrypt(String encryptedBase64Text, String key) { diff --git a/lib/backup/bloc/backup_cubit.dart b/lib/backup/bloc/backup_cubit.dart index 7991cc0c1..d029ca464 100644 --- a/lib/backup/bloc/backup_cubit.dart +++ b/lib/backup/bloc/backup_cubit.dart @@ -106,7 +106,7 @@ class BackupCubit extends Cubit { } final backupDir = - await Directory(appDir! + '/backups/').create(recursive: true); + await Directory('${appDir!}/backups/').create(recursive: true); final file = File(backupDir.path + filename); final content = json.encode({'id': backupId, 'encrypted': ciphertext}); diff --git a/lib/backup/bloc/keychain_cubit.dart b/lib/backup/bloc/keychain_cubit.dart index 144c1f11e..a28c75d2d 100644 --- a/lib/backup/bloc/keychain_cubit.dart +++ b/lib/backup/bloc/keychain_cubit.dart @@ -3,9 +3,9 @@ import 'dart:convert'; import 'package:bb_mobile/_pkg/consts/configs.dart'; import 'package:bb_mobile/_pkg/crypto.dart'; import 'package:bb_mobile/backup/bloc/keychain_state.dart'; +import 'package:dio/dio.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:hex/hex.dart'; -import 'package:http/http.dart' as http; class KeychainCubit extends Cubit { KeychainCubit() : super(const KeychainState()); @@ -36,14 +36,14 @@ class KeychainCubit extends Cubit { final secretHashHex = HEX.encode(secretHashBytes); try { - final response = await http.post( - Uri.parse('$keychainapi/store_key'), - headers: {'Content-Type': 'application/json'}, - body: json.encode({ + final response = await Dio().post( + '$keychainapi/store_key', + options: Options(headers: {'Content-Type': 'application/json'}), + data: { 'backup_id': backupId, 'backup_key': backupKey, 'secret_hash': secretHashHex, - }), + }, ); if (response.statusCode == 201) { From 4791998d488e708017574a9fdaa07f82045027cf Mon Sep 17 00:00:00 2001 From: ethicnology Date: Wed, 18 Dec 2024 11:15:51 -0500 Subject: [PATCH 017/401] chore: make backup visible in the ios UI --- ios/Runner/Info.plist | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 5d4761ede..e7c149e34 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -45,5 +45,9 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + LSSupportsOpeningDocumentsInPlace + + UIFileSharingEnabled + From 941915fcd015fe0658afa842d314238b7edb4b24 Mon Sep 17 00:00:00 2001 From: ethicnology Date: Wed, 18 Dec 2024 11:16:05 -0500 Subject: [PATCH 018/401] refactor: change filename format --- lib/backup/bloc/backup_cubit.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/backup/bloc/backup_cubit.dart b/lib/backup/bloc/backup_cubit.dart index d029ca464..0ba5e858c 100644 --- a/lib/backup/bloc/backup_cubit.dart +++ b/lib/backup/bloc/backup_cubit.dart @@ -97,7 +97,7 @@ class BackupCubit extends Cubit { // TODO : extract nonce? final now = DateTime.now(); - final formattedDate = DateFormat('yyyy-MM-dd_HH-mm-ss').format(now); + final formattedDate = DateFormat('yyyyMMdd_HHmm').format(now); final filename = '$formattedDate.json'; final (appDir, errDir) = await fileStorage.getAppDirectory(); From f4dd271eebff8107d5c430e05266adeb3b8b4eb4 Mon Sep 17 00:00:00 2001 From: ethicnology Date: Wed, 18 Dec 2024 11:28:42 -0500 Subject: [PATCH 019/401] refactor: catch wallet create error --- lib/recover/bloc/manual_cubit.dart | 68 ++++++++++++++++-------------- 1 file changed, 37 insertions(+), 31 deletions(-) diff --git a/lib/recover/bloc/manual_cubit.dart b/lib/recover/bloc/manual_cubit.dart index 2570fe56e..fc033536d 100644 --- a/lib/recover/bloc/manual_cubit.dart +++ b/lib/recover/bloc/manual_cubit.dart @@ -131,6 +131,7 @@ class ManualCubit extends Cubit { return true; } catch (e) { + print(e); emit(state.copyWith(error: 'Invalid backup key or file')); return false; } @@ -144,37 +145,42 @@ class ManualCubit extends Cubit { ScriptType script, BBWalletType type, ) async { - final (seed, error) = - await walletSensitiveCreate.mnemonicSeed(mnemonic, network); - - await walletSensitiveStorage.newSeed(seed: seed!); - - Wallet? wallet; - switch (layer) { - case BaseWalletType.Bitcoin: - final (btcWallet, btcError) = await bdkSensitiveCreate.oneFromBIP39( - seed: seed, - passphrase: passphrase, - scriptType: script, - network: network, - walletType: type, - walletCreate: walletCreate, - ); - wallet = btcWallet; - - case BaseWalletType.Liquid: - final (liqWallet, liqError) = - await lwkSensitiveCreate.oneLiquidFromBIP39( - seed: seed, - passphrase: passphrase, - scriptType: script, - network: network, - walletType: type, - walletCreate: walletCreate, - ); - wallet = liqWallet; - } + try { + final (seed, error) = + await walletSensitiveCreate.mnemonicSeed(mnemonic, network); + if (seed == null) return; + + await walletSensitiveStorage.newSeed(seed: seed); + + Wallet? wallet; + switch (layer) { + case BaseWalletType.Bitcoin: + final (btcWallet, btcError) = await bdkSensitiveCreate.oneFromBIP39( + seed: seed, + passphrase: passphrase, + scriptType: script, + network: network, + walletType: type, + walletCreate: walletCreate, + ); + wallet = btcWallet; + + case BaseWalletType.Liquid: + final (liqWallet, liqError) = + await lwkSensitiveCreate.oneLiquidFromBIP39( + seed: seed, + passphrase: passphrase, + scriptType: script, + network: network, + walletType: type, + walletCreate: walletCreate, + ); + wallet = liqWallet; + } - await walletsStorageRepository.newWallet(wallet!); + await walletsStorageRepository.newWallet(wallet!); + } catch (_) { + rethrow; + } } } From d75f6fbe46180ac62fdacb8d95d71192cc1f227d Mon Sep 17 00:00:00 2001 From: ethicnology Date: Wed, 18 Dec 2024 11:41:44 -0500 Subject: [PATCH 020/401] refactor: deserialize field only in the matching context --- lib/backup/bloc/social_cubit.dart | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/backup/bloc/social_cubit.dart b/lib/backup/bloc/social_cubit.dart index 3f1d47bb4..5ac3a28b8 100644 --- a/lib/backup/bloc/social_cubit.dart +++ b/lib/backup/bloc/social_cubit.dart @@ -65,12 +65,13 @@ class SocialCubit extends Cubit { final socialPayload = json.decode(event.content) as Map; final type = socialPayload['type'] as String; - final friendBackupKey = - socialPayload['backup_key'] as String; - final friendBackupKeySignature = - socialPayload['backup_key_sig'] as String; + switch (type) { case 'backup_request': + final friendBackupKey = + socialPayload['backup_key'] as String; + final friendBackupKeySignature = + socialPayload['backup_key_sig'] as String; emit( state.copyWith( friendBackupKey: friendBackupKey, From f05456b6e4cc8c2f736800c40b5b25cb61e0bcb7 Mon Sep 17 00:00:00 2001 From: ethicnology Date: Wed, 18 Dec 2024 14:41:30 -0500 Subject: [PATCH 021/401] feat: social recover backup key --- lib/backup/social_page.dart | 4 +- lib/backup/social_settings.dart | 26 +++-- lib/recover/bloc/social_cubit.dart | 176 +++++++++++++++++++++++++++++ lib/recover/bloc/social_state.dart | 15 +++ lib/recover/social_page.dart | 123 ++++++++++++++++++++ lib/recover/tweet_widget.dart | 94 +++++++++++++++ lib/routes.dart | 25 ++-- lib/settings/settings_page.dart | 2 +- 8 files changed, 444 insertions(+), 21 deletions(-) create mode 100644 lib/recover/bloc/social_cubit.dart create mode 100644 lib/recover/bloc/social_state.dart create mode 100644 lib/recover/social_page.dart create mode 100644 lib/recover/tweet_widget.dart diff --git a/lib/backup/social_page.dart b/lib/backup/social_page.dart index 0b4291728..e125ddd8b 100644 --- a/lib/backup/social_page.dart +++ b/lib/backup/social_page.dart @@ -9,8 +9,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; -class SocialPage extends StatelessWidget { - const SocialPage({super.key, required this.settings}); +class SocialBackupPage extends StatelessWidget { + const SocialBackupPage({super.key, required this.settings}); final SocialSettingState settings; @override diff --git a/lib/backup/social_settings.dart b/lib/backup/social_settings.dart index dfad95ecb..fe29d4e4c 100644 --- a/lib/backup/social_settings.dart +++ b/lib/backup/social_settings.dart @@ -84,16 +84,22 @@ class _SocialSettingsPageState extends State { maxLength: 64, ), const SizedBox(height: 16), - if (state.secretKey.length == 64 && - state.publicKey.length == 64) - BBButton.textWithStatusAndRightArrow( - label: 'Social', - onPressed: () { - if (cubit.form.currentState!.validate()) { - context.push('/social', extra: state); - } - }, - ), + BBButton.textWithStatusAndRightArrow( + label: 'Backup', + onPressed: () { + if (cubit.form.currentState!.validate()) { + context.push('/social-backup', extra: state); + } + }, + ), + BBButton.textWithStatusAndRightArrow( + label: 'Recovery', + onPressed: () { + if (cubit.form.currentState!.validate()) { + context.push('/social-recover', extra: state); + } + }, + ), ], ), ), diff --git a/lib/recover/bloc/social_cubit.dart b/lib/recover/bloc/social_cubit.dart new file mode 100644 index 000000000..f587b7d5a --- /dev/null +++ b/lib/recover/bloc/social_cubit.dart @@ -0,0 +1,176 @@ +import 'dart:async'; +import 'dart:collection'; +import 'dart:convert'; + +import 'package:bb_mobile/_pkg/crypto.dart'; +import 'package:bb_mobile/_pkg/file_picker.dart'; +import 'package:bb_mobile/recover/bloc/social_state.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:hex/hex.dart'; +import 'package:nostr/nostr.dart'; +import 'package:nostr_sdk/nostr_sdk.dart'; +import 'package:web_socket_channel/web_socket_channel.dart'; + +class SocialCubit extends Cubit { + SocialCubit({ + required this.filePick, + required this.relay, + required this.senderPublic, + required this.senderSecret, + required this.friendPublic, + required this.backupKey, + }) : channel = WebSocketChannel.connect(Uri.parse(relay)), + super(const SocialState()) { + _initializeListener(); + _sendInitialRequest(); + } + + final FilePick filePick; + final WebSocketChannel channel; + final String senderPublic; + final String senderSecret; + final String friendPublic; + final String relay; + final String backupKey; + StreamSubscription? _subscription; + + void clearToast() => state.copyWith(toast: ''); + + void _initializeListener() { + _subscription = channel.stream.listen( + (data) async { + try { + var event = Event.deserialize(json.decode(data as String)); + if (event.kind == 1059) { + try { + final unwrapped = await receiveNip17( + receiverSecretKey: senderSecret, + eventJson: json.encode(event.toJson()), + ); + if (unwrapped != null) { + final x = json.decode(unwrapped) as Map; + event = Event.partial( + id: x['id'], + pubkey: x['pubkey'], + createdAt: x['created_at'], + content: x['content'], + ); + + print('is friend: ${event.pubkey == friendPublic}'); + + if (event.pubkey == friendPublic) { + try { + final socialPayload = + json.decode(event.content) as Map; + final type = socialPayload['type'] as String; + + switch (type) { + case 'recover_backup': + final friendBackupKey = + socialPayload['backup_key'] as String; + final friendBackupKeySignature = + socialPayload['backup_key_sig'] as String; + emit( + state.copyWith( + friendBackupKey: friendBackupKey, + friendBackupKeySignature: friendBackupKeySignature, + ), + ); + default: + } + } catch (e) { + print(e); + } + } + } + } catch (e) { + print(e); + } + } + print('deserialized: ${event.id.substring(0, 6)}'); + emit( + state.copyWith(messages: List.from(state.messages)..add(event)), + ); + } catch (e) { + print(e); + } + }, + onError: (toast) => print(toast), + onDone: () => print('closed'), + ); + } + + void _sendInitialRequest() { + final request = Request(generate64RandomHexChars(), [ + Filter(limit: 0), + ]).serialize(); + channel.sink.add(request); + } + + Future sendPM(String message) async { + final id = await sendNip17( + senderSecretKey: senderSecret, + receiverPublicKey: friendPublic, + relay: relay, + message: message, + ); + + // fake event to display unencrypted locally + final fake = Event.partial( + content: message, + pubkey: senderPublic, + createdAt: currentUnixTimestampSeconds(), + ); + + emit( + state.copyWith( + filter: HashMap.from(state.filter)..[id] = fake, + ), + ); + + print('sent: ${id.substring(0, 7)}'); + return id; + } + + Future uploadFriendKey() async { + final (file, error) = await filePick.pickFile(); + + if (error != null) { + emit(state.copyWith(toast: error.toString())); + return; + } + + if (file == null || file.isEmpty) { + emit(state.copyWith(toast: 'Empty file')); + return; + } + + final backupKey = Crypto.aesDecrypt(file, senderSecret); + if (backupKey.isEmpty) { + emit(state.copyWith(toast: 'Invalid backup')); + return; + } + + emit(state.copyWith(friendBackupKey: backupKey)); + + final backupKeySig = sign( + signerSecretKey: HEX.decode(senderSecret), + message: HEX.decode(backupKey), + ); + + final payload = json.encode({ + 'type': 'recover_backup', + 'backup_key': backupKey, + 'backup_key_sig': HEX.encode(backupKeySig), + }); + + await sendPM(payload); + } + + @override + Future close() { + _subscription?.cancel(); + channel.sink.close(); + return super.close(); + } +} diff --git a/lib/recover/bloc/social_state.dart b/lib/recover/bloc/social_state.dart new file mode 100644 index 000000000..668d833ce --- /dev/null +++ b/lib/recover/bloc/social_state.dart @@ -0,0 +1,15 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:nostr/nostr.dart'; + +part 'social_state.freezed.dart'; + +@freezed +class SocialState with _$SocialState { + const factory SocialState({ + @Default('') String toast, + @Default('') String friendBackupKey, + @Default('') String friendBackupKeySignature, + @Default([]) List messages, + @Default({}) Map filter, + }) = _SocialState; +} diff --git a/lib/recover/social_page.dart b/lib/recover/social_page.dart new file mode 100644 index 000000000..c911e3aae --- /dev/null +++ b/lib/recover/social_page.dart @@ -0,0 +1,123 @@ +import 'package:bb_mobile/_pkg/file_picker.dart'; +import 'package:bb_mobile/_ui/app_bar.dart'; +import 'package:bb_mobile/backup/bloc/social_setting_state.dart'; +import 'package:bb_mobile/locator.dart'; +import 'package:bb_mobile/recover/bloc/social_cubit.dart'; +import 'package:bb_mobile/recover/bloc/social_state.dart'; +import 'package:bb_mobile/recover/tweet_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; + +class SocialRecoverPage extends StatelessWidget { + const SocialRecoverPage({super.key, required this.settings}); + final SocialSettingState settings; + + @override + Widget build(BuildContext context) { + final message = TextEditingController(); + + return BlocProvider( + create: (_) => SocialCubit( + filePick: locator(), + relay: settings.relay, + senderSecret: settings.secretKey, + senderPublic: settings.publicKey, + friendPublic: settings.receiverPublicKey, + backupKey: settings.backupKey, + ), + child: Scaffold( + backgroundColor: Colors.amber, + appBar: AppBar( + automaticallyImplyLeading: false, + flexibleSpace: BBAppBar( + text: 'Social', + onBack: () => context.pop(), + ), + ), + body: BlocListener( + listener: (context, state) { + if (state.toast.isNotEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.toast), + backgroundColor: Colors.red, + ), + ); + context.read().clearToast(); + } + }, + child: BlocBuilder( + builder: (context, state) { + final cubit = context.read(); + + return Column( + children: [ + Expanded( + child: ListView.builder( + itemCount: state.messages.length, + itemBuilder: (context, index) { + final pubkey = state.messages[index].pubkey; + final timestamp = state.messages[index].createdAt; + final content = state.messages[index].content; + final id = state.messages[index].id; + + if (state.filter.containsKey(id)) { + final fake = state.filter[id]!; + return TweetWidget( + pubkey: fake.pubkey, + timestamp: fake.createdAt, + text: fake.content, + ); + } else { + return TweetWidget( + pubkey: pubkey, + timestamp: timestamp, + text: content, + ); + } + }, + ), + ), + Row( + children: [ + ElevatedButton.icon( + icon: const Icon(Icons.upload_file_rounded), + label: const Text('Friend Key'), + onPressed: () async => await cubit.uploadFriendKey(), + ), + ], + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + Expanded( + child: TextFormField( + controller: message, + decoration: const InputDecoration( + border: OutlineInputBorder(), + labelText: 'Write Message', + ), + ), + ), + const SizedBox(width: 8), + IconButton( + icon: const Icon(Icons.send), + onPressed: () async { + await cubit.sendPM(message.text); + message.clear(); + }, + ), + ], + ), + ), + ], + ); + }, + ), + ), + ), + ); + } +} diff --git a/lib/recover/tweet_widget.dart b/lib/recover/tweet_widget.dart new file mode 100644 index 000000000..168987af1 --- /dev/null +++ b/lib/recover/tweet_widget.dart @@ -0,0 +1,94 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:oktoast/oktoast.dart'; + +class TweetWidget extends StatelessWidget { + const TweetWidget({ + super.key, + required this.pubkey, + required this.timestamp, + required this.text, + }); + + final String pubkey; + final int timestamp; + final String text; + + String formatDate(int secondsUnixTimestamp) { + final date = DateTime.fromMillisecondsSinceEpoch( + secondsUnixTimestamp * 1000, + isUtc: true, + ).toLocal(); + return "${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')} ${date.hour.toString().padLeft(2, '0')}:${date.minute.toString().padLeft(2, '0')}"; + } + + Color generateColor() { + final hexColor = pubkey.substring(0, 6).padRight(6, '0'); + return Color(int.parse('0xFF$hexColor')); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(5.0), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: Colors.black, + ), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 5), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ClipOval( + child: Container( + width: 46, + height: 46, + color: generateColor(), + child: Image.network( + 'https://robohash.org/$pubkey.png', + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => const SizedBox.shrink(), + ), + ), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + GestureDetector( + onTap: () { + Clipboard.setData(ClipboardData(text: pubkey)); + showToast('Copied to clipboard: $pubkey'); + }, + child: Text( + pubkey.substring(0, 8), + style: const TextStyle(color: Colors.white), + ), + ), + Text( + formatDate(timestamp), + style: const TextStyle(color: Colors.white70), + ), + ], + ), + const SizedBox(height: 10), + SelectableText( + text, + style: const TextStyle(color: Colors.white), + ), + ], + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/routes.dart b/lib/routes.dart index 160139e9e..37db233b0 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -19,6 +19,7 @@ import 'package:bb_mobile/locator.dart'; import 'package:bb_mobile/receive/receive_page.dart'; import 'package:bb_mobile/recover/keychain_page.dart'; import 'package:bb_mobile/recover/manual_page.dart'; +import 'package:bb_mobile/recover/social_page.dart'; import 'package:bb_mobile/send/bloc/send_cubit.dart'; // import 'package:bb_mobile/seeds/seeds_page.dart'; import 'package:bb_mobile/send/send_page.dart'; @@ -208,6 +209,13 @@ GoRouter setupRouter() => GoRouter( return ManualBackupPage(wallets: wallets); }, ), + GoRoute( + path: '/recoverbull', + builder: (context, state) { + final wallets = state.extra! as List; + return ManualRecoverPage(wallets: wallets); + }, + ), GoRoute( path: '/keychain-backup', builder: (context, state) { @@ -216,10 +224,10 @@ GoRouter setupRouter() => GoRouter( }, ), GoRoute( - path: '/recoverbull', + path: '/keychain-recover', builder: (context, state) { - final wallets = state.extra! as List; - return ManualRecoverPage(wallets: wallets); + final backupId = state.extra! as String; + return KeychainRecoverPage(backupId: backupId); }, ), GoRoute( @@ -229,19 +237,20 @@ GoRouter setupRouter() => GoRouter( }, ), GoRoute( - path: '/social', + path: '/social-backup', builder: (context, state) { final settings = state.extra! as SocialSettingState; - return SocialPage(settings: settings); + return SocialBackupPage(settings: settings); }, ), GoRoute( - path: '/keychain-recover', + path: '/social-recover', builder: (context, state) { - final backupId = state.extra! as String; - return KeychainRecoverPage(backupId: backupId); + final settings = state.extra! as SocialSettingState; + return SocialRecoverPage(settings: settings); }, ), + // GoRoute( // path: '/wallet-settings/open-test-backup', // builder: (context, state) { diff --git a/lib/settings/settings_page.dart b/lib/settings/settings_page.dart index 4b35741bd..f1ceb2376 100644 --- a/lib/settings/settings_page.dart +++ b/lib/settings/settings_page.dart @@ -205,7 +205,7 @@ class SocialButton extends StatelessWidget { @override Widget build(BuildContext context) { return BBButton.textWithStatusAndRightArrow( - label: 'Social Backup', + label: 'Social', onPressed: () { context.push('/social-settings'); }, From dfc469a2adfc727e24efdd31174edad146324c56 Mon Sep 17 00:00:00 2001 From: ethicnology Date: Wed, 18 Dec 2024 15:38:53 -0500 Subject: [PATCH 022/401] refactor: merge the two UI in one --- lib/backup/bloc/social_cubit.dart | 53 ++++++++- lib/backup/social_page.dart | 86 ++++++++------ lib/backup/social_settings.dart | 12 +- lib/recover/bloc/social_cubit.dart | 176 ----------------------------- lib/recover/bloc/social_state.dart | 15 --- lib/recover/social_page.dart | 123 -------------------- lib/recover/tweet_widget.dart | 94 --------------- lib/routes.dart | 12 +- pubspec.lock | 8 ++ pubspec.yaml | 1 + 10 files changed, 115 insertions(+), 465 deletions(-) delete mode 100644 lib/recover/bloc/social_cubit.dart delete mode 100644 lib/recover/bloc/social_state.dart delete mode 100644 lib/recover/social_page.dart delete mode 100644 lib/recover/tweet_widget.dart diff --git a/lib/backup/bloc/social_cubit.dart b/lib/backup/bloc/social_cubit.dart index 5ac3a28b8..f1696305a 100644 --- a/lib/backup/bloc/social_cubit.dart +++ b/lib/backup/bloc/social_cubit.dart @@ -4,6 +4,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:bb_mobile/_pkg/crypto.dart'; +import 'package:bb_mobile/_pkg/file_picker.dart'; import 'package:bb_mobile/_pkg/file_storage.dart'; import 'package:bb_mobile/backup/bloc/social_state.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -15,6 +16,7 @@ import 'package:web_socket_channel/web_socket_channel.dart'; class SocialCubit extends Cubit { SocialCubit({ + required this.filePick, required this.fileStorage, required this.relay, required this.senderPublic, @@ -27,6 +29,7 @@ class SocialCubit extends Cubit { _sendInitialRequest(); } + final FilePick filePick; final FileStorage fileStorage; final WebSocketChannel channel; final String senderPublic; @@ -78,6 +81,17 @@ class SocialCubit extends Cubit { friendBackupKeySignature: friendBackupKeySignature, ), ); + case 'recover_backup': + final friendBackupKey = + socialPayload['backup_key'] as String; + final friendBackupKeySignature = + socialPayload['backup_key_sig'] as String; + emit( + state.copyWith( + friendBackupKey: friendBackupKey, + friendBackupKeySignature: friendBackupKeySignature, + ), + ); default: } } catch (e) { @@ -182,7 +196,7 @@ class SocialCubit extends Cubit { ); final encrypted = Crypto.aesEncrypt(friendBackupKey, senderSecret); - fileSave(name: friendPublic.substring(0, 6), content: encrypted); + _fileSave(name: friendPublic.substring(0, 6), content: encrypted); // TODO: encrypt the key –> derivate a new BIP85 key? use nostr keys? final payload = json.encode({ @@ -193,7 +207,7 @@ class SocialCubit extends Cubit { sendPM(payload); } - void fileSave({ + Future _fileSave({ required String name, required String content, String ext = 'txt', @@ -218,4 +232,39 @@ class SocialCubit extends Cubit { print(file.path); } + + Future uploadFriendKey() async { + final (file, error) = await filePick.pickFile(); + + if (error != null) { + emit(state.copyWith(toast: error.toString())); + return; + } + + if (file == null || file.isEmpty) { + emit(state.copyWith(toast: 'Empty file')); + return; + } + + final backupKey = Crypto.aesDecrypt(file, senderSecret); + if (backupKey.isEmpty) { + emit(state.copyWith(toast: 'Invalid backup')); + return; + } + + emit(state.copyWith(friendBackupKey: backupKey)); + + final backupKeySig = sign( + signerSecretKey: HEX.decode(senderSecret), + message: HEX.decode(backupKey), + ); + + final payload = json.encode({ + 'type': 'recover_backup', + 'backup_key': backupKey, + 'backup_key_sig': HEX.encode(backupKeySig), + }); + + await sendPM(payload); + } } diff --git a/lib/backup/social_page.dart b/lib/backup/social_page.dart index e125ddd8b..a93613a95 100644 --- a/lib/backup/social_page.dart +++ b/lib/backup/social_page.dart @@ -1,3 +1,4 @@ +import 'package:bb_mobile/_pkg/file_picker.dart'; import 'package:bb_mobile/_pkg/file_storage.dart'; import 'package:bb_mobile/_ui/app_bar.dart'; import 'package:bb_mobile/backup/bloc/social_cubit.dart'; @@ -7,10 +8,11 @@ import 'package:bb_mobile/backup/tweet_widget.dart'; import 'package:bb_mobile/locator.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_speed_dial/flutter_speed_dial.dart'; import 'package:go_router/go_router.dart'; -class SocialBackupPage extends StatelessWidget { - const SocialBackupPage({super.key, required this.settings}); +class SocialPage extends StatelessWidget { + const SocialPage({super.key, required this.settings}); final SocialSettingState settings; @override @@ -19,6 +21,7 @@ class SocialBackupPage extends StatelessWidget { return BlocProvider( create: (_) => SocialCubit( + filePick: locator(), fileStorage: locator(), relay: settings.relay, senderSecret: settings.secretKey, @@ -26,32 +29,32 @@ class SocialBackupPage extends StatelessWidget { friendPublic: settings.receiverPublicKey, backupKey: settings.backupKey, ), - child: Scaffold( - backgroundColor: Colors.amber, - appBar: AppBar( - automaticallyImplyLeading: false, - flexibleSpace: BBAppBar( - text: 'Social', - onBack: () => context.pop(), - ), - ), - body: BlocListener( - listener: (context, state) { - if (state.toast.isNotEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(state.toast), - backgroundColor: Colors.red, - ), - ); - context.read().clearToast(); - } - }, - child: BlocBuilder( - builder: (context, state) { - final cubit = context.read(); + child: BlocListener( + listener: (context, state) { + if (state.toast.isNotEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.toast), + backgroundColor: Colors.red, + ), + ); + context.read().clearToast(); + } + }, + child: BlocBuilder( + builder: (context, state) { + final cubit = context.read(); - return Column( + return Scaffold( + backgroundColor: Colors.amber, + appBar: AppBar( + automaticallyImplyLeading: false, + flexibleSpace: BBAppBar( + text: 'Social', + onBack: () => context.pop(), + ), + ), + body: Column( children: [ Expanded( child: ListView.builder( @@ -81,11 +84,6 @@ class SocialBackupPage extends StatelessWidget { ), Row( children: [ - ElevatedButton.icon( - icon: const Icon(Icons.backup), - label: const Text('Request Backup'), - onPressed: () async => await cubit.backupRequest(), - ), if (state.friendBackupKey.isNotEmpty) ElevatedButton.icon( icon: const Icon(Icons.cloud_download), @@ -119,9 +117,27 @@ class SocialBackupPage extends StatelessWidget { ), ), ], - ); - }, - ), + ), + floatingActionButton: SpeedDial( + animatedIcon: AnimatedIcons.menu_close, + backgroundColor: Colors.blue, + overlayColor: Colors.black, + overlayOpacity: 0.5, + children: [ + SpeedDialChild( + child: const Icon(Icons.file_open_rounded), + label: 'Backup', + onTap: () async => await cubit.backupRequest(), + ), + SpeedDialChild( + child: const Icon(Icons.upload_file_rounded), + label: 'Recover', + onTap: () async => await cubit.uploadFriendKey(), + ), + ], + ), + ); + }, ), ), ); diff --git a/lib/backup/social_settings.dart b/lib/backup/social_settings.dart index fe29d4e4c..9a55a6a3b 100644 --- a/lib/backup/social_settings.dart +++ b/lib/backup/social_settings.dart @@ -85,18 +85,10 @@ class _SocialSettingsPageState extends State { ), const SizedBox(height: 16), BBButton.textWithStatusAndRightArrow( - label: 'Backup', + label: 'Chat with friend', onPressed: () { if (cubit.form.currentState!.validate()) { - context.push('/social-backup', extra: state); - } - }, - ), - BBButton.textWithStatusAndRightArrow( - label: 'Recovery', - onPressed: () { - if (cubit.form.currentState!.validate()) { - context.push('/social-recover', extra: state); + context.push('/social', extra: state); } }, ), diff --git a/lib/recover/bloc/social_cubit.dart b/lib/recover/bloc/social_cubit.dart deleted file mode 100644 index f587b7d5a..000000000 --- a/lib/recover/bloc/social_cubit.dart +++ /dev/null @@ -1,176 +0,0 @@ -import 'dart:async'; -import 'dart:collection'; -import 'dart:convert'; - -import 'package:bb_mobile/_pkg/crypto.dart'; -import 'package:bb_mobile/_pkg/file_picker.dart'; -import 'package:bb_mobile/recover/bloc/social_state.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:hex/hex.dart'; -import 'package:nostr/nostr.dart'; -import 'package:nostr_sdk/nostr_sdk.dart'; -import 'package:web_socket_channel/web_socket_channel.dart'; - -class SocialCubit extends Cubit { - SocialCubit({ - required this.filePick, - required this.relay, - required this.senderPublic, - required this.senderSecret, - required this.friendPublic, - required this.backupKey, - }) : channel = WebSocketChannel.connect(Uri.parse(relay)), - super(const SocialState()) { - _initializeListener(); - _sendInitialRequest(); - } - - final FilePick filePick; - final WebSocketChannel channel; - final String senderPublic; - final String senderSecret; - final String friendPublic; - final String relay; - final String backupKey; - StreamSubscription? _subscription; - - void clearToast() => state.copyWith(toast: ''); - - void _initializeListener() { - _subscription = channel.stream.listen( - (data) async { - try { - var event = Event.deserialize(json.decode(data as String)); - if (event.kind == 1059) { - try { - final unwrapped = await receiveNip17( - receiverSecretKey: senderSecret, - eventJson: json.encode(event.toJson()), - ); - if (unwrapped != null) { - final x = json.decode(unwrapped) as Map; - event = Event.partial( - id: x['id'], - pubkey: x['pubkey'], - createdAt: x['created_at'], - content: x['content'], - ); - - print('is friend: ${event.pubkey == friendPublic}'); - - if (event.pubkey == friendPublic) { - try { - final socialPayload = - json.decode(event.content) as Map; - final type = socialPayload['type'] as String; - - switch (type) { - case 'recover_backup': - final friendBackupKey = - socialPayload['backup_key'] as String; - final friendBackupKeySignature = - socialPayload['backup_key_sig'] as String; - emit( - state.copyWith( - friendBackupKey: friendBackupKey, - friendBackupKeySignature: friendBackupKeySignature, - ), - ); - default: - } - } catch (e) { - print(e); - } - } - } - } catch (e) { - print(e); - } - } - print('deserialized: ${event.id.substring(0, 6)}'); - emit( - state.copyWith(messages: List.from(state.messages)..add(event)), - ); - } catch (e) { - print(e); - } - }, - onError: (toast) => print(toast), - onDone: () => print('closed'), - ); - } - - void _sendInitialRequest() { - final request = Request(generate64RandomHexChars(), [ - Filter(limit: 0), - ]).serialize(); - channel.sink.add(request); - } - - Future sendPM(String message) async { - final id = await sendNip17( - senderSecretKey: senderSecret, - receiverPublicKey: friendPublic, - relay: relay, - message: message, - ); - - // fake event to display unencrypted locally - final fake = Event.partial( - content: message, - pubkey: senderPublic, - createdAt: currentUnixTimestampSeconds(), - ); - - emit( - state.copyWith( - filter: HashMap.from(state.filter)..[id] = fake, - ), - ); - - print('sent: ${id.substring(0, 7)}'); - return id; - } - - Future uploadFriendKey() async { - final (file, error) = await filePick.pickFile(); - - if (error != null) { - emit(state.copyWith(toast: error.toString())); - return; - } - - if (file == null || file.isEmpty) { - emit(state.copyWith(toast: 'Empty file')); - return; - } - - final backupKey = Crypto.aesDecrypt(file, senderSecret); - if (backupKey.isEmpty) { - emit(state.copyWith(toast: 'Invalid backup')); - return; - } - - emit(state.copyWith(friendBackupKey: backupKey)); - - final backupKeySig = sign( - signerSecretKey: HEX.decode(senderSecret), - message: HEX.decode(backupKey), - ); - - final payload = json.encode({ - 'type': 'recover_backup', - 'backup_key': backupKey, - 'backup_key_sig': HEX.encode(backupKeySig), - }); - - await sendPM(payload); - } - - @override - Future close() { - _subscription?.cancel(); - channel.sink.close(); - return super.close(); - } -} diff --git a/lib/recover/bloc/social_state.dart b/lib/recover/bloc/social_state.dart deleted file mode 100644 index 668d833ce..000000000 --- a/lib/recover/bloc/social_state.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:nostr/nostr.dart'; - -part 'social_state.freezed.dart'; - -@freezed -class SocialState with _$SocialState { - const factory SocialState({ - @Default('') String toast, - @Default('') String friendBackupKey, - @Default('') String friendBackupKeySignature, - @Default([]) List messages, - @Default({}) Map filter, - }) = _SocialState; -} diff --git a/lib/recover/social_page.dart b/lib/recover/social_page.dart deleted file mode 100644 index c911e3aae..000000000 --- a/lib/recover/social_page.dart +++ /dev/null @@ -1,123 +0,0 @@ -import 'package:bb_mobile/_pkg/file_picker.dart'; -import 'package:bb_mobile/_ui/app_bar.dart'; -import 'package:bb_mobile/backup/bloc/social_setting_state.dart'; -import 'package:bb_mobile/locator.dart'; -import 'package:bb_mobile/recover/bloc/social_cubit.dart'; -import 'package:bb_mobile/recover/bloc/social_state.dart'; -import 'package:bb_mobile/recover/tweet_widget.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:go_router/go_router.dart'; - -class SocialRecoverPage extends StatelessWidget { - const SocialRecoverPage({super.key, required this.settings}); - final SocialSettingState settings; - - @override - Widget build(BuildContext context) { - final message = TextEditingController(); - - return BlocProvider( - create: (_) => SocialCubit( - filePick: locator(), - relay: settings.relay, - senderSecret: settings.secretKey, - senderPublic: settings.publicKey, - friendPublic: settings.receiverPublicKey, - backupKey: settings.backupKey, - ), - child: Scaffold( - backgroundColor: Colors.amber, - appBar: AppBar( - automaticallyImplyLeading: false, - flexibleSpace: BBAppBar( - text: 'Social', - onBack: () => context.pop(), - ), - ), - body: BlocListener( - listener: (context, state) { - if (state.toast.isNotEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(state.toast), - backgroundColor: Colors.red, - ), - ); - context.read().clearToast(); - } - }, - child: BlocBuilder( - builder: (context, state) { - final cubit = context.read(); - - return Column( - children: [ - Expanded( - child: ListView.builder( - itemCount: state.messages.length, - itemBuilder: (context, index) { - final pubkey = state.messages[index].pubkey; - final timestamp = state.messages[index].createdAt; - final content = state.messages[index].content; - final id = state.messages[index].id; - - if (state.filter.containsKey(id)) { - final fake = state.filter[id]!; - return TweetWidget( - pubkey: fake.pubkey, - timestamp: fake.createdAt, - text: fake.content, - ); - } else { - return TweetWidget( - pubkey: pubkey, - timestamp: timestamp, - text: content, - ); - } - }, - ), - ), - Row( - children: [ - ElevatedButton.icon( - icon: const Icon(Icons.upload_file_rounded), - label: const Text('Friend Key'), - onPressed: () async => await cubit.uploadFriendKey(), - ), - ], - ), - Padding( - padding: const EdgeInsets.all(8.0), - child: Row( - children: [ - Expanded( - child: TextFormField( - controller: message, - decoration: const InputDecoration( - border: OutlineInputBorder(), - labelText: 'Write Message', - ), - ), - ), - const SizedBox(width: 8), - IconButton( - icon: const Icon(Icons.send), - onPressed: () async { - await cubit.sendPM(message.text); - message.clear(); - }, - ), - ], - ), - ), - ], - ); - }, - ), - ), - ), - ); - } -} diff --git a/lib/recover/tweet_widget.dart b/lib/recover/tweet_widget.dart deleted file mode 100644 index 168987af1..000000000 --- a/lib/recover/tweet_widget.dart +++ /dev/null @@ -1,94 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:oktoast/oktoast.dart'; - -class TweetWidget extends StatelessWidget { - const TweetWidget({ - super.key, - required this.pubkey, - required this.timestamp, - required this.text, - }); - - final String pubkey; - final int timestamp; - final String text; - - String formatDate(int secondsUnixTimestamp) { - final date = DateTime.fromMillisecondsSinceEpoch( - secondsUnixTimestamp * 1000, - isUtc: true, - ).toLocal(); - return "${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')} ${date.hour.toString().padLeft(2, '0')}:${date.minute.toString().padLeft(2, '0')}"; - } - - Color generateColor() { - final hexColor = pubkey.substring(0, 6).padRight(6, '0'); - return Color(int.parse('0xFF$hexColor')); - } - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(5.0), - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(15), - color: Colors.black, - ), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 5), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ClipOval( - child: Container( - width: 46, - height: 46, - color: generateColor(), - child: Image.network( - 'https://robohash.org/$pubkey.png', - fit: BoxFit.cover, - errorBuilder: (_, __, ___) => const SizedBox.shrink(), - ), - ), - ), - const SizedBox(width: 10), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - GestureDetector( - onTap: () { - Clipboard.setData(ClipboardData(text: pubkey)); - showToast('Copied to clipboard: $pubkey'); - }, - child: Text( - pubkey.substring(0, 8), - style: const TextStyle(color: Colors.white), - ), - ), - Text( - formatDate(timestamp), - style: const TextStyle(color: Colors.white70), - ), - ], - ), - const SizedBox(height: 10), - SelectableText( - text, - style: const TextStyle(color: Colors.white), - ), - ], - ), - ), - ], - ), - ), - ), - ); - } -} diff --git a/lib/routes.dart b/lib/routes.dart index 37db233b0..555efcf8a 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -19,7 +19,6 @@ import 'package:bb_mobile/locator.dart'; import 'package:bb_mobile/receive/receive_page.dart'; import 'package:bb_mobile/recover/keychain_page.dart'; import 'package:bb_mobile/recover/manual_page.dart'; -import 'package:bb_mobile/recover/social_page.dart'; import 'package:bb_mobile/send/bloc/send_cubit.dart'; // import 'package:bb_mobile/seeds/seeds_page.dart'; import 'package:bb_mobile/send/send_page.dart'; @@ -237,17 +236,10 @@ GoRouter setupRouter() => GoRouter( }, ), GoRoute( - path: '/social-backup', + path: '/social', builder: (context, state) { final settings = state.extra! as SocialSettingState; - return SocialBackupPage(settings: settings); - }, - ), - GoRoute( - path: '/social-recover', - builder: (context, state) { - final settings = state.extra! as SocialSettingState; - return SocialRecoverPage(settings: settings); + return SocialPage(settings: settings); }, ), diff --git a/pubspec.lock b/pubspec.lock index c45b81cde..e7e3f464d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -626,6 +626,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.1.3" + flutter_speed_dial: + dependency: "direct main" + description: + name: flutter_speed_dial + sha256: "698a037274a66dbae8697c265440e6acb6ab6cae9ac5f95c749e7944d8f28d41" + url: "https://pub.dev" + source: hosted + version: "7.0.0" flutter_svg: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 37a012f4d..8c631460a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -105,6 +105,7 @@ dependencies: ref: main web_socket_channel: ^2.4.5 nostr: ^1.5.0 + flutter_speed_dial: ^7.0.0 dev_dependencies: From 8d70105e5b5619c4317fea5c3926a2d4da277b33 Mon Sep 17 00:00:00 2001 From: ethicnology Date: Mon, 30 Dec 2024 10:33:41 -0500 Subject: [PATCH 023/401] fix: init backupId in the state --- lib/backup/bloc/keychain_cubit.dart | 1 + lib/recover/bloc/keychain_cubit.dart | 5 +++-- lib/recover/bloc/keychain_state.dart | 2 +- lib/recover/keychain_page.dart | 5 ++++- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/backup/bloc/keychain_cubit.dart b/lib/backup/bloc/keychain_cubit.dart index a28c75d2d..ff6999e13 100644 --- a/lib/backup/bloc/keychain_cubit.dart +++ b/lib/backup/bloc/keychain_cubit.dart @@ -54,6 +54,7 @@ class KeychainCubit extends Cubit { emit(state.copyWith(error: 'Key not secured \n${response.statusCode}')); } } catch (e) { + print(e); emit(state.copyWith(error: 'Server Inaccessible')); } } diff --git a/lib/recover/bloc/keychain_cubit.dart b/lib/recover/bloc/keychain_cubit.dart index a797dab0d..34c2474ec 100644 --- a/lib/recover/bloc/keychain_cubit.dart +++ b/lib/recover/bloc/keychain_cubit.dart @@ -9,7 +9,8 @@ import 'package:hex/hex.dart'; import 'package:http/http.dart' as http; class KeychainCubit extends Cubit { - KeychainCubit({required this.filePicker}) : super(const KeychainState()); + KeychainCubit({required String backupId, required this.filePicker}) + : super(KeychainState(backupId: backupId)); final FilePick filePicker; @@ -39,7 +40,7 @@ class KeychainCubit extends Cubit { } final response = await http.post( - Uri.parse(keychainapi + '/recover_key'), + Uri.parse('$keychainapi/recover_key'), headers: {'Content-Type': 'application/json'}, body: jsonEncode({ 'backup_id': state.backupId, diff --git a/lib/recover/bloc/keychain_state.dart b/lib/recover/bloc/keychain_state.dart index 81fd6e7b0..d4468096f 100644 --- a/lib/recover/bloc/keychain_state.dart +++ b/lib/recover/bloc/keychain_state.dart @@ -7,7 +7,7 @@ class KeychainState with _$KeychainState { const factory KeychainState({ @Default('') String error, @Default('') String backupKey, - @Default('') String backupId, + required String backupId, @Default('') String secret, }) = _KeychainState; } diff --git a/lib/recover/keychain_page.dart b/lib/recover/keychain_page.dart index e25a6c5d3..a8ba4bfbe 100644 --- a/lib/recover/keychain_page.dart +++ b/lib/recover/keychain_page.dart @@ -16,7 +16,10 @@ class KeychainRecoverPage extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider( - create: (_) => KeychainCubit(filePicker: locator()), + create: (_) => KeychainCubit( + filePicker: locator(), + backupId: backupId, + ), child: Scaffold( backgroundColor: Colors.amber, appBar: AppBar( From be46fd21204d8c8e9bf3286fd50e0639ef4c74ff Mon Sep 17 00:00:00 2001 From: ethicnology Date: Thu, 2 Jan 2025 09:37:13 -0500 Subject: [PATCH 024/401] chore: update lock files --- pubspec.lock | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index e7e3f464d..dd07e6776 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -91,7 +91,7 @@ packages: description: path: "bindings/dart-bip85" ref: master - resolved-ref: beee977ad2608987ceb350ef3bc3cc05c9e36455 + resolved-ref: "2321b17f3e1c74f15bdb95638bb8c65bbbe2a2af" url: "https://github.com/ethicnology/rust-bip85.git" source: git version: "1.0.2" @@ -1678,10 +1678,5 @@ packages: source: hosted version: "3.1.3" sdks: -<<<<<<< HEAD dart: ">=3.6.0 <4.0.0" flutter: ">=3.27.0" -======= - dart: ">=3.5.4 <4.0.0" - flutter: ">=3.24.0" ->>>>>>> 254ad28a (chore: update lock files) From 20cb1366332f368b136f6fcb8178aabb0e732b41 Mon Sep 17 00:00:00 2001 From: ethicnology Date: Mon, 6 Jan 2025 16:27:34 -0500 Subject: [PATCH 025/401] chore: dependencies --- macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 80 +++++++++++++++++++ pubspec.yaml | 3 + 3 files changed, 85 insertions(+) diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 061bdc9f7..6dac8526b 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,12 +6,14 @@ import FlutterMacOS import Foundation import flutter_secure_storage_macos +import google_sign_in_ios import path_provider_foundation import share_plus import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) + FLTGoogleSignInPlugin.register(with: registry.registrar(forPlugin: "FLTGoogleSignInPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) diff --git a/pubspec.lock b/pubspec.lock index dd07e6776..93307d493 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,14 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + _discoveryapis_commons: + dependency: transitive + description: + name: _discoveryapis_commons + sha256: "113c4100b90a5b70a983541782431b82168b3cae166ab130649c36eb3559d498" + url: "https://pub.dev" + source: hosted + version: "1.0.7" _fe_analyzer_shared: dependency: transitive description: @@ -376,6 +384,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + extension_google_sign_in_as_googleapis_auth: + dependency: "direct main" + description: + name: extension_google_sign_in_as_googleapis_auth + sha256: bcf4f8dedcc1e66ce5fe98fbd98cc86ed25ad7fce0511e8d6cdc46ccbf421e8e + url: "https://pub.dev" + source: hosted + version: "2.0.12" extra_alignments: dependency: "direct main" description: @@ -737,6 +753,70 @@ packages: url: "https://pub.dev" source: hosted version: "6.2.1" + google_identity_services_web: + dependency: transitive + description: + name: google_identity_services_web + sha256: "55580f436822d64c8ff9a77e37d61f5fb1e6c7ec9d632a43ee324e2a05c3c6c9" + url: "https://pub.dev" + source: hosted + version: "0.3.3" + google_sign_in: + dependency: "direct main" + description: + name: google_sign_in + sha256: fad6ddc80c427b0bba705f2116204ce1173e09cf299f85e053d57a55e5b2dd56 + url: "https://pub.dev" + source: hosted + version: "6.2.2" + google_sign_in_android: + dependency: transitive + description: + name: google_sign_in_android + sha256: "3b96f9b6cf61915f73cbe1218a192623e296a9b8b31965702503649477761e36" + url: "https://pub.dev" + source: hosted + version: "6.1.34" + google_sign_in_ios: + dependency: transitive + description: + name: google_sign_in_ios + sha256: "83f015169102df1ab2905cf8abd8934e28f87db9ace7a5fa676998842fed228a" + url: "https://pub.dev" + source: hosted + version: "5.7.8" + google_sign_in_platform_interface: + dependency: transitive + description: + name: google_sign_in_platform_interface + sha256: "1f6e5787d7a120cc0359ddf315c92309069171306242e181c09472d1b00a2971" + url: "https://pub.dev" + source: hosted + version: "2.4.5" + google_sign_in_web: + dependency: transitive + description: + name: google_sign_in_web + sha256: ada595df6c30cead48e66b1f3a050edf0c5cf2ba60c185d69690e08adcc6281b + url: "https://pub.dev" + source: hosted + version: "0.12.4+3" + googleapis: + dependency: "direct main" + description: + name: googleapis + sha256: "864f222aed3f2ff00b816c675edf00a39e2aaf373d728d8abec30b37bee1a81c" + url: "https://pub.dev" + source: hosted + version: "13.2.0" + googleapis_auth: + dependency: transitive + description: + name: googleapis_auth + sha256: befd71383a955535060acde8792e7efc11d2fccd03dd1d3ec434e85b68775938 + url: "https://pub.dev" + source: hosted + version: "1.6.0" graphs: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 8c631460a..a254b25a0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -106,6 +106,9 @@ dependencies: web_socket_channel: ^2.4.5 nostr: ^1.5.0 flutter_speed_dial: ^7.0.0 + googleapis: ^13.2.0 + google_sign_in: ^6.2.2 + extension_google_sign_in_as_googleapis_auth: ^2.0.12 dev_dependencies: From 0d59d2f750b95ed83acce9b25877499d5d9d7d4e Mon Sep 17 00:00:00 2001 From: ethicnology Date: Mon, 6 Jan 2025 16:29:53 -0500 Subject: [PATCH 026/401] feat: backup google drive integration --- lib/_pkg/gdrive.dart | 59 ++++++++++++++++++++++++++++ lib/backup/backup_page.dart | 8 ++++ lib/backup/bloc/backup_cubit.dart | 9 ++++- lib/backup/bloc/backup_state.dart | 2 + lib/backup/bloc/cloud_cubit.dart | 54 +++++++++++++++++++++++++ lib/backup/bloc/cloud_state.dart | 13 +++++++ lib/backup/cloud_page.dart | 65 +++++++++++++++++++++++++++++++ lib/routes.dart | 9 +++++ 8 files changed, 218 insertions(+), 1 deletion(-) create mode 100644 lib/_pkg/gdrive.dart create mode 100644 lib/backup/bloc/cloud_cubit.dart create mode 100644 lib/backup/bloc/cloud_state.dart create mode 100644 lib/backup/cloud_page.dart diff --git a/lib/_pkg/gdrive.dart b/lib/_pkg/gdrive.dart new file mode 100644 index 000000000..e0190621b --- /dev/null +++ b/lib/_pkg/gdrive.dart @@ -0,0 +1,59 @@ +import 'dart:convert'; + +import 'package:extension_google_sign_in_as_googleapis_auth/extension_google_sign_in_as_googleapis_auth.dart'; +import 'package:google_sign_in/google_sign_in.dart'; +import 'package:googleapis/drive/v3.dart'; + +class Gdrive { + static final google = GoogleSignIn(scopes: [DriveApi.driveFileScope]); + + final DriveApi api; + final GoogleSignInAccount account; + + Gdrive._(this.api, this.account); + + static Future connect() async { + final account = await google.signIn(); + if (account == null) { + print("User not signed in"); + return null; + } + + final client = await google.authenticatedClient(); + if (client == null) { + print("Client is null"); + return null; + } + + final api = DriveApi(client); + return Gdrive._(api, account); + } + + static Future disconnect() async => google.disconnect(); + + Future write({required String filename, required Map content}) async { + try { + // Create an empty file in the appDataFolder + final file = File()..name = filename; + final createdFile = await api.files.create(file); + + if (createdFile.id == null) { + print("Failed to create file."); + return false; + } + + // Update the file with content + final media = Media( + Stream.value(utf8.encode(jsonEncode(content))), + utf8.encode(jsonEncode(content)).length, + ); + await api.files.update(file, createdFile.id!, uploadMedia: media); + + print("File created"); + return true; + } catch (e) { + print("Error: $e"); + return false; + } + } +} diff --git a/lib/backup/backup_page.dart b/lib/backup/backup_page.dart index 77c1c961c..2b52f1e00 100644 --- a/lib/backup/backup_page.dart +++ b/lib/backup/backup_page.dart @@ -99,6 +99,14 @@ class _TheBackupPageState extends State { child: const Text('Generate'), ), ), + if (state.backupPath.isNotEmpty) + ElevatedButton( + onPressed: () => context.push( + '/cloud-backup', + extra: (state.backupPath, state.backupName), + ), + child: const Text('Cloud'), + ), ], ), ); diff --git a/lib/backup/bloc/backup_cubit.dart b/lib/backup/bloc/backup_cubit.dart index 0ba5e858c..c58a55ae3 100644 --- a/lib/backup/bloc/backup_cubit.dart +++ b/lib/backup/bloc/backup_cubit.dart @@ -115,7 +115,14 @@ class BackupCubit extends Cubit { emit(state.copyWith(error: 'Fail to save backup')); } - emit(state.copyWith(backupId: backupId, backupKey: backupKey)); + emit( + state.copyWith( + backupId: backupId, + backupKey: backupKey, + backupPath: file.path, + backupName: filename, + ), + ); } void clearError() => emit(state.copyWith(error: '')); diff --git a/lib/backup/bloc/backup_state.dart b/lib/backup/bloc/backup_state.dart index 894bf8181..ea4d31921 100644 --- a/lib/backup/bloc/backup_state.dart +++ b/lib/backup/bloc/backup_state.dart @@ -9,6 +9,8 @@ class BackupState with _$BackupState { @Default(true) bool loading, @Default([]) List backups, @Default('') String backupId, + @Default('') String backupPath, + @Default('') String backupName, @Default('') String backupKey, @Default('') String error, }) = _BackupState; diff --git a/lib/backup/bloc/cloud_cubit.dart b/lib/backup/bloc/cloud_cubit.dart new file mode 100644 index 000000000..2857a3932 --- /dev/null +++ b/lib/backup/bloc/cloud_cubit.dart @@ -0,0 +1,54 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:bb_mobile/_pkg/gdrive.dart'; +import 'package:bb_mobile/backup/bloc/cloud_state.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class CloudCubit extends Cubit { + final String backupPath; + final String backupName; + + CloudCubit({ + required this.backupPath, + required this.backupName, + }) : super(const CloudState()); + + void clearToast() => state.copyWith(toast: ''); + + Future connectAndStoreBackup() async { + try { + final gdrive = await Gdrive.connect(); + + if (gdrive != null) { + emit(state.copyWith(gdrive: gdrive)); + print("User has logged in" + "Sign in"); + } else { + print("User has not logged in" + "Sign in"); + } + } catch (e) { + print(e); + } + + if (state.gdrive == null) { + emit(state.copyWith(toast: 'not connected')); + return; + } + + final backup = File(backupPath); + final content = await backup.readAsString(); + + final bool isCreated = await state.gdrive!.write( + filename: backupName, + content: json.decode(content) as Map, + ); + + if (isCreated == false) { + emit(state.copyWith(toast: "Not created")); + } else { + emit(state.copyWith(toast: "File created successfully.")); + } + } + + void disconnect() => Gdrive.disconnect(); +} diff --git a/lib/backup/bloc/cloud_state.dart b/lib/backup/bloc/cloud_state.dart new file mode 100644 index 000000000..331581cc3 --- /dev/null +++ b/lib/backup/bloc/cloud_state.dart @@ -0,0 +1,13 @@ +import 'package:bb_mobile/_pkg/gdrive.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'cloud_state.freezed.dart'; + +@freezed +class CloudState with _$CloudState { + const factory CloudState({ + @Default(true) bool loading, + Gdrive? gdrive, + @Default('') String toast, + }) = _CloudState; +} diff --git a/lib/backup/cloud_page.dart b/lib/backup/cloud_page.dart new file mode 100644 index 000000000..1fe0d2597 --- /dev/null +++ b/lib/backup/cloud_page.dart @@ -0,0 +1,65 @@ +import 'package:bb_mobile/_ui/app_bar.dart'; +import 'package:bb_mobile/backup/bloc/cloud_cubit.dart'; +import 'package:bb_mobile/backup/bloc/cloud_state.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; + +class CloudPage extends StatelessWidget { + final String backupPath; + final String backupName; + + const CloudPage({ + super.key, + required this.backupPath, + required this.backupName, + }); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => CloudCubit(backupPath: backupPath, backupName: backupName), + child: BlocListener( + listener: (context, state) { + if (state.toast.isNotEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(state.toast)), + ); + context.read().clearToast(); + } + }, + child: BlocBuilder( + builder: (context, state) { + final cubit = context.read(); + + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + flexibleSpace: BBAppBar( + text: 'Cloud Backup', + onBack: () => context.pop(), + ), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: cubit.connectAndStoreBackup, + child: const Text("Google Drive"), + ), + if (state.gdrive != null) + ElevatedButton( + onPressed: cubit.disconnect, + child: const Text("log out"), + ), + ], + ), + ), + ); + }, + ), + ), + ); + } +} diff --git a/lib/routes.dart b/lib/routes.dart index 555efcf8a..eb322be34 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -6,6 +6,7 @@ import 'package:bb_mobile/_ui/logger_page.dart'; import 'package:bb_mobile/auth/page.dart'; import 'package:bb_mobile/backup/backup_page.dart'; import 'package:bb_mobile/backup/bloc/social_setting_state.dart'; +import 'package:bb_mobile/backup/cloud_page.dart'; import 'package:bb_mobile/backup/keychain_page.dart'; import 'package:bb_mobile/backup/social_page.dart'; import 'package:bb_mobile/backup/social_settings.dart'; @@ -222,6 +223,14 @@ GoRouter setupRouter() => GoRouter( return KeychainBackupPage(backupKey: backupKey, backupId: backupId); }, ), + GoRoute( + path: '/cloud-backup', + builder: (context, state) { + final (backupPath, backupName) = state.extra! as (String, String); + return CloudPage(backupPath: backupPath, backupName: backupName); + }, + ), + GoRoute( path: '/keychain-recover', builder: (context, state) { From 0717a98b95127f36dd076a6ab409cd449e60e1e4 Mon Sep 17 00:00:00 2001 From: ethicnology Date: Tue, 7 Jan 2025 10:44:41 -0500 Subject: [PATCH 027/401] refactor: remove social backup --- lib/_pkg/consts/configs.dart | 1 - lib/backup/bloc/social_cubit.dart | 270 --------------------- lib/backup/bloc/social_setting_state.dart | 15 -- lib/backup/bloc/social_settings_cubit.dart | 71 ------ lib/backup/bloc/social_state.dart | 15 -- lib/backup/social_page.dart | 145 ----------- lib/backup/social_settings.dart | 105 -------- lib/backup/tweet_widget.dart | 94 ------- lib/main.dart | 2 - lib/routes.dart | 17 -- lib/settings/settings_page.dart | 16 -- pubspec.lock | 33 --- pubspec.yaml | 5 - 13 files changed, 789 deletions(-) delete mode 100644 lib/backup/bloc/social_cubit.dart delete mode 100644 lib/backup/bloc/social_setting_state.dart delete mode 100644 lib/backup/bloc/social_settings_cubit.dart delete mode 100644 lib/backup/bloc/social_state.dart delete mode 100644 lib/backup/social_page.dart delete mode 100644 lib/backup/social_settings.dart delete mode 100644 lib/backup/tweet_widget.dart diff --git a/lib/_pkg/consts/configs.dart b/lib/_pkg/consts/configs.dart index 5aed4f491..719ceb0b3 100644 --- a/lib/_pkg/consts/configs.dart +++ b/lib/_pkg/consts/configs.dart @@ -3,7 +3,6 @@ import 'package:lwk/lwk.dart' as lwk; void setupConfigs() {} final keychainapi = dotenv.env['KEYCHAIN_API'] ?? 'http://localhost:3000'; -final socialrelay = dotenv.env['SOCIAL_RELAY'] ?? 'ws://localhost:7000'; final bbmempoolapi = dotenv.env['BB_MEMPOOL_API'] ?? 'mempool.bullbitcoin.com'; final openmempoolapi = dotenv.env['MEMPOOL_API'] ?? 'mempool.space'; final bbexchangeapi = dotenv.env['BB_API'] ?? 'api.bullbitcoin.com/price'; diff --git a/lib/backup/bloc/social_cubit.dart b/lib/backup/bloc/social_cubit.dart deleted file mode 100644 index f1696305a..000000000 --- a/lib/backup/bloc/social_cubit.dart +++ /dev/null @@ -1,270 +0,0 @@ -import 'dart:async'; -import 'dart:collection'; -import 'dart:convert'; -import 'dart:io'; - -import 'package:bb_mobile/_pkg/crypto.dart'; -import 'package:bb_mobile/_pkg/file_picker.dart'; -import 'package:bb_mobile/_pkg/file_storage.dart'; -import 'package:bb_mobile/backup/bloc/social_state.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:hex/hex.dart'; -import 'package:intl/intl.dart'; -import 'package:nostr/nostr.dart'; -import 'package:nostr_sdk/nostr_sdk.dart'; -import 'package:web_socket_channel/web_socket_channel.dart'; - -class SocialCubit extends Cubit { - SocialCubit({ - required this.filePick, - required this.fileStorage, - required this.relay, - required this.senderPublic, - required this.senderSecret, - required this.friendPublic, - required this.backupKey, - }) : channel = WebSocketChannel.connect(Uri.parse(relay)), - super(const SocialState()) { - _initializeListener(); - _sendInitialRequest(); - } - - final FilePick filePick; - final FileStorage fileStorage; - final WebSocketChannel channel; - final String senderPublic; - final String senderSecret; - final String friendPublic; - final String relay; - final String backupKey; - StreamSubscription? _subscription; - - void clearToast() => state.copyWith(toast: ''); - - void _initializeListener() { - _subscription = channel.stream.listen( - (data) async { - try { - var event = Event.deserialize(json.decode(data as String)); - if (event.kind == 1059) { - try { - final unwrapped = await receiveNip17( - receiverSecretKey: senderSecret, - eventJson: json.encode(event.toJson()), - ); - if (unwrapped != null) { - final x = json.decode(unwrapped) as Map; - event = Event.partial( - id: x['id'], - pubkey: x['pubkey'], - createdAt: x['created_at'], - content: x['content'], - ); - - print('is friend: ${event.pubkey == friendPublic}'); - - if (event.pubkey == friendPublic) { - try { - final socialPayload = - json.decode(event.content) as Map; - final type = socialPayload['type'] as String; - - switch (type) { - case 'backup_request': - final friendBackupKey = - socialPayload['backup_key'] as String; - final friendBackupKeySignature = - socialPayload['backup_key_sig'] as String; - emit( - state.copyWith( - friendBackupKey: friendBackupKey, - friendBackupKeySignature: friendBackupKeySignature, - ), - ); - case 'recover_backup': - final friendBackupKey = - socialPayload['backup_key'] as String; - final friendBackupKeySignature = - socialPayload['backup_key_sig'] as String; - emit( - state.copyWith( - friendBackupKey: friendBackupKey, - friendBackupKeySignature: friendBackupKeySignature, - ), - ); - default: - } - } catch (e) { - print(e); - } - } - } - } catch (e) { - print(e); - } - } - print('deserialized: ${event.id.substring(0, 6)}'); - emit( - state.copyWith(messages: List.from(state.messages)..add(event)), - ); - } catch (e) { - print(e); - } - }, - onError: (toast) => print(toast), - onDone: () => print('closed'), - ); - } - - void _sendInitialRequest() { - final request = Request(generate64RandomHexChars(), [ - Filter(limit: 0), - ]).serialize(); - channel.sink.add(request); - } - - Future sendPM(String message) async { - final id = await sendNip17( - senderSecretKey: senderSecret, - receiverPublicKey: friendPublic, - relay: relay, - message: message, - ); - - // fake event to display unencrypted locally - final fake = Event.partial( - content: message, - pubkey: senderPublic, - createdAt: currentUnixTimestampSeconds(), - ); - - emit( - state.copyWith( - filter: HashMap.from(state.filter)..[id] = fake, - ), - ); - - print('sent: ${id.substring(0, 7)}'); - return id; - } - - Future backupRequest() async { - final backupKeySig = sign( - signerSecretKey: HEX.decode(senderSecret), - message: HEX.decode(backupKey), - ); - - final payload = json.encode({ - 'type': 'backup_request', - 'backup_key': backupKey, - 'backup_key_sig': HEX.encode(backupKeySig), - }); - - return await sendPM(payload); - } - - @override - Future close() { - _subscription?.cancel(); - channel.sink.close(); - return super.close(); - } - - void backupConfirm() { - final friendBackupKey = state.friendBackupKey; - final friendBackupKeySignature = state.friendBackupKeySignature; - final signerKey = friendPublic; - - final isVerified = verify( - signerPublicKey: HEX.decode(signerKey), - message: HEX.decode(friendBackupKey), - signature: HEX.decode(friendBackupKeySignature), - ); - - if (isVerified == false) { - emit( - state.copyWith( - toast: 'The friendBackupKeySignature does not match the backup key', - ), - ); - return; - } - - final confirmSig = sign( - signerSecretKey: HEX.decode(senderPublic), - message: HEX.decode(friendBackupKey), - ); - - final encrypted = Crypto.aesEncrypt(friendBackupKey, senderSecret); - _fileSave(name: friendPublic.substring(0, 6), content: encrypted); - // TODO: encrypt the key –> derivate a new BIP85 key? use nostr keys? - - final payload = json.encode({ - 'type': 'backup_key_downloaded', - 'backup_key_sig': HEX.encode(confirmSig), - }); - - sendPM(payload); - } - - Future _fileSave({ - required String name, - required String content, - String ext = 'txt', - }) async { - final now = DateTime.now(); - final formattedDate = DateFormat('yyyy-MM-dd_HH-mm-ss').format(now); - final filename = '${name}_$formattedDate.$ext'; - - final (appDir, errDir) = await fileStorage.getAppDirectory(); - if (errDir != null) { - emit(state.copyWith(toast: 'Fail to get Download directory')); - } - - final backupDir = - await Directory(appDir! + '/backups/').create(recursive: true); - final file = File(backupDir.path + filename); - - final (f, errSave) = await fileStorage.saveToFile(file, content); - if (errSave != null) { - emit(state.copyWith(toast: 'Fail to save backup')); - } - - print(file.path); - } - - Future uploadFriendKey() async { - final (file, error) = await filePick.pickFile(); - - if (error != null) { - emit(state.copyWith(toast: error.toString())); - return; - } - - if (file == null || file.isEmpty) { - emit(state.copyWith(toast: 'Empty file')); - return; - } - - final backupKey = Crypto.aesDecrypt(file, senderSecret); - if (backupKey.isEmpty) { - emit(state.copyWith(toast: 'Invalid backup')); - return; - } - - emit(state.copyWith(friendBackupKey: backupKey)); - - final backupKeySig = sign( - signerSecretKey: HEX.decode(senderSecret), - message: HEX.decode(backupKey), - ); - - final payload = json.encode({ - 'type': 'recover_backup', - 'backup_key': backupKey, - 'backup_key_sig': HEX.encode(backupKeySig), - }); - - await sendPM(payload); - } -} diff --git a/lib/backup/bloc/social_setting_state.dart b/lib/backup/bloc/social_setting_state.dart deleted file mode 100644 index 0f69df00d..000000000 --- a/lib/backup/bloc/social_setting_state.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'social_setting_state.freezed.dart'; - -@freezed -class SocialSettingState with _$SocialSettingState { - const factory SocialSettingState({ - @Default('') String secretKey, - @Default('') String publicKey, - @Default('') String receiverPublicKey, - @Default('') String backupKey, - @Default('') String relay, - @Default('') String error, - }) = _SocialSettingState; -} diff --git a/lib/backup/bloc/social_settings_cubit.dart b/lib/backup/bloc/social_settings_cubit.dart deleted file mode 100644 index 1be69c71c..000000000 --- a/lib/backup/bloc/social_settings_cubit.dart +++ /dev/null @@ -1,71 +0,0 @@ -import 'package:bb_mobile/_pkg/consts/configs.dart'; -import 'package:bb_mobile/backup/bloc/social_setting_state.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:nostr/nostr.dart'; -import 'package:nostr_sdk/nostr_sdk.dart'; - -class SocialSettingsCubit extends Cubit { - SocialSettingsCubit() : super(const SocialSettingState()); - - final form = GlobalKey(); - - void init() { - final random = generate64RandomHexChars(); - final pair = keys(hex: random); - final secret = pair.$1; - final public = pair.$2; - - if (socialrelay.isEmpty) { - emit(state.copyWith(error: 'social nostr relay is not set')); - return; - } - - emit( - state.copyWith( - secretKey: secret, - publicKey: public, - relay: socialrelay, - ), - ); - } - - void clearError() => state.copyWith(error: ''); - - void updateRelay(String value) => emit(state.copyWith(relay: value)); - - void updateBackupKey(String v) => emit(state.copyWith(backupKey: v)); - - void updateSecretKey(String value) { - if (value.length == 64) { - final pair = keys(hex: value); - final secret = pair.$1; - final public = pair.$2; - - emit( - state.copyWith( - secretKey: secret, - publicKey: public, - ), - ); - } else { - emit(state.copyWith(secretKey: value, publicKey: 'N/A')); - } - } - - void updateReceiverPublicKey(String value) => - emit(state.copyWith(receiverPublicKey: value)); - - String? hexValidator(String? value) { - if (value == null || value.isEmpty) { - return 'Input cannot be empty'; - } - - final hexPattern = RegExp(r'^[0-9a-fA-F]+$'); - if (!hexPattern.hasMatch(value)) { - return 'Only hexadecimal characters are allowed'; - } - - return null; - } -} diff --git a/lib/backup/bloc/social_state.dart b/lib/backup/bloc/social_state.dart deleted file mode 100644 index 668d833ce..000000000 --- a/lib/backup/bloc/social_state.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:nostr/nostr.dart'; - -part 'social_state.freezed.dart'; - -@freezed -class SocialState with _$SocialState { - const factory SocialState({ - @Default('') String toast, - @Default('') String friendBackupKey, - @Default('') String friendBackupKeySignature, - @Default([]) List messages, - @Default({}) Map filter, - }) = _SocialState; -} diff --git a/lib/backup/social_page.dart b/lib/backup/social_page.dart deleted file mode 100644 index a93613a95..000000000 --- a/lib/backup/social_page.dart +++ /dev/null @@ -1,145 +0,0 @@ -import 'package:bb_mobile/_pkg/file_picker.dart'; -import 'package:bb_mobile/_pkg/file_storage.dart'; -import 'package:bb_mobile/_ui/app_bar.dart'; -import 'package:bb_mobile/backup/bloc/social_cubit.dart'; -import 'package:bb_mobile/backup/bloc/social_setting_state.dart'; -import 'package:bb_mobile/backup/bloc/social_state.dart'; -import 'package:bb_mobile/backup/tweet_widget.dart'; -import 'package:bb_mobile/locator.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_speed_dial/flutter_speed_dial.dart'; -import 'package:go_router/go_router.dart'; - -class SocialPage extends StatelessWidget { - const SocialPage({super.key, required this.settings}); - final SocialSettingState settings; - - @override - Widget build(BuildContext context) { - final message = TextEditingController(); - - return BlocProvider( - create: (_) => SocialCubit( - filePick: locator(), - fileStorage: locator(), - relay: settings.relay, - senderSecret: settings.secretKey, - senderPublic: settings.publicKey, - friendPublic: settings.receiverPublicKey, - backupKey: settings.backupKey, - ), - child: BlocListener( - listener: (context, state) { - if (state.toast.isNotEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(state.toast), - backgroundColor: Colors.red, - ), - ); - context.read().clearToast(); - } - }, - child: BlocBuilder( - builder: (context, state) { - final cubit = context.read(); - - return Scaffold( - backgroundColor: Colors.amber, - appBar: AppBar( - automaticallyImplyLeading: false, - flexibleSpace: BBAppBar( - text: 'Social', - onBack: () => context.pop(), - ), - ), - body: Column( - children: [ - Expanded( - child: ListView.builder( - itemCount: state.messages.length, - itemBuilder: (context, index) { - final pubkey = state.messages[index].pubkey; - final timestamp = state.messages[index].createdAt; - final content = state.messages[index].content; - final id = state.messages[index].id; - - if (state.filter.containsKey(id)) { - final fake = state.filter[id]!; - return TweetWidget( - pubkey: fake.pubkey, - timestamp: fake.createdAt, - text: fake.content, - ); - } else { - return TweetWidget( - pubkey: pubkey, - timestamp: timestamp, - text: content, - ); - } - }, - ), - ), - Row( - children: [ - if (state.friendBackupKey.isNotEmpty) - ElevatedButton.icon( - icon: const Icon(Icons.cloud_download), - label: const Text('Download Backup Key'), - onPressed: () async => cubit.backupConfirm(), - ), - ], - ), - Padding( - padding: const EdgeInsets.all(8.0), - child: Row( - children: [ - Expanded( - child: TextFormField( - controller: message, - decoration: const InputDecoration( - border: OutlineInputBorder(), - labelText: 'Write Message', - ), - ), - ), - const SizedBox(width: 8), - IconButton( - icon: const Icon(Icons.send), - onPressed: () async { - await cubit.sendPM(message.text); - message.clear(); - }, - ), - ], - ), - ), - ], - ), - floatingActionButton: SpeedDial( - animatedIcon: AnimatedIcons.menu_close, - backgroundColor: Colors.blue, - overlayColor: Colors.black, - overlayOpacity: 0.5, - children: [ - SpeedDialChild( - child: const Icon(Icons.file_open_rounded), - label: 'Backup', - onTap: () async => await cubit.backupRequest(), - ), - SpeedDialChild( - child: const Icon(Icons.upload_file_rounded), - label: 'Recover', - onTap: () async => await cubit.uploadFriendKey(), - ), - ], - ), - ); - }, - ), - ), - ); - } -} diff --git a/lib/backup/social_settings.dart b/lib/backup/social_settings.dart deleted file mode 100644 index 9a55a6a3b..000000000 --- a/lib/backup/social_settings.dart +++ /dev/null @@ -1,105 +0,0 @@ -import 'package:bb_mobile/_ui/app_bar.dart'; -import 'package:bb_mobile/_ui/components/button.dart'; -import 'package:bb_mobile/backup/bloc/social_setting_state.dart'; -import 'package:bb_mobile/backup/bloc/social_settings_cubit.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:go_router/go_router.dart'; - -class SocialSettingsPage extends StatefulWidget { - const SocialSettingsPage({super.key}); - - @override - _SocialSettingsPageState createState() => _SocialSettingsPageState(); -} - -class _SocialSettingsPageState extends State { - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (_) => SocialSettingsCubit()..init(), - child: Scaffold( - backgroundColor: Colors.amber, - appBar: AppBar( - automaticallyImplyLeading: false, - flexibleSpace: BBAppBar( - text: 'Social', - onBack: () => context.pop(), - ), - ), - body: BlocListener( - listener: (context, state) { - if (state.error.isNotEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(state.error), - backgroundColor: Colors.red, - ), - ); - context.read().clearError(); - } - }, - child: BlocBuilder( - builder: (context, state) { - final cubit = context.read(); - - return Form( - key: cubit.form, - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - TextFormField( - initialValue: state.relay, - decoration: const InputDecoration(labelText: 'Relay'), - onChanged: (v) => cubit.updateRelay(v), - ), - const SizedBox(height: 16), - TextFormField( - initialValue: state.secretKey, - decoration: - const InputDecoration(labelText: 'Your secret'), - onChanged: (v) => cubit.updateSecretKey(v), - validator: cubit.hexValidator, - maxLength: 64, - ), - SelectableText('Pubkey: ${state.publicKey}'), - const SizedBox(height: 16), - TextFormField( - initialValue: state.receiverPublicKey, - decoration: - const InputDecoration(labelText: 'Friend public'), - onChanged: (v) => cubit.updateReceiverPublicKey(v), - validator: cubit.hexValidator, - maxLength: 64, - ), - const SizedBox(height: 16), - TextFormField( - initialValue: state.backupKey, - decoration: - const InputDecoration(labelText: 'Backup Key'), - onChanged: (v) => cubit.updateBackupKey(v), - validator: cubit.hexValidator, - maxLength: 64, - ), - const SizedBox(height: 16), - BBButton.textWithStatusAndRightArrow( - label: 'Chat with friend', - onPressed: () { - if (cubit.form.currentState!.validate()) { - context.push('/social', extra: state); - } - }, - ), - ], - ), - ), - ); - }, - ), - ), - ), - ); - } -} diff --git a/lib/backup/tweet_widget.dart b/lib/backup/tweet_widget.dart deleted file mode 100644 index 168987af1..000000000 --- a/lib/backup/tweet_widget.dart +++ /dev/null @@ -1,94 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:oktoast/oktoast.dart'; - -class TweetWidget extends StatelessWidget { - const TweetWidget({ - super.key, - required this.pubkey, - required this.timestamp, - required this.text, - }); - - final String pubkey; - final int timestamp; - final String text; - - String formatDate(int secondsUnixTimestamp) { - final date = DateTime.fromMillisecondsSinceEpoch( - secondsUnixTimestamp * 1000, - isUtc: true, - ).toLocal(); - return "${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')} ${date.hour.toString().padLeft(2, '0')}:${date.minute.toString().padLeft(2, '0')}"; - } - - Color generateColor() { - final hexColor = pubkey.substring(0, 6).padRight(6, '0'); - return Color(int.parse('0xFF$hexColor')); - } - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(5.0), - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(15), - color: Colors.black, - ), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 5), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ClipOval( - child: Container( - width: 46, - height: 46, - color: generateColor(), - child: Image.network( - 'https://robohash.org/$pubkey.png', - fit: BoxFit.cover, - errorBuilder: (_, __, ___) => const SizedBox.shrink(), - ), - ), - ), - const SizedBox(width: 10), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - GestureDetector( - onTap: () { - Clipboard.setData(ClipboardData(text: pubkey)); - showToast('Copied to clipboard: $pubkey'); - }, - child: Text( - pubkey.substring(0, 8), - style: const TextStyle(color: Colors.white), - ), - ), - Text( - formatDate(timestamp), - style: const TextStyle(color: Colors.white70), - ), - ], - ), - const SizedBox(height: 10), - SelectableText( - text, - style: const TextStyle(color: Colors.white), - ), - ], - ), - ), - ], - ), - ), - ), - ); - } -} diff --git a/lib/main.dart b/lib/main.dart index 16ee02c9e..1765942e8 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -29,7 +29,6 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:go_router/go_router.dart'; import 'package:lwk/lwk.dart'; -import 'package:nostr_sdk/nostr_sdk.dart'; import 'package:oktoast/oktoast.dart'; import 'package:payjoin_flutter/src/generated/frb_generated.dart'; @@ -43,7 +42,6 @@ Future main({bool fromTest = false}) async { await LibLwk.init(); await LibBoltz.init(); await LibBip85.init(); - await Nip17.init(); await dotenv.load(isOptional: true); Bloc.observer = BBlocObserver(); // await FlutterWindowManager.addFlags(FlutterWindowManager.FLAG_SECURE); diff --git a/lib/routes.dart b/lib/routes.dart index eb322be34..c7ef1c71c 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -5,11 +5,8 @@ import 'package:bb_mobile/_model/transaction.dart'; import 'package:bb_mobile/_ui/logger_page.dart'; import 'package:bb_mobile/auth/page.dart'; import 'package:bb_mobile/backup/backup_page.dart'; -import 'package:bb_mobile/backup/bloc/social_setting_state.dart'; import 'package:bb_mobile/backup/cloud_page.dart'; import 'package:bb_mobile/backup/keychain_page.dart'; -import 'package:bb_mobile/backup/social_page.dart'; -import 'package:bb_mobile/backup/social_settings.dart'; import 'package:bb_mobile/create/page.dart'; import 'package:bb_mobile/home/home_page.dart'; import 'package:bb_mobile/home/market.dart'; @@ -21,7 +18,6 @@ import 'package:bb_mobile/receive/receive_page.dart'; import 'package:bb_mobile/recover/keychain_page.dart'; import 'package:bb_mobile/recover/manual_page.dart'; import 'package:bb_mobile/send/bloc/send_cubit.dart'; -// import 'package:bb_mobile/seeds/seeds_page.dart'; import 'package:bb_mobile/send/send_page.dart'; import 'package:bb_mobile/settings/application_settings_page.dart'; import 'package:bb_mobile/settings/bitcoin_settings_page.dart'; @@ -238,19 +234,6 @@ GoRouter setupRouter() => GoRouter( return KeychainRecoverPage(backupId: backupId); }, ), - GoRoute( - path: '/social-settings', - builder: (context, state) { - return const SocialSettingsPage(); - }, - ), - GoRoute( - path: '/social', - builder: (context, state) { - final settings = state.extra! as SocialSettingState; - return SocialPage(settings: settings); - }, - ), // GoRoute( // path: '/wallet-settings/open-test-backup', diff --git a/lib/settings/settings_page.dart b/lib/settings/settings_page.dart index f1ceb2376..0ea1206e8 100644 --- a/lib/settings/settings_page.dart +++ b/lib/settings/settings_page.dart @@ -54,8 +54,6 @@ class _Screen extends StatelessWidget { const BackupBullButton(), const Gap(8), const RecoverBullButton(), - const Gap(8), - const SocialButton(), const Gap(24), const Center( @@ -199,20 +197,6 @@ class RecoverBullButton extends StatelessWidget { } } -class SocialButton extends StatelessWidget { - const SocialButton({super.key}); - - @override - Widget build(BuildContext context) { - return BBButton.textWithStatusAndRightArrow( - label: 'Social', - onPressed: () { - context.push('/social-settings'); - }, - ); - } -} - class ApplicationSettingsButton extends StatelessWidget { const ApplicationSettingsButton({super.key}); diff --git a/pubspec.lock b/pubspec.lock index 93307d493..3e6dc0478 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -78,22 +78,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.31.2" - bech32: - dependency: transitive - description: - name: bech32 - sha256: "156cbace936f7720c79a79d16a03efad343b1ef17106716e04b8b8e39f99f7f7" - url: "https://pub.dev" - source: hosted - version: "0.2.2" - bip340: - dependency: transitive - description: - name: bip340 - sha256: "2a92f6ed68959f75d67c9a304c17928b9c9449587d4f75ee68f34152f7f69e87" - url: "https://pub.dev" - source: hosted - version: "0.2.0" bip85: dependency: "direct main" description: @@ -1086,23 +1070,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.2" - nostr: - dependency: "direct main" - description: - name: nostr - sha256: a99942e4eedd5823d16f42e6df96240488028666a329ee1047552f79db564123 - url: "https://pub.dev" - source: hosted - version: "1.5.0" - nostr_sdk: - dependency: "direct main" - description: - path: "." - ref: main - resolved-ref: cae2399dcba0a3ed03c0284db26236f9e5e31be6 - url: "https://github.com/ethicnology/dart-nip17" - source: git - version: "0.0.1" oktoast: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index a254b25a0..eaecce610 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -99,12 +99,7 @@ dependencies: url: https://github.com/ethicnology/rust-bip85.git path: bindings/dart-bip85 ref: master - nostr_sdk: - git: - url: https://github.com/ethicnology/dart-nip17 - ref: main web_socket_channel: ^2.4.5 - nostr: ^1.5.0 flutter_speed_dial: ^7.0.0 googleapis: ^13.2.0 google_sign_in: ^6.2.2 From 445bb406dc434a102399355bd040b25e11f419e8 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Thu, 9 Jan 2025 18:28:57 -0500 Subject: [PATCH 028/401] added recoverbull_dart to handle backup encryption --- ios/Podfile.lock | 32 ++++++++++++++++++++++++++++++++ pubspec.lock | 25 +++++++++++++++++++++++++ pubspec.yaml | 3 +++ 3 files changed, 60 insertions(+) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index b80316c56..9862b3654 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,4 +1,10 @@ PODS: + - AppAuth (1.7.6): + - AppAuth/Core (= 1.7.6) + - AppAuth/ExternalUserAgent (= 1.7.6) + - AppAuth/Core (1.7.6) + - AppAuth/ExternalUserAgent (1.7.6): + - AppAuth/Core - bdk_flutter (0.31.2): - Flutter - bip85 (0.0.1): @@ -51,6 +57,24 @@ PODS: - Flutter - flutter_secure_storage (6.0.0): - Flutter + - google_sign_in_ios (0.0.1): + - AppAuth (>= 1.7.4) + - Flutter + - FlutterMacOS + - GoogleSignIn (~> 7.1) + - GTMSessionFetcher (>= 3.4.0) + - GoogleSignIn (7.1.0): + - AppAuth (< 2.0, >= 1.7.3) + - GTMAppAuth (< 5.0, >= 4.1.1) + - GTMSessionFetcher/Core (~> 3.3) + - GTMAppAuth (4.1.1): + - AppAuth/Core (~> 1.7) + - GTMSessionFetcher/Core (< 4.0, >= 3.3) + - GTMSessionFetcher (3.5.0): + - GTMSessionFetcher/Full (= 3.5.0) + - GTMSessionFetcher/Core (3.5.0) + - GTMSessionFetcher/Full (3.5.0): + - GTMSessionFetcher/Core - integration_test (0.0.1): - Flutter - lwk (0.1.5) @@ -87,6 +111,7 @@ DEPENDENCIES: - flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`) - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) + - google_sign_in_ios (from `.symlinks/plugins/google_sign_in_ios/darwin`) - integration_test (from `.symlinks/plugins/integration_test/ios`) - lwk (from `.symlinks/plugins/lwk/ios`) - no_screenshot (from `.symlinks/plugins/no_screenshot/ios`) @@ -99,8 +124,12 @@ DEPENDENCIES: SPEC REPOS: trunk: + - AppAuth - DKImagePickerController - DKPhotoGallery + - GoogleSignIn + - GTMAppAuth + - GTMSessionFetcher - ScreenProtectorKit - SDWebImage - SwiftyGif @@ -128,6 +157,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/flutter_native_splash/ios" flutter_secure_storage: :path: ".symlinks/plugins/flutter_secure_storage/ios" + google_sign_in_ios: + :path: ".symlinks/plugins/google_sign_in_ios/darwin" integration_test: :path: ".symlinks/plugins/integration_test/ios" lwk: @@ -151,6 +182,7 @@ SPEC CHECKSUMS: bdk_flutter: fb57a7400a7f3f181c5977bcdc2a5ef347ae4e7f bip85: f656a7e6b23afda4960efb11c87d51d68e8be3db boltz: 6388ec2412f3753b63a9e65c97f87ea26f43bddc + AppAuth: d4f13a8fe0baf391b2108511793e4b479691fb73 DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 document_file_save_plus: 913d440d8b611ae19add4522ed578e3ed1483a2f diff --git a/pubspec.lock b/pubspec.lock index 3e6dc0478..8a8ed10d6 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -304,6 +304,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.6" + cryptography: + dependency: transitive + description: + name: cryptography + sha256: d146b76d33d94548cf035233fbc2f4338c1242fa119013bead807d033fc4ae05 + url: "https://pub.dev" + source: hosted + version: "2.7.0" csslib: dependency: transitive description: @@ -958,6 +966,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + logger: + dependency: transitive + description: + name: logger + sha256: be4b23575aac7ebf01f225a241eb7f6b5641eeaf43c6a8613510fc2f8cf187d1 + url: "https://pub.dev" + source: hosted + version: "2.5.0" logging: dependency: transitive description: @@ -1319,6 +1335,15 @@ packages: url: "https://pub.dev" source: hosted version: "0.28.0" + recoverbull_dart: + dependency: "direct main" + description: + path: "." + ref: HEAD + resolved-ref: d3b856d0cafea2b96c784eb229466a8b057c07f6 + url: "https://github.com/StaxoLotl/recoverbull-dart.git" + source: git + version: "0.0.1" share_plus: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index eaecce610..87fadca99 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -104,6 +104,9 @@ dependencies: googleapis: ^13.2.0 google_sign_in: ^6.2.2 extension_google_sign_in_as_googleapis_auth: ^2.0.12 + recoverbull_dart: + git: + url: https://github.com/StaxoLotl/recoverbull-dart.git dev_dependencies: From 26211b30baee63e798ced3a4b526ece5a48b20d1 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Thu, 9 Jan 2025 18:31:05 -0500 Subject: [PATCH 029/401] feat(Backup): exposed mnemonicFingerPrint to verify backup-wallet compatibility --- lib/_model/backup.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/_model/backup.dart b/lib/_model/backup.dart index 7e7d4c0f9..f051b0787 100644 --- a/lib/_model/backup.dart +++ b/lib/_model/backup.dart @@ -15,6 +15,7 @@ class Backup with _$Backup { @Default('') String type, @Default([]) List mnemonic, @Default('') String passphrase, + @Default('') String mnemonicFingerPrint, @Default([]) List labels, @Default([]) List descriptors, }) = _Backup; From 562b33a7fb4b30cee54c60b44a43b8be5eac6ea0 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Thu, 9 Jan 2025 18:37:57 -0500 Subject: [PATCH 030/401] feat(BackupCubit): Make mnemonic, passphrase, labels, and descriptors optional This update introduces user-defined selection of data components for inclusion in backups. --- lib/backup/backup_page.dart | 171 ++++++++++++++++--- lib/backup/bloc/backup_cubit.dart | 263 +++++++++++++++++++++--------- lib/backup/bloc/backup_state.dart | 3 +- 3 files changed, 329 insertions(+), 108 deletions(-) diff --git a/lib/backup/backup_page.dart b/lib/backup/backup_page.dart index 2b52f1e00..f5fd32735 100644 --- a/lib/backup/backup_page.dart +++ b/lib/backup/backup_page.dart @@ -1,12 +1,16 @@ import 'package:bb_mobile/_pkg/file_storage.dart'; import 'package:bb_mobile/_pkg/wallet/repository/sensitive_storage.dart'; import 'package:bb_mobile/_ui/app_bar.dart'; +import 'package:bb_mobile/_ui/components/button.dart'; +import 'package:bb_mobile/_ui/components/controls.dart'; +import 'package:bb_mobile/_ui/components/text.dart'; import 'package:bb_mobile/backup/bloc/backup_cubit.dart'; import 'package:bb_mobile/backup/bloc/backup_state.dart'; import 'package:bb_mobile/locator.dart'; import 'package:bb_mobile/wallet/bloc/wallet_bloc.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:gap/gap.dart'; import 'package:go_router/go_router.dart'; class ManualBackupPage extends StatefulWidget { @@ -26,13 +30,14 @@ class _TheBackupPageState extends State { wallets: widget.wallets, walletSensitiveStorage: locator(), fileStorage: locator(), - )..loadBackupData(), + )..loadConfirmedBackups(), child: Scaffold( - backgroundColor: Colors.amber, + backgroundColor: Colors.white, appBar: AppBar( automaticallyImplyLeading: false, + elevation: 0, flexibleSpace: BBAppBar( - text: 'Recover Backup', + text: 'Backup', onBack: () => context.pop(), ), ), @@ -61,52 +66,75 @@ class _TheBackupPageState extends State { return state.loading ? const Center(child: CircularProgressIndicator()) : Padding( - padding: const EdgeInsets.all(16.0), + padding: const EdgeInsets.all(24.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + BackupToggleItem( + title: 'Mnemonics & Passwords', + value: state.confirmedBackups['mnemonic'] ?? false, + onChanged: () { + context + .read() + .toggleAllMnemonicAndPassphrase(); + }, + ), + Gap(8), + BackupToggleItem( + title: 'Descriptors', + value: + state.confirmedBackups['descriptors'] ?? false, + onChanged: () { + context.read().toggleDescriptors(); + }, + ), + Gap(8), + BackupToggleItem( + title: 'Labels', + value: state.confirmedBackups['labels'] ?? false, + onChanged: () { + context.read().toggleLabels(); + }, + ), + Gap(8), + if (state.backupKey.isEmpty) + Center(child: _GenerateBackupButton()), + Gap(20), if (state.backupKey.isNotEmpty) Column( children: [ - const Text( - 'Backup Key:', - style: TextStyle(fontWeight: FontWeight.bold), - ), - const SizedBox(height: 10), + BBText.bodyBold("Generated Backup Key"), + Gap(10), SelectableText( state.backupKey, + textAlign: TextAlign.center, style: const TextStyle( - fontWeight: FontWeight.bold, + fontWeight: FontWeight.normal, ), ), + Gap(20), if (state.backupId.isNotEmpty) - ElevatedButton( + BBButton.big( onPressed: () => context.push( '/keychain-backup', extra: (state.backupKey, state.backupId), ), - child: const Text('Keychain'), + label: 'SAVE TO KEYCHAIN', ), ], ), - const SizedBox(height: 20), - if (state.backupKey.isEmpty) - Center( - child: ElevatedButton( - onPressed: context - .read() - .writeEncryptedBackup, - child: const Text('Generate'), - ), - ), + Gap(50), if (state.backupPath.isNotEmpty) - ElevatedButton( - onPressed: () => context.push( - '/cloud-backup', - extra: (state.backupPath, state.backupName), + Center( + child: BBButton.big( + onPressed: () => context.push( + '/cloud-backup', + extra: (state.backupPath, state.backupName), + ), + label: "SAVE TO CLOUD", ), - child: const Text('Cloud'), ), + Gap(10), ], ), ); @@ -117,3 +145,92 @@ class _TheBackupPageState extends State { ); } } + +class BackupToggleItem extends StatelessWidget { + final String title; + final bool value; + final VoidCallback onChanged; + const BackupToggleItem({ + super.key, + required this.title, + required this.value, + required this.onChanged, + }); + + @override + Widget build(BuildContext context) { + return BlocListener( + listenWhen: (previous, current) => + previous.confirmedBackups != current.confirmedBackups, + listener: (context, state) {}, + child: Row( + children: [ + BBText.body( + title, + ), + const Spacer(), + BBSwitch( + // key: UIKeys.settingsBackupToggleSwitch,//TODO; Add switch key + value: value, + onChanged: (e) { + onChanged(); + }, + ), + ], + ), + ); + } +} + +class _GenerateBackupButton extends StatelessWidget { + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final mnemonicConfirmed = state.confirmedBackups['mnemonic'] ?? false; + + return Center( + child: BBButton.big( + onPressed: () { + if (!mnemonicConfirmed) { + _showConfirmDialog(context); + } else { + context.read().writeEncryptedBackup(); + } + }, + label: "GENERATE BACKUP", + ), + ); + }, + ); + } +} + +void _showConfirmDialog(BuildContext context) { + showDialog( + context: context, + builder: (BuildContext dialogContext) { + return AlertDialog( + title: const BBText.body('Confirm New Mnemonic'), + content: const BBText.bodySmall( + 'You have not confirmed your mnemonic. Generating a backup now will create a new mnemonic for the backup key. Are you sure you want to proceed?', + ), + actions: [ + TextButton( + child: const Text('Cancel'), + onPressed: () { + Navigator.of(dialogContext).pop(); + }, + ), + TextButton( + child: const Text('Confirm'), + onPressed: () { + Navigator.of(dialogContext).pop(); + context.read().writeEncryptedBackup(); + }, + ), + ], + ); + }, + ); +} diff --git a/lib/backup/bloc/backup_cubit.dart b/lib/backup/bloc/backup_cubit.dart index c58a55ae3..e4dcc587b 100644 --- a/lib/backup/bloc/backup_cubit.dart +++ b/lib/backup/bloc/backup_cubit.dart @@ -13,6 +13,7 @@ import 'package:bip85/bip85.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:hex/hex.dart'; import 'package:intl/intl.dart'; +import 'package:recoverbull_dart/recoverbull_dart.dart'; class BackupCubit extends Cubit { BackupCubit({ @@ -26,104 +27,206 @@ class BackupCubit extends Cubit { final WalletSensitiveStorageRepository walletSensitiveStorage; Future loadBackupData() async { + emit(state.copyWith(loading: true, error: '')); final backups = []; + final confirmedBackups = state.confirmedBackups; for (final walletBloc in wallets) { - final wallet = walletBloc.state.wallet!; - - final (seed, error) = await walletSensitiveStorage.readSeed( - fingerprintIndex: wallet.getRelatedSeedStorageString(), - ); - final mnemonic = seed?.mnemonic.split(' ') ?? []; - - final passphrase = wallet.hasPassphrase() - ? seed!.passphrases - .firstWhere( - (e) => e.sourceFingerprint == wallet.sourceFingerprint, - ) - .passphrase - : ''; - - final descriptors = [wallet.getDescriptorCombined()]; - - final walletLabels = WalletLabels(); - final labels = await walletLabels.txsToBip329( - wallet.transactions, - wallet.originString(), - ) - ..addAll( - await walletLabels.addressesToBip329( - wallet.myAddressBook, - wallet.originString(), - ), - ); - - backups.add( - Backup( + final wallet = walletBloc.state.wallet; + if (wallet == null) { + emit(state.copyWith(error: 'Wallet data is missing.')); + return; + } + var backup = Backup( name: wallet.name ?? '', network: wallet.network.name.toLowerCase(), layer: wallet.baseWalletType.name.toLowerCase(), script: wallet.scriptType.name.toLowerCase(), type: wallet.type.name.toLowerCase(), - mnemonic: mnemonic, - passphrase: passphrase, - descriptors: descriptors, - labels: labels, - ), - ); + mnemonicFingerPrint: wallet.mnemonicFingerprint); + + final seedStorageString = wallet.getRelatedSeedStorageString(); + + if (confirmedBackups["mnemonic"] == true && + confirmedBackups["passphrase"] == true) { + final (seed, error) = await walletSensitiveStorage.readSeed( + fingerprintIndex: seedStorageString, + ); + if (error != null) { + emit(state.copyWith(error: 'Error reading seed: ${error.message}')); + return; + } + if (seed == null) { + emit(state.copyWith(error: 'Seed data is missing.')); + return; + } + + final mnemonic = seed.mnemonic.split(' '); + + final passphrase = wallet.hasPassphrase() + ? seed.passphrases + .firstWhere( + (e) => e.sourceFingerprint == wallet.sourceFingerprint, + ) + .passphrase + : ''; + backup = backup.copyWith(mnemonic: mnemonic, passphrase: passphrase); + } + // why backup the descriptors since in _recoverBackup() calling [oneFromBIP39] to generate the descriptors again + if (confirmedBackups["descriptors"] == true) { + final descriptors = [wallet.getDescriptorCombined()]; + backup = backup.copyWith(descriptors: descriptors); + } + + // why backup the labels since in restore we are not using them _recoverBackup() + if (confirmedBackups["labels"] == true) { + final walletLabels = WalletLabels(); + final labels = await walletLabels.txsToBip329( + wallet.transactions, + wallet.originString(), + ) + ..addAll( + await walletLabels.addressesToBip329( + wallet.myAddressBook, + wallet.originString(), + ), + ); + backup = backup.copyWith(labels: labels); + } + backups.add(backup); } - emit(state.copyWith(backups: backups, loading: false)); + emit(state.copyWith(loadedBackups: backups, loading: false)); } - Future writeEncryptedBackup() async { - final backups = state.backups; + Future loadConfirmedBackups() async { + const confirmedBackups = { + "mnemonic": true, + "passphrase": true, + "descriptors": true, + "labels": true, + "script": true, + }; + emit(state.copyWith(confirmedBackups: confirmedBackups, loading: false)); + } - final firstMnemonic = backups.first.mnemonic; - final bdkMnemonic = await bdk.Mnemonic.fromString(firstMnemonic.join(' ')); - final xprv = await bdk.DescriptorSecretKey.create( - network: bdk.Network.bitcoin, // TODO: handle testnet? - mnemonic: bdkMnemonic, - password: '', // TODO: which passphrase? - ); - final rootXprv = xprv.toString().substring(0, 111); // remove /* - - final derived = derive(xprv: rootXprv, path: "m/1608'/0'"); - final backupKey = HEX.encode(derived.sublist(0, 32)); - final backupId = HEX.encode(Crypto.generateRandomBytes(32)); - - final plaintext = json.encode(backups.map((i) => i.toJson()).toList()); - final ciphertext = Crypto.aesEncrypt(plaintext, backupKey); - // TODO : extract nonce? - - final now = DateTime.now(); - final formattedDate = DateFormat('yyyyMMdd_HHmm').format(now); - final filename = '$formattedDate.json'; - - final (appDir, errDir) = await fileStorage.getAppDirectory(); - if (errDir != null) { - emit(state.copyWith(error: 'Fail to get Download directory')); - } + void toggleDescriptors() { + _toggleBackupOption("descriptors"); + } + + void toggleLabels() { + _toggleBackupOption("labels"); + } + + void _toggleBackupOption(String option) { + final confirmedBackups = Map.from(state.confirmedBackups); + final confirmed = confirmedBackups[option] ?? false; + confirmedBackups[option] = !confirmed; + emit(state.copyWith(confirmedBackups: confirmedBackups)); + } - final backupDir = - await Directory('${appDir!}/backups/').create(recursive: true); - final file = File(backupDir.path + filename); - final content = json.encode({'id': backupId, 'encrypted': ciphertext}); + void toggleAllMnemonicAndPassphrase() { + final confirmedBackups = Map.from(state.confirmedBackups); + final areBothConfirmed = confirmedBackups["mnemonic"] == true && + confirmedBackups["passphrase"] == true; + final newConfirmed = !areBothConfirmed; + confirmedBackups["mnemonic"] = newConfirmed; + confirmedBackups["passphrase"] = newConfirmed; + emit(state.copyWith(confirmedBackups: confirmedBackups)); + } - final (f, errSave) = await fileStorage.saveToFile(file, content); - if (errSave != null) { - emit(state.copyWith(error: 'Fail to save backup')); + Future writeEncryptedBackup() async { + emit(state.copyWith(loading: true, error: '')); + await loadBackupData(); + final backups = state.loadedBackups; + if (backups.isEmpty) { + emit(state.copyWith(error: 'No backup data available.')); + return; } + // TODO; Implement a proper backup key generation logic in case the user has not provided a mnemonic + final firstMnemonic = backups.first.mnemonic; - emit( - state.copyWith( - backupId: backupId, - backupKey: backupKey, - backupPath: file.path, - backupName: filename, - ), - ); + try { + final backupKey = await _createBackupKey( + firstMnemonic, + // TODO; Implement a proper network selection logic + bdk.Network.bitcoin, + ); + final backupId = HEX.encode(Crypto.generateRandomBytes(32)); + + final plaintext = json.encode(backups.map((i) => i.toJson()).toList()); + final encrypted = + await BackupService.createBackup(backupId, plaintext, backupKey); + final now = DateTime.now(); + final formattedDate = DateFormat('yyyyMMdd_HHmm').format(now); + final filename = '$formattedDate.json'; + + final (appDir, errDir) = await fileStorage.getAppDirectory(); + if (errDir != null) { + emit(state.copyWith(error: 'Failed to get application directory.')); + return; + } + + final backupDir = + await Directory('${appDir!}/backups/').create(recursive: true); + final file = File(backupDir.path + filename); + + final (f, errSave) = await fileStorage.saveToFile( + file, HEX.encode(utf8.encode(encrypted))); + if (errSave != null) { + emit(state.copyWith(error: 'Failed to save backup file.')); + return; + } + + emit( + state.copyWith( + backupId: backupId, + backupKey: backupKey, + backupPath: file.path, + backupName: filename, + loading: false, + ), + ); + } catch (e) { + emit(state.copyWith(error: 'An unexpected error occurred: $e')); + } } void clearError() => emit(state.copyWith(error: '')); } + +Future _createBackupKey( + List? mnemonicWords, bdk.Network network) async { + late final bdk.Mnemonic bdkMnemonic; + + if (mnemonicWords != null && mnemonicWords.isNotEmpty) { + // Mnemonic is present: Use it + final mnemonicString = mnemonicWords.join(' '); + bdkMnemonic = await bdk.Mnemonic.fromString(mnemonicString); + } else { + // Mnemonic is absent: Generate a new random one + bdkMnemonic = await bdk.Mnemonic.create(bdk.WordCount.words12); + } + final descriptorSecretKey = await bdk.DescriptorSecretKey.create( + network: network, + mnemonic: bdkMnemonic, + //TODO: Implement actual password logic + password: '', // Passphrase (if any) + ); + + final extendedPrivateKey = descriptorSecretKey.asString().substring(0, 111); + const String derivationPath = "m/1608'/0'"; + final derivedKeyBytes = + _deriveBip85(xprv: extendedPrivateKey, path: derivationPath); + final backupKeyHex = HEX.encode(derivedKeyBytes.sublist(0, 32)); + return backupKeyHex; +} + +List _deriveBip85({required String xprv, required String path}) { + //TODO: Implement actual derivation logic + // This is a dummy implementation for demonstration purposes. + // Replace this with your actual derivation logic. + print("Deriving with xprv: $xprv and path: $path"); + final derived = derive(xprv: xprv, path: path); + return derived.sublist(0, 64); // Dummy 64-byte result +} diff --git a/lib/backup/bloc/backup_state.dart b/lib/backup/bloc/backup_state.dart index ea4d31921..6657f90be 100644 --- a/lib/backup/bloc/backup_state.dart +++ b/lib/backup/bloc/backup_state.dart @@ -7,7 +7,8 @@ part 'backup_state.freezed.dart'; class BackupState with _$BackupState { const factory BackupState({ @Default(true) bool loading, - @Default([]) List backups, + @Default([]) List loadedBackups, + @Default({}) Map confirmedBackups, @Default('') String backupId, @Default('') String backupPath, @Default('') String backupName, From d29b97e507779f586b066b6d150d9cb2612cc4e3 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Thu, 9 Jan 2025 19:05:12 -0500 Subject: [PATCH 031/401] code cleanup --- lib/_pkg/gdrive.dart | 60 +++++++++++++++++++++----------------------- 1 file changed, 29 insertions(+), 31 deletions(-) diff --git a/lib/_pkg/gdrive.dart b/lib/_pkg/gdrive.dart index e0190621b..827659714 100644 --- a/lib/_pkg/gdrive.dart +++ b/lib/_pkg/gdrive.dart @@ -1,18 +1,16 @@ -import 'dart:convert'; - import 'package:extension_google_sign_in_as_googleapis_auth/extension_google_sign_in_as_googleapis_auth.dart'; import 'package:google_sign_in/google_sign_in.dart'; import 'package:googleapis/drive/v3.dart'; -class Gdrive { +class GoogleDriveApi { static final google = GoogleSignIn(scopes: [DriveApi.driveFileScope]); final DriveApi api; final GoogleSignInAccount account; - Gdrive._(this.api, this.account); + GoogleDriveApi._(this.api, this.account); - static Future connect() async { + static Future connect() async { final account = await google.signIn(); if (account == null) { print("User not signed in"); @@ -26,34 +24,34 @@ class Gdrive { } final api = DriveApi(client); - return Gdrive._(api, account); + return GoogleDriveApi._(api, account); } static Future disconnect() async => google.disconnect(); - Future write({required String filename, required Map content}) async { - try { - // Create an empty file in the appDataFolder - final file = File()..name = filename; - final createdFile = await api.files.create(file); - - if (createdFile.id == null) { - print("Failed to create file."); - return false; - } - - // Update the file with content - final media = Media( - Stream.value(utf8.encode(jsonEncode(content))), - utf8.encode(jsonEncode(content)).length, - ); - await api.files.update(file, createdFile.id!, uploadMedia: media); - - print("File created"); - return true; - } catch (e) { - print("Error: $e"); - return false; - } - } + // Future write({required String filename, required Map content}) async { + // try { + // // Create an empty file in the appDataFolder + // final file = File()..name = filename; + // final createdFile = await api.files.create(file); + // + // if (createdFile.id == null) { + // print("Failed to create file."); + // return false; + // } + // + // // Update the file with content + // final media = Media( + // Stream.value(utf8.encode(jsonEncode(content))), + // utf8.encode(jsonEncode(content)).length, + // ); + // await api.files.update(file, createdFile.id!, uploadMedia: media); + // + // print("File created"); + // return true; + // } catch (e) { + // print("Error: $e"); + // return false; + // } + // } } From 9d6e544c56f8b0eeec94e80e19a2dfa876b6fa4a Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Thu, 9 Jan 2025 19:09:22 -0500 Subject: [PATCH 032/401] refactor: replaced Gdrive with GoogleDriveStorage for drive functions --- lib/backup/bloc/cloud_cubit.dart | 37 +++++++++++++++++--------------- lib/backup/bloc/cloud_state.dart | 3 ++- lib/backup/cloud_page.dart | 4 ++-- 3 files changed, 24 insertions(+), 20 deletions(-) diff --git a/lib/backup/bloc/cloud_cubit.dart b/lib/backup/bloc/cloud_cubit.dart index 2857a3932..a944bf554 100644 --- a/lib/backup/bloc/cloud_cubit.dart +++ b/lib/backup/bloc/cloud_cubit.dart @@ -1,9 +1,11 @@ -import 'dart:convert'; import 'dart:io'; import 'package:bb_mobile/_pkg/gdrive.dart'; import 'package:bb_mobile/backup/bloc/cloud_state.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:hex/hex.dart'; +import 'package:recoverbull_dart/recoverbull_dart.dart'; class CloudCubit extends Cubit { final String backupPath; @@ -18,37 +20,38 @@ class CloudCubit extends Cubit { Future connectAndStoreBackup() async { try { - final gdrive = await Gdrive.connect(); - - if (gdrive != null) { - emit(state.copyWith(gdrive: gdrive)); - print("User has logged in" + "Sign in"); + final googleSignInAccount = await GoogleDriveApi.google.signIn(); + final googleDriveStorage = await GoogleDriveStorage.connect( + googleSignInAccount, + "bullbitcoin/backups", + ); + if (googleSignInAccount != null) { + emit(state.copyWith(googleDriveStorage: googleDriveStorage)); + debugPrint("User has logged in"); } else { - print("User has not logged in" + "Sign in"); + debugPrint("User has not logged in"); } } catch (e) { - print(e); + debugPrint("GoogleDriveStorage.connect Error: $e"); } - if (state.gdrive == null) { - emit(state.copyWith(toast: 'not connected')); + if (state.googleDriveStorage == null) { + emit(state.copyWith(toast: 'User has not logged in')); return; } final backup = File(backupPath); final content = await backup.readAsString(); - - final bool isCreated = await state.gdrive!.write( - filename: backupName, - content: json.decode(content) as Map, - ); + final decoded = HEX.decode(content); + final isCreated = + await state.googleDriveStorage?.writeMetaData(decoded, backupName); if (isCreated == false) { - emit(state.copyWith(toast: "Not created")); + emit(state.copyWith(toast: "Failed to backup file to google drive.")); } else { emit(state.copyWith(toast: "File created successfully.")); } } - void disconnect() => Gdrive.disconnect(); + void disconnect() => GoogleDriveApi.disconnect(); } diff --git a/lib/backup/bloc/cloud_state.dart b/lib/backup/bloc/cloud_state.dart index 331581cc3..7db5fccab 100644 --- a/lib/backup/bloc/cloud_state.dart +++ b/lib/backup/bloc/cloud_state.dart @@ -1,5 +1,6 @@ import 'package:bb_mobile/_pkg/gdrive.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:recoverbull_dart/recoverbull_dart.dart'; part 'cloud_state.freezed.dart'; @@ -7,7 +8,7 @@ part 'cloud_state.freezed.dart'; class CloudState with _$CloudState { const factory CloudState({ @Default(true) bool loading, - Gdrive? gdrive, + GoogleDriveStorage? googleDriveStorage, @Default('') String toast, }) = _CloudState; } diff --git a/lib/backup/cloud_page.dart b/lib/backup/cloud_page.dart index 1fe0d2597..1fd4f3e89 100644 --- a/lib/backup/cloud_page.dart +++ b/lib/backup/cloud_page.dart @@ -48,10 +48,10 @@ class CloudPage extends StatelessWidget { onPressed: cubit.connectAndStoreBackup, child: const Text("Google Drive"), ), - if (state.gdrive != null) + if (state.googleDriveStorage != null) ElevatedButton( onPressed: cubit.disconnect, - child: const Text("log out"), + child: const Text("Log out"), ), ], ), From 96535e1ef065ea0b0a296a57c4f0cbab2e27976c Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Thu, 9 Jan 2025 19:11:04 -0500 Subject: [PATCH 033/401] refactor: replaced keychainapi http calls with KeyManagementService functions --- lib/backup/bloc/keychain_cubit.dart | 29 +++------------------ lib/recover/bloc/keychain_cubit.dart | 38 ++++++++-------------------- 2 files changed, 13 insertions(+), 54 deletions(-) diff --git a/lib/backup/bloc/keychain_cubit.dart b/lib/backup/bloc/keychain_cubit.dart index ff6999e13..edea42641 100644 --- a/lib/backup/bloc/keychain_cubit.dart +++ b/lib/backup/bloc/keychain_cubit.dart @@ -1,11 +1,7 @@ -import 'dart:convert'; - import 'package:bb_mobile/_pkg/consts/configs.dart'; -import 'package:bb_mobile/_pkg/crypto.dart'; import 'package:bb_mobile/backup/bloc/keychain_state.dart'; -import 'package:dio/dio.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:hex/hex.dart'; +import 'package:recoverbull_dart/recoverbull_dart.dart'; class KeychainCubit extends Cubit { KeychainCubit() : super(const KeychainState()); @@ -27,32 +23,13 @@ class KeychainCubit extends Cubit { emit(state.copyWith(error: 'keychain api is not set')); return; } - await _storeBackupKey(backupId, backupKey); } Future _storeBackupKey(String backupId, String backupKey) async { - final secretHashBytes = Crypto.sha256(utf8.encode(state.secret)); - final secretHashHex = HEX.encode(secretHashBytes); - try { - final response = await Dio().post( - '$keychainapi/store_key', - options: Options(headers: {'Content-Type': 'application/json'}), - data: { - 'backup_id': backupId, - 'backup_key': backupKey, - 'secret_hash': secretHashHex, - }, - ); - - if (response.statusCode == 201) { - emit(state.copyWith(completed: true)); - } else if (response.statusCode == 403) { - emit(state.copyWith(error: 'Key already stored')); - } else { - emit(state.copyWith(error: 'Key not secured \n${response.statusCode}')); - } + await KeyManagementService(keychainapi: keychainapi) + .storeBackupKey(backupId, backupKey, state.secret); } catch (e) { print(e); emit(state.copyWith(error: 'Server Inaccessible')); diff --git a/lib/recover/bloc/keychain_cubit.dart b/lib/recover/bloc/keychain_cubit.dart index 34c2474ec..5e2c07cb7 100644 --- a/lib/recover/bloc/keychain_cubit.dart +++ b/lib/recover/bloc/keychain_cubit.dart @@ -1,12 +1,8 @@ -import 'dart:convert'; - import 'package:bb_mobile/_pkg/consts/configs.dart'; -import 'package:bb_mobile/_pkg/crypto.dart'; import 'package:bb_mobile/_pkg/file_picker.dart'; import 'package:bb_mobile/recover/bloc/keychain_state.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:hex/hex.dart'; -import 'package:http/http.dart' as http; +import 'package:recoverbull_dart/recoverbull_dart.dart'; class KeychainCubit extends Cubit { KeychainCubit({required String backupId, required this.filePicker}) @@ -32,30 +28,16 @@ class KeychainCubit extends Cubit { } void _recoverBackupKey(String secret, String backupId) async { - final secretHash = Crypto.sha256(utf8.encode(secret)); - - if (keychainapi.isEmpty) { - emit(state.copyWith(error: 'keychain api is not set')); - return; - } - - final response = await http.post( - Uri.parse('$keychainapi/recover_key'), - headers: {'Content-Type': 'application/json'}, - body: jsonEncode({ - 'backup_id': state.backupId, - 'secret_hash': HEX.encode(secretHash), - }), - ); - final body = jsonDecode(response.body); - final backupKey = body['backup_key']; - - if (response.statusCode == 200 && - backupKey != null && - backupKey is String) { + try { + if (keychainapi.isEmpty) { + emit(state.copyWith(error: 'keychain api is not set')); + return; + } + final backupKey = await KeyManagementService(keychainapi: keychainapi) + .recoverBackupKey(backupId, secret); emit(state.copyWith(backupKey: backupKey)); - } else { - emit(state.copyWith(error: '${response.statusCode} | ${response.body}')); + } on KeyManagementException catch (e) { + emit(state.copyWith(error: e.message)); } } } From 18d23d67ee7a182a523d1f8de90326c1de19981b Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Thu, 9 Jan 2025 19:11:34 -0500 Subject: [PATCH 034/401] code cleanup --- lib/backup/keychain_page.dart | 96 +++++++++++++++++++---------------- 1 file changed, 52 insertions(+), 44 deletions(-) diff --git a/lib/backup/keychain_page.dart b/lib/backup/keychain_page.dart index 7c1ee17c1..3ae0cd00f 100644 --- a/lib/backup/keychain_page.dart +++ b/lib/backup/keychain_page.dart @@ -5,6 +5,7 @@ import 'package:bb_mobile/home/bloc/home_cubit.dart'; import 'package:bb_mobile/locator.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:gap/gap.dart'; import 'package:go_router/go_router.dart'; class KeychainBackupPage extends StatelessWidget { @@ -25,6 +26,7 @@ class KeychainBackupPage extends StatelessWidget { backgroundColor: Colors.amber, appBar: AppBar( automaticallyImplyLeading: false, + elevation: 0, flexibleSpace: BBAppBar( text: 'Keychain Backup', onBack: () => context.pop(), @@ -55,53 +57,59 @@ class KeychainBackupPage extends StatelessWidget { child: BlocBuilder( builder: (context, state) { final cubit = context.read(); - return Column( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Column( - children: [ - SelectableText('Backup Key: $backupKey'), - SelectableText('Backup ID: $backupId'), - ], - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - SizedBox( - width: 100, - child: TextFormField( - decoration: - const InputDecoration(labelText: 'Enter PIN'), - keyboardType: TextInputType.number, - maxLength: 6, - onChanged: (value) => cubit.updateSecret(value), + return Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + SelectableText('Backup Key: $backupKey'), + Gap(8), + SelectableText('Backup ID: $backupId'), + ], + ), + Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + SizedBox( + width: double.infinity, + child: TextFormField( + decoration: const InputDecoration( + labelText: 'Enter PIN', + ), + keyboardType: TextInputType.number, + maxLength: 6, + onChanged: (value) => cubit.updateSecret(value), + ), ), - ), - SizedBox( - width: 100, - child: TextFormField( - decoration: - const InputDecoration(labelText: 'Confirm PIN'), - keyboardType: TextInputType.number, - obscureText: true, - maxLength: 6, - onChanged: (value) => cubit.confirmSecret(value), + SizedBox( + width: double.infinity, + child: TextFormField( + decoration: + const InputDecoration(labelText: 'Confirm PIN'), + keyboardType: TextInputType.number, + obscureText: true, + maxLength: 6, + onChanged: (value) => cubit.confirmSecret(value), + ), ), - ), - ], - ), - if (state.secretConfirmed) - ElevatedButton( - onPressed: () => - cubit.clickSecureKey(backupId, backupKey), - child: const Text('Secure my backup key'), - ), - if (!state.secretConfirmed) - const Text( - 'PINs do not match! Please confirm your PIN.', - style: TextStyle(color: Colors.red), + ], ), - ], + if (state.secretConfirmed) + ElevatedButton( + onPressed: () => + cubit.clickSecureKey(backupId, backupKey), + child: const Text('Secure my backup key'), + ), + if (!state.secretConfirmed) + const Text( + 'PINs do not match! Please confirm your PIN.', + style: TextStyle(color: Colors.red), + ), + ], + ), ); }, ), From adcdd6f887854da243c1e8faf45dbb41e112793e Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Thu, 9 Jan 2025 19:14:55 -0500 Subject: [PATCH 035/401] refactor: Implement restoration of backups without mnemonic using fingerprint identification --- lib/recover/bloc/manual_cubit.dart | 72 +++++++++++++++++++++++------- 1 file changed, 56 insertions(+), 16 deletions(-) diff --git a/lib/recover/bloc/manual_cubit.dart b/lib/recover/bloc/manual_cubit.dart index fc033536d..7b2793ba3 100644 --- a/lib/recover/bloc/manual_cubit.dart +++ b/lib/recover/bloc/manual_cubit.dart @@ -2,7 +2,6 @@ import 'dart:convert'; import 'package:bb_mobile/_model/backup.dart'; import 'package:bb_mobile/_model/wallet.dart'; -import 'package:bb_mobile/_pkg/crypto.dart'; import 'package:bb_mobile/_pkg/file_picker.dart'; import 'package:bb_mobile/_pkg/wallet/bdk/sensitive_create.dart'; import 'package:bb_mobile/_pkg/wallet/create.dart'; @@ -13,6 +12,8 @@ import 'package:bb_mobile/_pkg/wallet/repository/storage.dart'; import 'package:bb_mobile/recover/bloc/manual_state.dart'; import 'package:bb_mobile/wallet/bloc/wallet_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:hex/hex.dart'; +import 'package:recoverbull_dart/recoverbull_dart.dart'; class ManualCubit extends Cubit { ManualCubit({ @@ -50,16 +51,14 @@ class ManualCubit extends Cubit { emit(state.copyWith(error: 'Empty file')); return; } - - final json = jsonDecode(file); - final id = json['id']?.toString() ?? ''; - final encrypted = json['encrypted']?.toString() ?? ''; - if (encrypted.isEmpty || id.isEmpty) { + final decodeEncryptedFile = utf8.decode(HEX.decode(file)); + final id = jsonDecode(decodeEncryptedFile)['backupId']?.toString() ?? ''; + if (decodeEncryptedFile.isEmpty || id.isEmpty) { emit(state.copyWith(error: 'Invalid backup')); return; } - emit(state.copyWith(backupId: id, encrypted: encrypted)); + emit(state.copyWith(backupId: id, encrypted: decodeEncryptedFile)); } Future clickRecover() async { @@ -74,13 +73,13 @@ class ManualCubit extends Cubit { } try { - final plaintext = Crypto.aesDecrypt(state.encrypted, state.backupKey); + final plaintext = + await BackupService.restoreBackup(state.encrypted, state.backupKey); final decodedJson = jsonDecode(plaintext) as List; final backups = decodedJson .map((item) => Backup.fromJson(item as Map)) .toList(); - for (final backup in backups) { final network = switch (backup.network.toLowerCase()) { 'mainnet' => BBNetwork.Mainnet, @@ -118,17 +117,58 @@ class ManualCubit extends Cubit { } if (backup.mnemonic.isNotEmpty) { - await _addWallet( - backup.mnemonic.join(' '), - backup.passphrase, + await _addOrUpdateWallet( network, layer, script, type, + backup.mnemonic.join(' '), + backup.passphrase, ); + } else { + //find the mnemonic & passphrase associated with the fingerprint. + + for (final walletBloc in wallets) { + if (walletBloc.state.wallet!.mnemonicFingerprint == + backup.mnemonicFingerPrint) { + final seedStorageString = + walletBloc.state.wallet!.getRelatedSeedStorageString(); + final (seed, error) = await walletSensitiveStorage.readSeed( + fingerprintIndex: seedStorageString, + ); + if (error != null) { + emit(state.copyWith( + error: 'Error reading seed: ${error.message}')); + return false; + } + if (seed == null) { + emit(state.copyWith(error: 'Seed data is missing.')); + return false; + } + final passphrase = walletBloc.state.wallet!.hasPassphrase() + ? seed.passphrases + .firstWhere( + (e) => + e.sourceFingerprint == + walletBloc.state.wallet!.sourceFingerprint, + ) + .passphrase + : ''; + await _addOrUpdateWallet( + network, + layer, + script, + type, + seed.mnemonic, + passphrase, + ); + } else { + emit(state.copyWith(error: 'Backup does not match any wallet')); + return false; + } + } } } - return true; } catch (e) { print(e); @@ -137,13 +177,13 @@ class ManualCubit extends Cubit { } } - Future _addWallet( - String mnemonic, - String passphrase, + Future _addOrUpdateWallet( BBNetwork network, BaseWalletType layer, ScriptType script, BBWalletType type, + String mnemonic, + String passphrase, ) async { try { final (seed, error) = From 994fedb4630f15b2875de6b510406907991c9115 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Thu, 9 Jan 2025 19:15:04 -0500 Subject: [PATCH 036/401] code cleanup --- lib/recover/manual_page.dart | 68 +++++++++++++++++++----------------- 1 file changed, 36 insertions(+), 32 deletions(-) diff --git a/lib/recover/manual_page.dart b/lib/recover/manual_page.dart index 3fc68dc4d..4dab1d3a2 100644 --- a/lib/recover/manual_page.dart +++ b/lib/recover/manual_page.dart @@ -39,6 +39,7 @@ class ManualRecoverPage extends StatelessWidget { backgroundColor: Colors.amber, appBar: AppBar( automaticallyImplyLeading: false, + elevation: 0, flexibleSpace: BBAppBar( text: 'Recover Backup', onBack: () => context.pop(), @@ -71,39 +72,42 @@ class ManualRecoverPage extends StatelessWidget { builder: (context, state) { final cubit = context.read(); - return Column( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - if (!state.recovered && - state.backupId.isNotEmpty && - state.backupKey.isEmpty) - ElevatedButton( - onPressed: () => context.push( - '/keychain-recover', - extra: state.backupId, + return Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + if (!state.recovered && + state.backupId.isNotEmpty && + state.backupKey.isEmpty) + ElevatedButton( + onPressed: () => context.push( + '/keychain-recover', + extra: state.backupId, + ), + child: const Text('Keychain'), ), - child: const Text('Keychain'), - ), - if (!state.recovered && state.backupId.isNotEmpty) - TextFormField( - decoration: - const InputDecoration(labelText: 'Backup Key'), - maxLength: 64, - onChanged: (value) => cubit.updateBackupKey(value), - ), - if (!state.recovered && state.backupKey.isEmpty) - BBButton.big( - label: 'Select File', - center: true, - onPressed: () => cubit.selectFile(), - ), - if (!state.recovered && state.backupKey.isNotEmpty) - BBButton.big( - label: 'Recover', - center: true, - onPressed: () => cubit.clickRecover(), - ), - ], + if (!state.recovered && state.backupId.isNotEmpty) + TextFormField( + decoration: + const InputDecoration(labelText: 'Backup Key'), + maxLength: 64, + onChanged: (value) => cubit.updateBackupKey(value), + ), + if (!state.recovered && state.backupKey.isEmpty) + BBButton.big( + label: 'Select File', + center: true, + onPressed: () => cubit.selectFile(), + ), + if (!state.recovered && state.backupKey.isNotEmpty) + BBButton.big( + label: 'Recover', + center: true, + onPressed: () => cubit.clickRecover(), + ), + ], + ), ); }, ), From 2fccecba50679633150d7c7f59e4e69336cfe963 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Tue, 14 Jan 2025 08:57:29 -0500 Subject: [PATCH 037/401] refactor: backup file naming format updated --- lib/backup/bloc/backup_cubit.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/backup/bloc/backup_cubit.dart b/lib/backup/bloc/backup_cubit.dart index e4dcc587b..ef55dfe21 100644 --- a/lib/backup/bloc/backup_cubit.dart +++ b/lib/backup/bloc/backup_cubit.dart @@ -12,7 +12,6 @@ import 'package:bdk_flutter/bdk_flutter.dart' as bdk; import 'package:bip85/bip85.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:hex/hex.dart'; -import 'package:intl/intl.dart'; import 'package:recoverbull_dart/recoverbull_dart.dart'; class BackupCubit extends Cubit { @@ -158,8 +157,9 @@ class BackupCubit extends Cubit { final encrypted = await BackupService.createBackup(backupId, plaintext, backupKey); final now = DateTime.now(); - final formattedDate = DateFormat('yyyyMMdd_HHmm').format(now); - final filename = '$formattedDate.json'; + //TODO; Find a better filename format. + final formattedDate = now.millisecondsSinceEpoch; + final filename = '${formattedDate}_$backupId.json'; final (appDir, errDir) = await fileStorage.getAppDirectory(); if (errDir != null) { From 922942653ab5665844c0832d83f3d6e0e9fdbbd9 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Tue, 14 Jan 2025 09:02:36 -0500 Subject: [PATCH 038/401] refactor: renamed recover manual components to recover fs_cloud --- lib/backup/backup_page.dart | 2 ++ .../{manual_cubit.dart => fs_cloud_cubit.dart} | 8 ++++---- .../{manual_state.dart => fs_cloud_state.dart} | 8 ++++---- .../{manual_page.dart => fs_cloud_page.dart} | 16 ++++++++-------- lib/routes.dart | 2 +- 5 files changed, 19 insertions(+), 17 deletions(-) rename lib/recover/bloc/{manual_cubit.dart => fs_cloud_cubit.dart} (97%) rename lib/recover/bloc/{manual_state.dart => fs_cloud_state.dart} (65%) rename lib/recover/{manual_page.dart => fs_cloud_page.dart} (90%) diff --git a/lib/backup/backup_page.dart b/lib/backup/backup_page.dart index f5fd32735..d675ef76d 100644 --- a/lib/backup/backup_page.dart +++ b/lib/backup/backup_page.dart @@ -6,6 +6,8 @@ import 'package:bb_mobile/_ui/components/controls.dart'; import 'package:bb_mobile/_ui/components/text.dart'; import 'package:bb_mobile/backup/bloc/backup_cubit.dart'; import 'package:bb_mobile/backup/bloc/backup_state.dart'; +import 'package:bb_mobile/backup/bloc/cloud_cubit.dart'; +import 'package:bb_mobile/backup/bloc/cloud_state.dart'; import 'package:bb_mobile/locator.dart'; import 'package:bb_mobile/wallet/bloc/wallet_bloc.dart'; import 'package:flutter/material.dart'; diff --git a/lib/recover/bloc/manual_cubit.dart b/lib/recover/bloc/fs_cloud_cubit.dart similarity index 97% rename from lib/recover/bloc/manual_cubit.dart rename to lib/recover/bloc/fs_cloud_cubit.dart index 7b2793ba3..8ebfa2333 100644 --- a/lib/recover/bloc/manual_cubit.dart +++ b/lib/recover/bloc/fs_cloud_cubit.dart @@ -9,14 +9,14 @@ import 'package:bb_mobile/_pkg/wallet/create_sensitive.dart'; import 'package:bb_mobile/_pkg/wallet/lwk/sensitive_create.dart'; import 'package:bb_mobile/_pkg/wallet/repository/sensitive_storage.dart'; import 'package:bb_mobile/_pkg/wallet/repository/storage.dart'; -import 'package:bb_mobile/recover/bloc/manual_state.dart'; +import 'package:bb_mobile/recover/bloc/fs_cloud_state.dart'; import 'package:bb_mobile/wallet/bloc/wallet_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:hex/hex.dart'; import 'package:recoverbull_dart/recoverbull_dart.dart'; -class ManualCubit extends Cubit { - ManualCubit({ +class FsCloudCubit extends Cubit { + FsCloudCubit({ required this.bdkSensitiveCreate, required this.lwkSensitiveCreate, required this.walletSensitiveCreate, @@ -25,7 +25,7 @@ class ManualCubit extends Cubit { required this.wallets, required this.walletSensitiveStorage, required this.filePicker, - }) : super(const ManualState()); + }) : super(const FsCloudState()); final FilePick filePicker; final List wallets; diff --git a/lib/recover/bloc/manual_state.dart b/lib/recover/bloc/fs_cloud_state.dart similarity index 65% rename from lib/recover/bloc/manual_state.dart rename to lib/recover/bloc/fs_cloud_state.dart index e559db9e1..3f6f93120 100644 --- a/lib/recover/bloc/manual_state.dart +++ b/lib/recover/bloc/fs_cloud_state.dart @@ -1,14 +1,14 @@ import 'package:freezed_annotation/freezed_annotation.dart'; -part 'manual_state.freezed.dart'; +part 'fs_cloud_state.freezed.dart'; @freezed -class ManualState with _$ManualState { - const factory ManualState({ +class FsCloudState with _$FsCloudState { + const factory FsCloudState({ @Default('') String error, @Default(false) bool recovered, @Default('') String backupKey, @Default('') String backupId, @Default('') String encrypted, - }) = _ManualState; + }) = _FsCloudState; } diff --git a/lib/recover/manual_page.dart b/lib/recover/fs_cloud_page.dart similarity index 90% rename from lib/recover/manual_page.dart rename to lib/recover/fs_cloud_page.dart index 4dab1d3a2..5e0c7c0aa 100644 --- a/lib/recover/manual_page.dart +++ b/lib/recover/fs_cloud_page.dart @@ -10,8 +10,8 @@ import 'package:bb_mobile/_ui/app_bar.dart'; import 'package:bb_mobile/_ui/components/button.dart'; import 'package:bb_mobile/home/bloc/home_cubit.dart'; import 'package:bb_mobile/locator.dart'; -import 'package:bb_mobile/recover/bloc/manual_cubit.dart'; -import 'package:bb_mobile/recover/bloc/manual_state.dart'; +import 'package:bb_mobile/recover/bloc/fs_cloud_cubit.dart'; +import 'package:bb_mobile/recover/bloc/fs_cloud_state.dart'; import 'package:bb_mobile/wallet/bloc/wallet_bloc.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -24,8 +24,8 @@ class ManualRecoverPage extends StatelessWidget { @override Widget build(BuildContext context) { - return BlocProvider( - create: (_) => ManualCubit( + return BlocProvider( + create: (_) => FsCloudCubit( filePicker: locator(), walletCreate: locator(), walletSensitiveCreate: locator(), @@ -46,7 +46,7 @@ class ManualRecoverPage extends StatelessWidget { buttonKey: UIKeys.settingsBackButton, ), ), - body: BlocListener( + body: BlocListener( listener: (context, state) async { if (state.error.isNotEmpty) { ScaffoldMessenger.of(context).showSnackBar( @@ -55,7 +55,7 @@ class ManualRecoverPage extends StatelessWidget { backgroundColor: Colors.red, ), ); - context.read().clearError(); + context.read().clearError(); } if (state.recovered) { ScaffoldMessenger.of(context).showSnackBar( @@ -68,9 +68,9 @@ class ManualRecoverPage extends StatelessWidget { context.go('/home'); } }, - child: BlocBuilder( + child: BlocBuilder( builder: (context, state) { - final cubit = context.read(); + final cubit = context.read(); return Padding( padding: const EdgeInsets.all(20.0), diff --git a/lib/routes.dart b/lib/routes.dart index c7ef1c71c..4d726fba1 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -15,8 +15,8 @@ import 'package:bb_mobile/import/hardware_page.dart'; import 'package:bb_mobile/import/page.dart'; import 'package:bb_mobile/locator.dart'; import 'package:bb_mobile/receive/receive_page.dart'; +import 'package:bb_mobile/recover/fs_cloud_page.dart'; import 'package:bb_mobile/recover/keychain_page.dart'; -import 'package:bb_mobile/recover/manual_page.dart'; import 'package:bb_mobile/send/bloc/send_cubit.dart'; import 'package:bb_mobile/send/send_page.dart'; import 'package:bb_mobile/settings/application_settings_page.dart'; From 1b3bbf1ee7131c02eb29266bcc4615e8e4f48f16 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Tue, 14 Jan 2025 09:05:25 -0500 Subject: [PATCH 039/401] fix(CloudCubit): clearToast() not updating the CloudState --- lib/backup/bloc/cloud_cubit.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/backup/bloc/cloud_cubit.dart b/lib/backup/bloc/cloud_cubit.dart index a944bf554..19e78c3e6 100644 --- a/lib/backup/bloc/cloud_cubit.dart +++ b/lib/backup/bloc/cloud_cubit.dart @@ -16,7 +16,7 @@ class CloudCubit extends Cubit { required this.backupName, }) : super(const CloudState()); - void clearToast() => state.copyWith(toast: ''); + void clearToast() => emit(state.copyWith(toast: '')); Future connectAndStoreBackup() async { try { From e48cc4f5462d0c90b809a724debf4ee07a33d935 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Tue, 14 Jan 2025 09:07:57 -0500 Subject: [PATCH 040/401] feat: exposed error & clearError to handle cloud backup errors --- lib/backup/bloc/cloud_cubit.dart | 22 ++++++++++++++++------ lib/backup/bloc/cloud_state.dart | 1 + 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/lib/backup/bloc/cloud_cubit.dart b/lib/backup/bloc/cloud_cubit.dart index 19e78c3e6..34ba31159 100644 --- a/lib/backup/bloc/cloud_cubit.dart +++ b/lib/backup/bloc/cloud_cubit.dart @@ -18,6 +18,7 @@ class CloudCubit extends Cubit { void clearToast() => emit(state.copyWith(toast: '')); + void clearError() => emit(state.copyWith(error: '')); Future connectAndStoreBackup() async { try { final googleSignInAccount = await GoogleDriveApi.google.signIn(); @@ -29,14 +30,17 @@ class CloudCubit extends Cubit { emit(state.copyWith(googleDriveStorage: googleDriveStorage)); debugPrint("User has logged in"); } else { - debugPrint("User has not logged in"); + emit(state.copyWith(error: "Google drive user has not authenticated")); + return; } } catch (e) { debugPrint("GoogleDriveStorage.connect Error: $e"); - } - - if (state.googleDriveStorage == null) { - emit(state.copyWith(toast: 'User has not logged in')); + emit( + state.copyWith( + error: "GoogleDriveStorage.connect Error: $e", + loading: false, + ), + ); return; } @@ -47,7 +51,13 @@ class CloudCubit extends Cubit { await state.googleDriveStorage?.writeMetaData(decoded, backupName); if (isCreated == false) { - emit(state.copyWith(toast: "Failed to backup file to google drive.")); + emit( + state.copyWith( + error: "Failed to backup file to google drive.", + loading: false, + ), + ); + return; } else { emit(state.copyWith(toast: "File created successfully.")); } diff --git a/lib/backup/bloc/cloud_state.dart b/lib/backup/bloc/cloud_state.dart index 7db5fccab..ed153d379 100644 --- a/lib/backup/bloc/cloud_state.dart +++ b/lib/backup/bloc/cloud_state.dart @@ -10,5 +10,6 @@ class CloudState with _$CloudState { @Default(true) bool loading, GoogleDriveStorage? googleDriveStorage, @Default('') String toast, + @Default('') String error, }) = _CloudState; } From 241a92f8fe693ff2980a5544dc562b07030d99b7 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Tue, 14 Jan 2025 09:08:44 -0500 Subject: [PATCH 041/401] feat: exposed defaultCloudBackupPath --- lib/_pkg/consts/configs.dart | 4 ++++ lib/backup/bloc/cloud_cubit.dart | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/_pkg/consts/configs.dart b/lib/_pkg/consts/configs.dart index 719ceb0b3..f86a35635 100644 --- a/lib/_pkg/consts/configs.dart +++ b/lib/_pkg/consts/configs.dart @@ -30,3 +30,7 @@ const liquidMempoolTestnet = 'https://liquid.network/testnet'; const liquidMainnetAssetId = lwk.lBtcAssetId; const liquidTestnetAssetId = lwk.lTestAssetId; + +//Backups +const defaultCloudBackupPath = + 'backups'; //todo; create a better folder structure diff --git a/lib/backup/bloc/cloud_cubit.dart b/lib/backup/bloc/cloud_cubit.dart index 34ba31159..828190321 100644 --- a/lib/backup/bloc/cloud_cubit.dart +++ b/lib/backup/bloc/cloud_cubit.dart @@ -24,7 +24,7 @@ class CloudCubit extends Cubit { final googleSignInAccount = await GoogleDriveApi.google.signIn(); final googleDriveStorage = await GoogleDriveStorage.connect( googleSignInAccount, - "bullbitcoin/backups", + defaultCloudBackupPath, ); if (googleSignInAccount != null) { emit(state.copyWith(googleDriveStorage: googleDriveStorage)); From cdf19a66de5d1e718da76b8e300678e678a29914 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Tue, 14 Jan 2025 09:14:13 -0500 Subject: [PATCH 042/401] refactor: moved CloudCubit creation --- lib/backup/backup_page.dart | 56 +++++++++++++++++++++++++++++++++---- lib/routes.dart | 8 ++++-- 2 files changed, 57 insertions(+), 7 deletions(-) diff --git a/lib/backup/backup_page.dart b/lib/backup/backup_page.dart index d675ef76d..2c5281b8b 100644 --- a/lib/backup/backup_page.dart +++ b/lib/backup/backup_page.dart @@ -127,11 +127,57 @@ class _TheBackupPageState extends State { ), Gap(50), if (state.backupPath.isNotEmpty) - Center( - child: BBButton.big( - onPressed: () => context.push( - '/cloud-backup', - extra: (state.backupPath, state.backupName), + BlocProvider( + create: (context) => CloudCubit( + backupPath: state.backupPath, + backupName: state.backupName, + ), + child: Center( + child: BlocConsumer( + listener: (context, cloudState) { + if (!cloudState.loading) { + if (cloudState.error != '') { + ScaffoldMessenger.of(context) + .showSnackBar( + SnackBar( + content: Text( + cloudState.error, + textAlign: TextAlign.center, + ), + backgroundColor: Colors.red, + ), + ); + context.read().clearError(); + } else { + ScaffoldMessenger.of(context) + .showSnackBar( + SnackBar( + content: Text( + cloudState.toast, + textAlign: TextAlign.center, + ), + backgroundColor: Colors.green, + ), + ); + context.read().clearToast(); + } + } + }, + builder: (context, cloudState) { + return BBButton.big( + loading: cloudState.loading, + onPressed: () { + context + .read() + .connectAndStoreBackup(); + context.push( + '/cloud-backup', + extra: context.read(), + ); + }, + label: "SAVE TO GOOGLE DRIVE", + ); + }, ), label: "SAVE TO CLOUD", ), diff --git a/lib/routes.dart b/lib/routes.dart index 4d726fba1..5e203e797 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -5,6 +5,7 @@ import 'package:bb_mobile/_model/transaction.dart'; import 'package:bb_mobile/_ui/logger_page.dart'; import 'package:bb_mobile/auth/page.dart'; import 'package:bb_mobile/backup/backup_page.dart'; +import 'package:bb_mobile/backup/bloc/cloud_cubit.dart'; import 'package:bb_mobile/backup/cloud_page.dart'; import 'package:bb_mobile/backup/keychain_page.dart'; import 'package:bb_mobile/create/page.dart'; @@ -222,8 +223,11 @@ GoRouter setupRouter() => GoRouter( GoRoute( path: '/cloud-backup', builder: (context, state) { - final (backupPath, backupName) = state.extra! as (String, String); - return CloudPage(backupPath: backupPath, backupName: backupName); + final cloudCubit = state.extra! as CloudCubit; + return BlocProvider.value( + value: cloudCubit, + child: const CloudPage(), + ); }, ), From b986beb702a645f7e24e57939d97c165345f5498 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Tue, 14 Jan 2025 09:14:28 -0500 Subject: [PATCH 043/401] code cleanup --- lib/backup/backup_page.dart | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/backup/backup_page.dart b/lib/backup/backup_page.dart index 2c5281b8b..cd0d4b9fd 100644 --- a/lib/backup/backup_page.dart +++ b/lib/backup/backup_page.dart @@ -105,7 +105,7 @@ class _TheBackupPageState extends State { if (state.backupKey.isNotEmpty) Column( children: [ - BBText.bodyBold("Generated Backup Key"), + const BBText.bodyBold("Generated Backup Key"), Gap(10), SelectableText( state.backupKey, @@ -125,7 +125,7 @@ class _TheBackupPageState extends State { ), ], ), - Gap(50), + const Gap(50), if (state.backupPath.isNotEmpty) BlocProvider( create: (context) => CloudCubit( @@ -179,10 +179,9 @@ class _TheBackupPageState extends State { ); }, ), - label: "SAVE TO CLOUD", ), ), - Gap(10), + const Gap(10), ], ), ); From cd07c0f6c42971c9605dafcb0ae5ea9e60f225e7 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Tue, 14 Jan 2025 09:15:02 -0500 Subject: [PATCH 044/401] code cleanup --- ios/Podfile.lock | 6 ------ 1 file changed, 6 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 9862b3654..ad99ba3e0 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -81,8 +81,6 @@ PODS: - "no_screenshot (0.0.1+4)": - Flutter - ScreenProtectorKit (~> 1.3.1) - - nostr_sdk (0.0.1): - - Flutter - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS @@ -115,7 +113,6 @@ DEPENDENCIES: - integration_test (from `.symlinks/plugins/integration_test/ios`) - lwk (from `.symlinks/plugins/lwk/ios`) - no_screenshot (from `.symlinks/plugins/no_screenshot/ios`) - - nostr_sdk (from `.symlinks/plugins/nostr_sdk/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - payjoin_flutter (from `.symlinks/plugins/payjoin_flutter/ios`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) @@ -165,8 +162,6 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/lwk/ios" no_screenshot: :path: ".symlinks/plugins/no_screenshot/ios" - nostr_sdk: - :path: ".symlinks/plugins/nostr_sdk/ios" path_provider_foundation: :path: ".symlinks/plugins/path_provider_foundation/darwin" payjoin_flutter: @@ -196,7 +191,6 @@ SPEC CHECKSUMS: integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573 lwk: 22e06bc5664247d6b2dac91cfe209b63b70dd580 no_screenshot: 67d110f12466f4913b488803d4e498d03ef2889e - nostr_sdk: ef4a055cafc285eb55c5e44057da60bfc96cf3d0 path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 payjoin_flutter: 6397d7b698cdad6453be4949ab6aca1863f6c5e5 permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 From ea4d5e6ec8acb5f1cc8c5954674707127c02b0d4 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Tue, 14 Jan 2025 09:16:06 -0500 Subject: [PATCH 045/401] feat: exposed readAllBackups --- lib/backup/bloc/cloud_cubit.dart | 37 ++++++++++++++++++++++++++++++-- lib/backup/bloc/cloud_state.dart | 5 +++-- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/lib/backup/bloc/cloud_cubit.dart b/lib/backup/bloc/cloud_cubit.dart index 828190321..221c87b66 100644 --- a/lib/backup/bloc/cloud_cubit.dart +++ b/lib/backup/bloc/cloud_cubit.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:bb_mobile/_pkg/consts/configs.dart'; import 'package:bb_mobile/_pkg/gdrive.dart'; import 'package:bb_mobile/backup/bloc/cloud_state.dart'; import 'package:flutter/cupertino.dart'; @@ -21,6 +22,7 @@ class CloudCubit extends Cubit { void clearError() => emit(state.copyWith(error: '')); Future connectAndStoreBackup() async { try { + emit(state.copyWith(loading: true)); final googleSignInAccount = await GoogleDriveApi.google.signIn(); final googleDriveStorage = await GoogleDriveStorage.connect( googleSignInAccount, @@ -28,7 +30,6 @@ class CloudCubit extends Cubit { ); if (googleSignInAccount != null) { emit(state.copyWith(googleDriveStorage: googleDriveStorage)); - debugPrint("User has logged in"); } else { emit(state.copyWith(error: "Google drive user has not authenticated")); return; @@ -59,7 +60,39 @@ class CloudCubit extends Cubit { ); return; } else { - emit(state.copyWith(toast: "File created successfully.")); + emit( + state.copyWith( + toast: "Google drive backup successful", + loading: false, + ), + ); + } + } + + Future readAllBackups() async { + try { + emit( + state.copyWith( + loading: true, + ), + ); + final availableBackups = + await state.googleDriveStorage!.readAllMetaDataFiles(); + + emit( + state.copyWith( + loading: false, + toast: "Found ${availableBackups.length} backups files", + availableBackups: availableBackups, + ), + ); + } catch (e) { + emit( + state.copyWith( + loading: false, + error: "Failed to read all backups: $e", + ), + ); } } diff --git a/lib/backup/bloc/cloud_state.dart b/lib/backup/bloc/cloud_state.dart index ed153d379..d1cbd5605 100644 --- a/lib/backup/bloc/cloud_state.dart +++ b/lib/backup/bloc/cloud_state.dart @@ -1,5 +1,5 @@ -import 'package:bb_mobile/_pkg/gdrive.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:googleapis/drive/v3.dart'; import 'package:recoverbull_dart/recoverbull_dart.dart'; part 'cloud_state.freezed.dart'; @@ -7,8 +7,9 @@ part 'cloud_state.freezed.dart'; @freezed class CloudState with _$CloudState { const factory CloudState({ - @Default(true) bool loading, + @Default(false) bool loading, GoogleDriveStorage? googleDriveStorage, + @Default([]) List availableBackups, @Default('') String toast, @Default('') String error, }) = _CloudState; From 8b245e16d470d0355af65b4f10f34f8aa206ba32 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Tue, 14 Jan 2025 09:16:27 -0500 Subject: [PATCH 046/401] code cleanup --- lib/backup/cloud_page.dart | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/lib/backup/cloud_page.dart b/lib/backup/cloud_page.dart index 1fd4f3e89..2887af74d 100644 --- a/lib/backup/cloud_page.dart +++ b/lib/backup/cloud_page.dart @@ -6,14 +6,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; class CloudPage extends StatelessWidget { - final String backupPath; - final String backupName; - - const CloudPage({ - super.key, - required this.backupPath, - required this.backupName, - }); + const CloudPage({super.key}); @override Widget build(BuildContext context) { From 76df0ec462d709a189f547e7defbadb3fd3db9cd Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Tue, 14 Jan 2025 09:20:09 -0500 Subject: [PATCH 047/401] feat: exposed AvailableBackup list --- lib/backup/cloud_page.dart | 170 ++++++++++++++++++++++++++++--------- 1 file changed, 131 insertions(+), 39 deletions(-) diff --git a/lib/backup/cloud_page.dart b/lib/backup/cloud_page.dart index 2887af74d..5bf5b4172 100644 --- a/lib/backup/cloud_page.dart +++ b/lib/backup/cloud_page.dart @@ -1,58 +1,150 @@ import 'package:bb_mobile/_ui/app_bar.dart'; +import 'package:bb_mobile/_ui/components/button.dart'; +import 'package:bb_mobile/_ui/components/text.dart'; import 'package:bb_mobile/backup/bloc/cloud_cubit.dart'; import 'package:bb_mobile/backup/bloc/cloud_state.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:gap/gap.dart'; import 'package:go_router/go_router.dart'; +import 'package:googleapis/drive/v3.dart'; +import 'package:intl/intl.dart'; class CloudPage extends StatelessWidget { const CloudPage({super.key}); @override Widget build(BuildContext context) { - return BlocProvider( - create: (_) => CloudCubit(backupPath: backupPath, backupName: backupName), - child: BlocListener( - listener: (context, state) { - if (state.toast.isNotEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(state.toast)), - ); - context.read().clearToast(); - } - }, - child: BlocBuilder( - builder: (context, state) { - final cubit = context.read(); - - return Scaffold( - appBar: AppBar( - automaticallyImplyLeading: false, - flexibleSpace: BBAppBar( - text: 'Cloud Backup', - onBack: () => context.pop(), - ), - ), - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - ElevatedButton( - onPressed: cubit.connectAndStoreBackup, - child: const Text("Google Drive"), - ), - if (state.googleDriveStorage != null) - ElevatedButton( - onPressed: cubit.disconnect, - child: const Text("Log out"), + return BlocConsumer( + listener: (context, state) { + if (state.toast.isNotEmpty) { + _showSnackBar(context, state.toast, Colors.green); + context.read().clearToast(); + } + if (state.error.isNotEmpty) { + _showSnackBar(context, state.error, Colors.red); + context.read().clearError(); + } + }, + builder: (context, state) { + final cubit = context.read(); + return Scaffold( + appBar: AppBar( + elevation: 0, + automaticallyImplyLeading: false, + flexibleSpace: BBAppBar( + text: 'Cloud Backup', + onBack: () => context.pop(), + ), + ), + body: Center( + child: state.loading + ? const CircularProgressIndicator() + : Column( + children: [ + const Gap(50), + AvailableBackups( + onFileSelected: (file) { + debugPrint('Selected file: ${file.name}'); + }, ), - ], - ), + const Gap(10), + if (state.googleDriveStorage != null) + BBButton.big( + onPressed: cubit.disconnect, + label: "LOGOUT", + ), + ], + ), + ), + ); + }, + ); + } + + void _showSnackBar(BuildContext context, String message, Color color) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(message), + backgroundColor: color, + ), + ); + } +} + +class AvailableBackups extends StatelessWidget { + const AvailableBackups({super.key, required this.onFileSelected}); + final void Function(File) onFileSelected; + + @override + Widget build(BuildContext context) { + final cubit = context.read(); + return SizedBox( + height: 500, + child: BlocBuilder( + builder: (context, state) { + if (state.availableBackups.isEmpty) { + return Center( + child: Column( + children: [ + const Text('No backups found'), + const Gap(10), + BBButton.big( + onPressed: () { + cubit.clearToast(); + cubit.clearError(); + cubit.readAllBackups(); + }, + label: "READ ALL BACKUPS", + ), + ], ), ); - }, - ), + } + return ListView.separated( + // Use ListView.separated for dividers + itemCount: state.availableBackups.length, + shrinkWrap: true, + separatorBuilder: (context, index) => + const Divider(), // Add dividers between items + itemBuilder: (context, index) => BackupTile( + file: state.availableBackups[index], + onFileSelected: onFileSelected, + ), + ); + }, ), ); } } + +class BackupTile extends StatelessWidget { + const BackupTile({ + super.key, + required this.file, + required this.onFileSelected, + }); + final File file; + final void Function(File) onFileSelected; // Use void Function for clarity + + @override + Widget build(BuildContext context) { + final fileName = file.name?.replaceAll(".json", ""); + final parts = fileName?.split('_'); + final backupId = parts?.last; + final dateTimeString = parts?.first; + final dateTime = + DateTime.fromMillisecondsSinceEpoch(int.parse(dateTimeString!)); + + final formattedDate = DateFormat('yyyy-MM-dd HH:mm').format(dateTime); + return ListTile( + onTap: () => onFileSelected(file), + title: BBText.body( + backupId ?? 'Unnamed File', + isBold: true, + ), + subtitle: BBText.bodySmall( + formattedDate, + )); + } +} From 8815f9f63193f9c16dfc85a88e867109365f750d Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Tue, 14 Jan 2025 15:16:02 -0500 Subject: [PATCH 048/401] feat(BBNetwork): exposed fromString & toBdkNetwork --- lib/_model/wallet.dart | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/lib/_model/wallet.dart b/lib/_model/wallet.dart index a9a2537a5..3dccfb469 100644 --- a/lib/_model/wallet.dart +++ b/lib/_model/wallet.dart @@ -11,7 +11,31 @@ import 'package:freezed_annotation/freezed_annotation.dart'; part 'wallet.freezed.dart'; part 'wallet.g.dart'; -enum BBNetwork { Testnet, Mainnet } +enum BBNetwork { + Testnet, + + Mainnet; + + static BBNetwork fromString(String network) { + switch (network) { + case 'Testnet': + return BBNetwork.Testnet; + case 'Mainnet': + return BBNetwork.Mainnet; + default: + return BBNetwork.Mainnet; + } + } + + bdk.Network toBdkNetwork() { + switch (this) { + case BBNetwork.Testnet: + return bdk.Network.testnet; + case BBNetwork.Mainnet: + return bdk.Network.bitcoin; + } + } +} enum BBWalletType { main, xpub, descriptors, words, coldcard } From 6e8f26d193f9d36f4bcb2cf4f4112eafc0969a6d Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Tue, 14 Jan 2025 15:16:55 -0500 Subject: [PATCH 049/401] refactor: renamed defaultBackupPath --- lib/_pkg/consts/configs.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/_pkg/consts/configs.dart b/lib/_pkg/consts/configs.dart index f86a35635..f618cbdb5 100644 --- a/lib/_pkg/consts/configs.dart +++ b/lib/_pkg/consts/configs.dart @@ -32,5 +32,4 @@ const liquidMainnetAssetId = lwk.lBtcAssetId; const liquidTestnetAssetId = lwk.lTestAssetId; //Backups -const defaultCloudBackupPath = - 'backups'; //todo; create a better folder structure +const defaultBackupPath = 'backups'; //todo; create a better folder structure From edfaf91c54566b3db1b6e342c362e7d20e3d5eef Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Tue, 14 Jan 2025 16:51:39 -0500 Subject: [PATCH 050/401] refactor: exposed setSelectedBack to handle cloud recover --- lib/recover/bloc/fs_cloud_state.dart | 14 --------- ...{fs_cloud_cubit.dart => manual_cubit.dart} | 29 ++++++++++++------- lib/recover/bloc/manual_state.dart | 19 ++++++++++++ 3 files changed, 38 insertions(+), 24 deletions(-) delete mode 100644 lib/recover/bloc/fs_cloud_state.dart rename lib/recover/bloc/{fs_cloud_cubit.dart => manual_cubit.dart} (90%) create mode 100644 lib/recover/bloc/manual_state.dart diff --git a/lib/recover/bloc/fs_cloud_state.dart b/lib/recover/bloc/fs_cloud_state.dart deleted file mode 100644 index 3f6f93120..000000000 --- a/lib/recover/bloc/fs_cloud_state.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'fs_cloud_state.freezed.dart'; - -@freezed -class FsCloudState with _$FsCloudState { - const factory FsCloudState({ - @Default('') String error, - @Default(false) bool recovered, - @Default('') String backupKey, - @Default('') String backupId, - @Default('') String encrypted, - }) = _FsCloudState; -} diff --git a/lib/recover/bloc/fs_cloud_cubit.dart b/lib/recover/bloc/manual_cubit.dart similarity index 90% rename from lib/recover/bloc/fs_cloud_cubit.dart rename to lib/recover/bloc/manual_cubit.dart index 8ebfa2333..1c6fb771e 100644 --- a/lib/recover/bloc/fs_cloud_cubit.dart +++ b/lib/recover/bloc/manual_cubit.dart @@ -2,21 +2,25 @@ import 'dart:convert'; import 'package:bb_mobile/_model/backup.dart'; import 'package:bb_mobile/_model/wallet.dart'; +import 'package:bb_mobile/_pkg/consts/configs.dart'; import 'package:bb_mobile/_pkg/file_picker.dart'; +import 'package:bb_mobile/_pkg/gdrive.dart'; import 'package:bb_mobile/_pkg/wallet/bdk/sensitive_create.dart'; import 'package:bb_mobile/_pkg/wallet/create.dart'; import 'package:bb_mobile/_pkg/wallet/create_sensitive.dart'; import 'package:bb_mobile/_pkg/wallet/lwk/sensitive_create.dart'; import 'package:bb_mobile/_pkg/wallet/repository/sensitive_storage.dart'; import 'package:bb_mobile/_pkg/wallet/repository/storage.dart'; -import 'package:bb_mobile/recover/bloc/fs_cloud_state.dart'; +import 'package:bb_mobile/recover/bloc/manual_state.dart'; import 'package:bb_mobile/wallet/bloc/wallet_bloc.dart'; +import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:googleapis/drive/v3.dart'; import 'package:hex/hex.dart'; import 'package:recoverbull_dart/recoverbull_dart.dart'; -class FsCloudCubit extends Cubit { - FsCloudCubit({ +class ManualCubit extends Cubit { + ManualCubit({ required this.bdkSensitiveCreate, required this.lwkSensitiveCreate, required this.walletSensitiveCreate, @@ -24,10 +28,10 @@ class FsCloudCubit extends Cubit { required this.walletCreate, required this.wallets, required this.walletSensitiveStorage, - required this.filePicker, - }) : super(const FsCloudState()); + this.filePicker, + }) : super(const ManualState()); - final FilePick filePicker; + FilePick? filePicker; final List wallets; final WalletSensitiveStorageRepository walletSensitiveStorage; final WalletsStorageRepository walletsStorageRepository; @@ -39,8 +43,11 @@ class FsCloudCubit extends Cubit { void updateBackupKey(String value) => emit(state.copyWith(backupKey: value)); void clearError() => emit(state.copyWith(error: '')); - Future selectFile() async { - final (file, error) = await filePicker.pickFile(); + Future selectFileFromFs() async { + if (filePicker == null) { + return; + } + final (file, error) = await filePicker!.pickFile(); if (error != null) { emit(state.copyWith(error: error.toString())); @@ -57,10 +64,12 @@ class FsCloudCubit extends Cubit { emit(state.copyWith(error: 'Invalid backup')); return; } - - emit(state.copyWith(backupId: id, encrypted: decodeEncryptedFile)); + setSelectedBack(id, decodeEncryptedFile); } + void setSelectedBack(String id, String encrypted) => + emit(state.copyWith(backupId: id, encrypted: encrypted)); + Future clickRecover() async { final recovered = await _recoverBackup(); if (recovered) emit(state.copyWith(recovered: true)); diff --git a/lib/recover/bloc/manual_state.dart b/lib/recover/bloc/manual_state.dart new file mode 100644 index 000000000..3ed05dc7e --- /dev/null +++ b/lib/recover/bloc/manual_state.dart @@ -0,0 +1,19 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:googleapis/drive/v3.dart'; +import 'package:recoverbull_dart/recoverbull_dart.dart'; + +part 'manual_state.freezed.dart'; + +@freezed +class ManualState with _$ManualState { + const factory ManualState({ + @Default('') String error, + @Default(false) bool loading, + GoogleDriveStorage? googleDriveStorage, + @Default([]) List availableBackups, + @Default(false) bool recovered, + @Default('') String backupKey, + @Default('') String backupId, + @Default('') String encrypted, + }) = _ManualState; +} From 4252b2534a6b91c3bfc5d54cb5fe2bc5f70c729d Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Tue, 14 Jan 2025 16:53:08 -0500 Subject: [PATCH 051/401] refactor: exposed on selected callback as paramter --- lib/routes.dart | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/routes.dart b/lib/routes.dart index 5e203e797..c77d16c08 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -16,7 +16,7 @@ import 'package:bb_mobile/import/hardware_page.dart'; import 'package:bb_mobile/import/page.dart'; import 'package:bb_mobile/locator.dart'; import 'package:bb_mobile/receive/receive_page.dart'; -import 'package:bb_mobile/recover/fs_cloud_page.dart'; +import 'package:bb_mobile/recover/manual_page.dart'; import 'package:bb_mobile/recover/keychain_page.dart'; import 'package:bb_mobile/send/bloc/send_cubit.dart'; import 'package:bb_mobile/send/send_page.dart'; @@ -223,10 +223,16 @@ GoRouter setupRouter() => GoRouter( GoRoute( path: '/cloud-backup', builder: (context, state) { - final cloudCubit = state.extra! as CloudCubit; + final data = state.extra! as Map; + final cloudCubit = data['cubit'] as CloudCubit; + Function(String, String)? onBackupSelected; + if (data['callback'] != null) { + onBackupSelected = data['callback']! as Function(String, String)?; + } + return BlocProvider.value( value: cloudCubit, - child: const CloudPage(), + child: CloudPage(onBackupSelected: onBackupSelected), ); }, ), From 6a5e35305696a39e465912f623c6b1f0c63628ec Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Tue, 14 Jan 2025 16:54:02 -0500 Subject: [PATCH 052/401] feat(CloudState): exposed selectedBackup --- lib/backup/bloc/cloud_state.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/backup/bloc/cloud_state.dart b/lib/backup/bloc/cloud_state.dart index d1cbd5605..7d76dcbe3 100644 --- a/lib/backup/bloc/cloud_state.dart +++ b/lib/backup/bloc/cloud_state.dart @@ -10,6 +10,7 @@ class CloudState with _$CloudState { @Default(false) bool loading, GoogleDriveStorage? googleDriveStorage, @Default([]) List availableBackups, + @Default(('', '')) (String, String) selectedBackup, @Default('') String toast, @Default('') String error, }) = _CloudState; From b34a962bde885cd022ac901c22c8ddfe136945e7 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Tue, 14 Jan 2025 16:58:36 -0500 Subject: [PATCH 053/401] feat(CloudCubit): exposed readCloudBackup --- lib/backup/bloc/cloud_cubit.dart | 94 ++++++++++++++++++++++++-------- 1 file changed, 72 insertions(+), 22 deletions(-) diff --git a/lib/backup/bloc/cloud_cubit.dart b/lib/backup/bloc/cloud_cubit.dart index 221c87b66..3f1bb3e36 100644 --- a/lib/backup/bloc/cloud_cubit.dart +++ b/lib/backup/bloc/cloud_cubit.dart @@ -1,37 +1,45 @@ -import 'dart:io'; +import 'dart:convert'; +import 'dart:io' as io; import 'package:bb_mobile/_pkg/consts/configs.dart'; import 'package:bb_mobile/_pkg/gdrive.dart'; import 'package:bb_mobile/backup/bloc/cloud_state.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:googleapis/drive/v3.dart'; import 'package:hex/hex.dart'; import 'package:recoverbull_dart/recoverbull_dart.dart'; +//TODO; Move this google drive cubit and share it with recover and backup cubit class CloudCubit extends Cubit { - final String backupPath; - final String backupName; + CloudCubit() : super(const CloudState()); - CloudCubit({ - required this.backupPath, - required this.backupName, - }) : super(const CloudState()); + void clearToast() => emit(state.copyWith(toast: '', loading: false)); - void clearToast() => emit(state.copyWith(toast: '')); - - void clearError() => emit(state.copyWith(error: '')); - Future connectAndStoreBackup() async { + void clearError() => emit(state.copyWith(error: '', loading: false)); + //move to a google drive repository + Future driveConnect() async { try { emit(state.copyWith(loading: true)); final googleSignInAccount = await GoogleDriveApi.google.signIn(); final googleDriveStorage = await GoogleDriveStorage.connect( googleSignInAccount, - defaultCloudBackupPath, + defaultBackupPath, ); if (googleSignInAccount != null) { - emit(state.copyWith(googleDriveStorage: googleDriveStorage)); + emit( + state.copyWith( + googleDriveStorage: googleDriveStorage, + loading: false, + ), + ); } else { - emit(state.copyWith(error: "Google drive user has not authenticated")); + emit( + state.copyWith( + error: "Google drive user has not authenticated", + loading: false, + ), + ); return; } } catch (e) { @@ -44,13 +52,17 @@ class CloudCubit extends Cubit { ); return; } + } + + Future uploadBackup(String backupPath, String backupName) async { + if (state.googleDriveStorage == null) await driveConnect(); + final backup = io.File(backupPath); - final backup = File(backupPath); final content = await backup.readAsString(); + final decoded = HEX.decode(content); final isCreated = await state.googleDriveStorage?.writeMetaData(decoded, backupName); - if (isCreated == false) { emit( state.copyWith( @@ -66,31 +78,69 @@ class CloudCubit extends Cubit { loading: false, ), ); + return; } } Future readAllBackups() async { try { + if (state.googleDriveStorage == null) driveConnect(); + emit(state.copyWith(loading: true)); + final availableBackups = + await state.googleDriveStorage?.readAllMetaDataFiles(); + if (availableBackups != null) { + emit( + state.copyWith( + loading: false, + toast: "Found ${availableBackups.length} backups files", + availableBackups: availableBackups, + ), + ); + } else { + emit( + state.copyWith( + loading: false, + error: "No backup files found", + ), + ); + } + } catch (e) { emit( state.copyWith( - loading: true, + loading: false, + error: "Failed to read all backups: $e", ), ); - final availableBackups = - await state.googleDriveStorage!.readAllMetaDataFiles(); + } + } + Future readCloudBackup(File file) async { + try { + if (state.googleDriveStorage == null) driveConnect(); + emit( + state.copyWith( + loading: true, + ), + ); + final metaData = + await state.googleDriveStorage!.readMetaDataContent(file); + final decodeEncryptedFile = utf8.decode(metaData); + final id = jsonDecode(decodeEncryptedFile)['backupId']?.toString() ?? ''; + if (decodeEncryptedFile.isEmpty || id.isEmpty) { + emit(state.copyWith(error: 'Invalid backup data')); + return; + } emit( state.copyWith( loading: false, - toast: "Found ${availableBackups.length} backups files", - availableBackups: availableBackups, + selectedBackup: (id, decodeEncryptedFile), ), ); } catch (e) { emit( state.copyWith( loading: false, - error: "Failed to read all backups: $e", + error: "Failed to read backup: $e", ), ); } From 6e8df45d54f842442d7a86338a6d13e4066ec59a Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Tue, 14 Jan 2025 16:58:54 -0500 Subject: [PATCH 054/401] code cleanup --- lib/backup/backup_page.dart | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/lib/backup/backup_page.dart b/lib/backup/backup_page.dart index cd0d4b9fd..79eff87cf 100644 --- a/lib/backup/backup_page.dart +++ b/lib/backup/backup_page.dart @@ -128,10 +128,7 @@ class _TheBackupPageState extends State { const Gap(50), if (state.backupPath.isNotEmpty) BlocProvider( - create: (context) => CloudCubit( - backupPath: state.backupPath, - backupName: state.backupName, - ), + create: (context) => CloudCubit(), child: Center( child: BlocConsumer( listener: (context, cloudState) { @@ -163,16 +160,20 @@ class _TheBackupPageState extends State { } } }, + buildWhen: (p, q) => p.loading != q.loading, builder: (context, cloudState) { return BBButton.big( loading: cloudState.loading, onPressed: () { - context - .read() - .connectAndStoreBackup(); + context.read().uploadBackup( + state.backupPath, + state.backupName, + ); context.push( '/cloud-backup', - extra: context.read(), + extra: { + 'cubit': context.read(), + }, ); }, label: "SAVE TO GOOGLE DRIVE", From 4bd5ac87c21e6c870344a9332b2c870430d33142 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Tue, 14 Jan 2025 17:00:25 -0500 Subject: [PATCH 055/401] feat: added recover from google drive --- lib/backup/cloud_page.dart | 50 ++++++++++---- .../{fs_cloud_page.dart => manual_page.dart} | 65 +++++++++++++++---- 2 files changed, 92 insertions(+), 23 deletions(-) rename lib/recover/{fs_cloud_page.dart => manual_page.dart} (66%) diff --git a/lib/backup/cloud_page.dart b/lib/backup/cloud_page.dart index 5bf5b4172..e3a2bc674 100644 --- a/lib/backup/cloud_page.dart +++ b/lib/backup/cloud_page.dart @@ -1,8 +1,3 @@ -import 'package:bb_mobile/_ui/app_bar.dart'; -import 'package:bb_mobile/_ui/components/button.dart'; -import 'package:bb_mobile/_ui/components/text.dart'; -import 'package:bb_mobile/backup/bloc/cloud_cubit.dart'; -import 'package:bb_mobile/backup/bloc/cloud_state.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:gap/gap.dart'; @@ -10,18 +5,51 @@ import 'package:go_router/go_router.dart'; import 'package:googleapis/drive/v3.dart'; import 'package:intl/intl.dart'; -class CloudPage extends StatelessWidget { - const CloudPage({super.key}); +import 'package:bb_mobile/_ui/app_bar.dart'; +import 'package:bb_mobile/_ui/components/button.dart'; +import 'package:bb_mobile/_ui/components/text.dart'; +import 'package:bb_mobile/backup/bloc/cloud_cubit.dart'; +import 'package:bb_mobile/backup/bloc/cloud_state.dart'; + +class CloudPage extends StatefulWidget { + final Function(String, String)? onBackupSelected; + const CloudPage({ + Key? key, + this.onBackupSelected, + }); + + @override + State createState() => _CloudPageState(); +} + +class _CloudPageState extends State { + @override + void initState() { + final cubit = context.read(); + cubit.readAllBackups(); + super.initState(); + } @override Widget build(BuildContext context) { return BlocConsumer( listener: (context, state) { - if (state.toast.isNotEmpty) { + if (widget.onBackupSelected != null) { + if (state.selectedBackup.$1.isNotEmpty && + state.selectedBackup.$2.isNotEmpty) { + widget.onBackupSelected!( + state.selectedBackup.$1, + state.selectedBackup.$2, + ); + context.pop(); + } + } + + if (state.toast.isNotEmpty && state.toast != '') { _showSnackBar(context, state.toast, Colors.green); context.read().clearToast(); } - if (state.error.isNotEmpty) { + if (state.error.isNotEmpty && state.error != '') { _showSnackBar(context, state.error, Colors.red); context.read().clearError(); } @@ -45,7 +73,7 @@ class CloudPage extends StatelessWidget { const Gap(50), AvailableBackups( onFileSelected: (file) { - debugPrint('Selected file: ${file.name}'); + cubit.readCloudBackup(file); }, ), const Gap(10), @@ -80,7 +108,7 @@ class AvailableBackups extends StatelessWidget { Widget build(BuildContext context) { final cubit = context.read(); return SizedBox( - height: 500, + height: 700, child: BlocBuilder( builder: (context, state) { if (state.availableBackups.isEmpty) { diff --git a/lib/recover/fs_cloud_page.dart b/lib/recover/manual_page.dart similarity index 66% rename from lib/recover/fs_cloud_page.dart rename to lib/recover/manual_page.dart index 5e0c7c0aa..10eaf1a16 100644 --- a/lib/recover/fs_cloud_page.dart +++ b/lib/recover/manual_page.dart @@ -8,13 +8,17 @@ import 'package:bb_mobile/_pkg/wallet/repository/sensitive_storage.dart'; import 'package:bb_mobile/_pkg/wallet/repository/storage.dart'; import 'package:bb_mobile/_ui/app_bar.dart'; import 'package:bb_mobile/_ui/components/button.dart'; +import 'package:bb_mobile/backup/bloc/cloud_cubit.dart'; +import 'package:bb_mobile/backup/cloud_page.dart'; import 'package:bb_mobile/home/bloc/home_cubit.dart'; import 'package:bb_mobile/locator.dart'; -import 'package:bb_mobile/recover/bloc/fs_cloud_cubit.dart'; -import 'package:bb_mobile/recover/bloc/fs_cloud_state.dart'; +import 'package:bb_mobile/recover/bloc/manual_cubit.dart'; +import 'package:bb_mobile/recover/bloc/manual_state.dart'; import 'package:bb_mobile/wallet/bloc/wallet_bloc.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:gap/gap.dart'; import 'package:go_router/go_router.dart'; class ManualRecoverPage extends StatelessWidget { @@ -24,8 +28,8 @@ class ManualRecoverPage extends StatelessWidget { @override Widget build(BuildContext context) { - return BlocProvider( - create: (_) => FsCloudCubit( + return BlocProvider( + create: (_) => ManualCubit( filePicker: locator(), walletCreate: locator(), walletSensitiveCreate: locator(), @@ -46,7 +50,7 @@ class ManualRecoverPage extends StatelessWidget { buttonKey: UIKeys.settingsBackButton, ), ), - body: BlocListener( + body: BlocListener( listener: (context, state) async { if (state.error.isNotEmpty) { ScaffoldMessenger.of(context).showSnackBar( @@ -55,7 +59,7 @@ class ManualRecoverPage extends StatelessWidget { backgroundColor: Colors.red, ), ); - context.read().clearError(); + context.read().clearError(); } if (state.recovered) { ScaffoldMessenger.of(context).showSnackBar( @@ -68,9 +72,9 @@ class ManualRecoverPage extends StatelessWidget { context.go('/home'); } }, - child: BlocBuilder( + child: BlocBuilder( builder: (context, state) { - final cubit = context.read(); + final cubit = context.read(); return Padding( padding: const EdgeInsets.all(20.0), @@ -95,10 +99,29 @@ class ManualRecoverPage extends StatelessWidget { onChanged: (value) => cubit.updateBackupKey(value), ), if (!state.recovered && state.backupKey.isEmpty) - BBButton.big( - label: 'Select File', - center: true, - onPressed: () => cubit.selectFile(), + Column( + children: [ + BBButton.big( + label: 'Select file from FileSystem', + center: true, + onPressed: () => cubit.selectFileFromFs(), + ), + const Gap(20), + BBButton.big( + label: 'Select file from Cloud', + center: true, + onPressed: () => { + context.push( + '/cloud-backup', + extra: { + 'cubit': CloudCubit(), + 'callback': (String id, String encrypted) => + cubit.setSelectedBack(id, encrypted), + }, + ), + }, + ), + ], ), if (!state.recovered && state.backupKey.isNotEmpty) BBButton.big( @@ -115,4 +138,22 @@ class ManualRecoverPage extends StatelessWidget { ), ); } + + void showModalPopup({ + required BuildContext context, + required List children, + }) { + showCupertinoModalPopup( + context: context, + builder: (BuildContext modalContext) => Container( + height: 700, + decoration: const BoxDecoration( + color: Colors.white, + ), + child: Column( + children: children, + ), + ), + ); + } } From 10bedcc0a781bfafb930bc1fd5a1e1b48eb2feb7 Mon Sep 17 00:00:00 2001 From: ethicnology Date: Wed, 15 Jan 2025 09:01:50 -0500 Subject: [PATCH 056/401] fix: post-rebase conflicts --- ios/Podfile.lock | 15 +++++---- lib/backup/backup_page.dart | 18 +++++------ lib/backup/bloc/backup_cubit.dart | 34 ++++++++++----------- lib/backup/cloud_page.dart | 33 +++++++++----------- lib/backup/keychain_page.dart | 5 +-- lib/recover/bloc/keychain_cubit.dart | 4 +-- lib/recover/bloc/manual_cubit.dart | 32 ++++++++----------- lib/recover/manual_page.dart | 11 +++---- lib/routes.dart | 7 +++-- lib/settings/core_wallet_settings_page.dart | 7 +++-- lib/settings/settings_page.dart | 18 ++++++----- linux/flutter/generated_plugins.cmake | 1 - pubspec.lock | 20 ++++++------ pubspec.yaml | 2 +- 14 files changed, 97 insertions(+), 110 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index ad99ba3e0..b2ec154d8 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -177,11 +177,10 @@ SPEC CHECKSUMS: bdk_flutter: fb57a7400a7f3f181c5977bcdc2a5ef347ae4e7f bip85: f656a7e6b23afda4960efb11c87d51d68e8be3db boltz: 6388ec2412f3753b63a9e65c97f87ea26f43bddc - AppAuth: d4f13a8fe0baf391b2108511793e4b479691fb73 DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 - document_file_save_plus: 913d440d8b611ae19add4522ed578e3ed1483a2f - file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655 + document_file_save_plus: 47b52a647efb29f7d431af45a856ce1a841f32fc + file_picker: 9b3292d7c8bc68c8a7bf8eb78f730e49c8efc517 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 flutter_barcode_scanner: 7a1144744c28dc0c57a8de7218ffe5ec59a9e4bf flutter_file_dialog: 4c014a45b105709a27391e266c277d7e588e9299 @@ -190,15 +189,15 @@ SPEC CHECKSUMS: flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12 integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573 lwk: 22e06bc5664247d6b2dac91cfe209b63b70dd580 - no_screenshot: 67d110f12466f4913b488803d4e498d03ef2889e - path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 + no_screenshot: 6d183496405a3ab709a67a54e5cd0f639e94729e + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 payjoin_flutter: 6397d7b698cdad6453be4949ab6aca1863f6c5e5 - permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 + permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d ScreenProtectorKit: 83a6281b02c7a5902ee6eac4f5045f674e902ae4 SDWebImage: 8a6b7b160b4d710e2a22b6900e25301075c34cb3 - share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad + share_plus: 011d6fb4f9d2576b83179a3a5c5e323202cdabcf SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 - url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe + url_launcher_ios: 694010445543906933d732453a59da0a173ae33d PODFILE CHECKSUM: 819463e6a0290f5a72f145ba7cde16e8b6ef0796 diff --git a/lib/backup/backup_page.dart b/lib/backup/backup_page.dart index 79eff87cf..4de8e7936 100644 --- a/lib/backup/backup_page.dart +++ b/lib/backup/backup_page.dart @@ -1,5 +1,6 @@ +import 'package:bb_mobile/_model/wallet.dart'; import 'package:bb_mobile/_pkg/file_storage.dart'; -import 'package:bb_mobile/_pkg/wallet/repository/sensitive_storage.dart'; +import 'package:bb_mobile/_repository/wallet/sensitive_wallet_storage.dart'; import 'package:bb_mobile/_ui/app_bar.dart'; import 'package:bb_mobile/_ui/components/button.dart'; import 'package:bb_mobile/_ui/components/controls.dart'; @@ -9,7 +10,6 @@ import 'package:bb_mobile/backup/bloc/backup_state.dart'; import 'package:bb_mobile/backup/bloc/cloud_cubit.dart'; import 'package:bb_mobile/backup/bloc/cloud_state.dart'; import 'package:bb_mobile/locator.dart'; -import 'package:bb_mobile/wallet/bloc/wallet_bloc.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:gap/gap.dart'; @@ -18,7 +18,7 @@ import 'package:go_router/go_router.dart'; class ManualBackupPage extends StatefulWidget { const ManualBackupPage({super.key, required this.wallets}); - final List wallets; + final List wallets; @override _TheBackupPageState createState() => _TheBackupPageState(); @@ -81,7 +81,7 @@ class _TheBackupPageState extends State { .toggleAllMnemonicAndPassphrase(); }, ), - Gap(8), + const Gap(8), BackupToggleItem( title: 'Descriptors', value: @@ -90,7 +90,7 @@ class _TheBackupPageState extends State { context.read().toggleDescriptors(); }, ), - Gap(8), + const Gap(8), BackupToggleItem( title: 'Labels', value: state.confirmedBackups['labels'] ?? false, @@ -98,15 +98,15 @@ class _TheBackupPageState extends State { context.read().toggleLabels(); }, ), - Gap(8), + const Gap(8), if (state.backupKey.isEmpty) Center(child: _GenerateBackupButton()), - Gap(20), + const Gap(20), if (state.backupKey.isNotEmpty) Column( children: [ const BBText.bodyBold("Generated Backup Key"), - Gap(10), + const Gap(10), SelectableText( state.backupKey, textAlign: TextAlign.center, @@ -114,7 +114,7 @@ class _TheBackupPageState extends State { fontWeight: FontWeight.normal, ), ), - Gap(20), + const Gap(20), if (state.backupId.isNotEmpty) BBButton.big( onPressed: () => context.push( diff --git a/lib/backup/bloc/backup_cubit.dart b/lib/backup/bloc/backup_cubit.dart index ef55dfe21..dde0be18d 100644 --- a/lib/backup/bloc/backup_cubit.dart +++ b/lib/backup/bloc/backup_cubit.dart @@ -2,12 +2,12 @@ import 'dart:convert'; import 'dart:io'; import 'package:bb_mobile/_model/backup.dart'; +import 'package:bb_mobile/_model/wallet.dart'; import 'package:bb_mobile/_pkg/crypto.dart'; import 'package:bb_mobile/_pkg/file_storage.dart'; import 'package:bb_mobile/_pkg/wallet/labels.dart'; -import 'package:bb_mobile/_pkg/wallet/repository/sensitive_storage.dart'; +import 'package:bb_mobile/_repository/wallet/sensitive_wallet_storage.dart'; import 'package:bb_mobile/backup/bloc/backup_state.dart'; -import 'package:bb_mobile/wallet/bloc/wallet_bloc.dart'; import 'package:bdk_flutter/bdk_flutter.dart' as bdk; import 'package:bip85/bip85.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -22,7 +22,7 @@ class BackupCubit extends Cubit { }) : super(const BackupState()); final FileStorage fileStorage; - final List wallets; + final List wallets; final WalletSensitiveStorageRepository walletSensitiveStorage; Future loadBackupData() async { @@ -30,19 +30,15 @@ class BackupCubit extends Cubit { final backups = []; final confirmedBackups = state.confirmedBackups; - for (final walletBloc in wallets) { - final wallet = walletBloc.state.wallet; - if (wallet == null) { - emit(state.copyWith(error: 'Wallet data is missing.')); - return; - } + for (final wallet in wallets) { var backup = Backup( - name: wallet.name ?? '', - network: wallet.network.name.toLowerCase(), - layer: wallet.baseWalletType.name.toLowerCase(), - script: wallet.scriptType.name.toLowerCase(), - type: wallet.type.name.toLowerCase(), - mnemonicFingerPrint: wallet.mnemonicFingerprint); + name: wallet.name ?? '', + network: wallet.network.name.toLowerCase(), + layer: wallet.baseWalletType.name.toLowerCase(), + script: wallet.scriptType.name.toLowerCase(), + type: wallet.type.name.toLowerCase(), + mnemonicFingerPrint: wallet.mnemonicFingerprint, + ); final seedStorageString = wallet.getRelatedSeedStorageString(); @@ -172,7 +168,9 @@ class BackupCubit extends Cubit { final file = File(backupDir.path + filename); final (f, errSave) = await fileStorage.saveToFile( - file, HEX.encode(utf8.encode(encrypted))); + file, + HEX.encode(utf8.encode(encrypted)), + ); if (errSave != null) { emit(state.copyWith(error: 'Failed to save backup file.')); return; @@ -196,7 +194,9 @@ class BackupCubit extends Cubit { } Future _createBackupKey( - List? mnemonicWords, bdk.Network network) async { + List? mnemonicWords, + bdk.Network network, +) async { late final bdk.Mnemonic bdkMnemonic; if (mnemonicWords != null && mnemonicWords.isNotEmpty) { diff --git a/lib/backup/cloud_page.dart b/lib/backup/cloud_page.dart index e3a2bc674..d5a171d1e 100644 --- a/lib/backup/cloud_page.dart +++ b/lib/backup/cloud_page.dart @@ -1,3 +1,8 @@ +import 'package:bb_mobile/_ui/app_bar.dart'; +import 'package:bb_mobile/_ui/components/button.dart'; +import 'package:bb_mobile/_ui/components/text.dart'; +import 'package:bb_mobile/backup/bloc/cloud_cubit.dart'; +import 'package:bb_mobile/backup/bloc/cloud_state.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:gap/gap.dart'; @@ -5,18 +10,9 @@ import 'package:go_router/go_router.dart'; import 'package:googleapis/drive/v3.dart'; import 'package:intl/intl.dart'; -import 'package:bb_mobile/_ui/app_bar.dart'; -import 'package:bb_mobile/_ui/components/button.dart'; -import 'package:bb_mobile/_ui/components/text.dart'; -import 'package:bb_mobile/backup/bloc/cloud_cubit.dart'; -import 'package:bb_mobile/backup/bloc/cloud_state.dart'; - class CloudPage extends StatefulWidget { final Function(String, String)? onBackupSelected; - const CloudPage({ - Key? key, - this.onBackupSelected, - }); + const CloudPage({this.onBackupSelected}); @override State createState() => _CloudPageState(); @@ -166,13 +162,14 @@ class BackupTile extends StatelessWidget { final formattedDate = DateFormat('yyyy-MM-dd HH:mm').format(dateTime); return ListTile( - onTap: () => onFileSelected(file), - title: BBText.body( - backupId ?? 'Unnamed File', - isBold: true, - ), - subtitle: BBText.bodySmall( - formattedDate, - )); + onTap: () => onFileSelected(file), + title: BBText.body( + backupId ?? 'Unnamed File', + isBold: true, + ), + subtitle: BBText.bodySmall( + formattedDate, + ), + ); } } diff --git a/lib/backup/keychain_page.dart b/lib/backup/keychain_page.dart index 3ae0cd00f..95a76ed1b 100644 --- a/lib/backup/keychain_page.dart +++ b/lib/backup/keychain_page.dart @@ -1,8 +1,6 @@ import 'package:bb_mobile/_ui/app_bar.dart'; import 'package:bb_mobile/backup/bloc/keychain_cubit.dart'; import 'package:bb_mobile/backup/bloc/keychain_state.dart'; -import 'package:bb_mobile/home/bloc/home_cubit.dart'; -import 'package:bb_mobile/locator.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:gap/gap.dart'; @@ -50,7 +48,6 @@ class KeychainBackupPage extends StatelessWidget { backgroundColor: Colors.green, ), ); - locator().getWalletsFromStorage(); context.go('/home'); } }, @@ -66,7 +63,7 @@ class KeychainBackupPage extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ SelectableText('Backup Key: $backupKey'), - Gap(8), + const Gap(8), SelectableText('Backup ID: $backupId'), ], ), diff --git a/lib/recover/bloc/keychain_cubit.dart b/lib/recover/bloc/keychain_cubit.dart index 5e2c07cb7..0cd2b62f4 100644 --- a/lib/recover/bloc/keychain_cubit.dart +++ b/lib/recover/bloc/keychain_cubit.dart @@ -13,7 +13,7 @@ class KeychainCubit extends Cubit { void clearError() => emit(state.copyWith(error: '')); void updateSecret(String value) => emit(state.copyWith(secret: value)); - void clickRecoverKey() async { + Future clickRecoverKey() async { if (state.backupId.isEmpty) { emit(state.copyWith(error: 'backup id is missing')); return; @@ -27,7 +27,7 @@ class KeychainCubit extends Cubit { _recoverBackupKey(state.secret, state.backupId); } - void _recoverBackupKey(String secret, String backupId) async { + Future _recoverBackupKey(String secret, String backupId) async { try { if (keychainapi.isEmpty) { emit(state.copyWith(error: 'keychain api is not set')); diff --git a/lib/recover/bloc/manual_cubit.dart b/lib/recover/bloc/manual_cubit.dart index 1c6fb771e..dd65d7dad 100644 --- a/lib/recover/bloc/manual_cubit.dart +++ b/lib/recover/bloc/manual_cubit.dart @@ -2,20 +2,15 @@ import 'dart:convert'; import 'package:bb_mobile/_model/backup.dart'; import 'package:bb_mobile/_model/wallet.dart'; -import 'package:bb_mobile/_pkg/consts/configs.dart'; import 'package:bb_mobile/_pkg/file_picker.dart'; -import 'package:bb_mobile/_pkg/gdrive.dart'; import 'package:bb_mobile/_pkg/wallet/bdk/sensitive_create.dart'; import 'package:bb_mobile/_pkg/wallet/create.dart'; import 'package:bb_mobile/_pkg/wallet/create_sensitive.dart'; import 'package:bb_mobile/_pkg/wallet/lwk/sensitive_create.dart'; -import 'package:bb_mobile/_pkg/wallet/repository/sensitive_storage.dart'; -import 'package:bb_mobile/_pkg/wallet/repository/storage.dart'; +import 'package:bb_mobile/_repository/wallet/sensitive_wallet_storage.dart'; +import 'package:bb_mobile/_repository/wallet/wallet_storage.dart'; import 'package:bb_mobile/recover/bloc/manual_state.dart'; -import 'package:bb_mobile/wallet/bloc/wallet_bloc.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:googleapis/drive/v3.dart'; import 'package:hex/hex.dart'; import 'package:recoverbull_dart/recoverbull_dart.dart'; @@ -32,7 +27,7 @@ class ManualCubit extends Cubit { }) : super(const ManualState()); FilePick? filePicker; - final List wallets; + final List wallets; final WalletSensitiveStorageRepository walletSensitiveStorage; final WalletsStorageRepository walletsStorageRepository; final WalletSensitiveCreate walletSensitiveCreate; @@ -137,29 +132,28 @@ class ManualCubit extends Cubit { } else { //find the mnemonic & passphrase associated with the fingerprint. - for (final walletBloc in wallets) { - if (walletBloc.state.wallet!.mnemonicFingerprint == - backup.mnemonicFingerPrint) { - final seedStorageString = - walletBloc.state.wallet!.getRelatedSeedStorageString(); + for (final wallet in wallets) { + if (wallet.mnemonicFingerprint == backup.mnemonicFingerPrint) { + final seedStorageString = wallet.getRelatedSeedStorageString(); final (seed, error) = await walletSensitiveStorage.readSeed( fingerprintIndex: seedStorageString, ); if (error != null) { - emit(state.copyWith( - error: 'Error reading seed: ${error.message}')); + emit( + state.copyWith( + error: 'Error reading seed: ${error.message}', + ), + ); return false; } if (seed == null) { emit(state.copyWith(error: 'Seed data is missing.')); return false; } - final passphrase = walletBloc.state.wallet!.hasPassphrase() + final passphrase = wallet.hasPassphrase() ? seed.passphrases .firstWhere( - (e) => - e.sourceFingerprint == - walletBloc.state.wallet!.sourceFingerprint, + (e) => e.sourceFingerprint == wallet.sourceFingerprint, ) .passphrase : ''; diff --git a/lib/recover/manual_page.dart b/lib/recover/manual_page.dart index 10eaf1a16..677d7a9c2 100644 --- a/lib/recover/manual_page.dart +++ b/lib/recover/manual_page.dart @@ -1,20 +1,18 @@ +import 'package:bb_mobile/_model/wallet.dart'; import 'package:bb_mobile/_pkg/consts/keys.dart'; import 'package:bb_mobile/_pkg/file_picker.dart'; import 'package:bb_mobile/_pkg/wallet/bdk/sensitive_create.dart'; import 'package:bb_mobile/_pkg/wallet/create.dart'; import 'package:bb_mobile/_pkg/wallet/create_sensitive.dart'; import 'package:bb_mobile/_pkg/wallet/lwk/sensitive_create.dart'; -import 'package:bb_mobile/_pkg/wallet/repository/sensitive_storage.dart'; -import 'package:bb_mobile/_pkg/wallet/repository/storage.dart'; +import 'package:bb_mobile/_repository/wallet/sensitive_wallet_storage.dart'; +import 'package:bb_mobile/_repository/wallet/wallet_storage.dart'; import 'package:bb_mobile/_ui/app_bar.dart'; import 'package:bb_mobile/_ui/components/button.dart'; import 'package:bb_mobile/backup/bloc/cloud_cubit.dart'; -import 'package:bb_mobile/backup/cloud_page.dart'; -import 'package:bb_mobile/home/bloc/home_cubit.dart'; import 'package:bb_mobile/locator.dart'; import 'package:bb_mobile/recover/bloc/manual_cubit.dart'; import 'package:bb_mobile/recover/bloc/manual_state.dart'; -import 'package:bb_mobile/wallet/bloc/wallet_bloc.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -24,7 +22,7 @@ import 'package:go_router/go_router.dart'; class ManualRecoverPage extends StatelessWidget { const ManualRecoverPage({super.key, required this.wallets}); - final List wallets; + final List wallets; @override Widget build(BuildContext context) { @@ -68,7 +66,6 @@ class ManualRecoverPage extends StatelessWidget { backgroundColor: Colors.green, ), ); - await locator().getWalletsFromStorage(); context.go('/home'); } }, diff --git a/lib/routes.dart b/lib/routes.dart index c77d16c08..93ede3df0 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:bb_mobile/_model/swap.dart'; import 'package:bb_mobile/_model/transaction.dart'; +import 'package:bb_mobile/_model/wallet.dart'; import 'package:bb_mobile/_ui/logger_page.dart'; import 'package:bb_mobile/auth/page.dart'; import 'package:bb_mobile/backup/backup_page.dart'; @@ -16,8 +17,8 @@ import 'package:bb_mobile/import/hardware_page.dart'; import 'package:bb_mobile/import/page.dart'; import 'package:bb_mobile/locator.dart'; import 'package:bb_mobile/receive/receive_page.dart'; -import 'package:bb_mobile/recover/manual_page.dart'; import 'package:bb_mobile/recover/keychain_page.dart'; +import 'package:bb_mobile/recover/manual_page.dart'; import 'package:bb_mobile/send/bloc/send_cubit.dart'; import 'package:bb_mobile/send/send_page.dart'; import 'package:bb_mobile/settings/application_settings_page.dart'; @@ -202,14 +203,14 @@ GoRouter setupRouter() => GoRouter( GoRoute( path: '/backupbull', builder: (context, state) { - final wallets = state.extra! as List; + final wallets = state.extra! as List; return ManualBackupPage(wallets: wallets); }, ), GoRoute( path: '/recoverbull', builder: (context, state) { - final wallets = state.extra! as List; + final wallets = state.extra! as List; return ManualRecoverPage(wallets: wallets); }, ), diff --git a/lib/settings/core_wallet_settings_page.dart b/lib/settings/core_wallet_settings_page.dart index 8539636b8..28ae555bb 100644 --- a/lib/settings/core_wallet_settings_page.dart +++ b/lib/settings/core_wallet_settings_page.dart @@ -119,9 +119,10 @@ class BackupBullButton extends StatelessWidget { return BBButton.textWithStatusAndRightArrow( label: 'RecoverBull', onPressed: () { - final network = context.read().state.getBBNetwork(); - final wallets = - context.read().state.walletBlocsFromNetwork(network); + final network = context.read().getBBNetwork; + final wallets = context + .read() + .walletNotMainFromNetwork(network); context.push('/recoverbull', extra: wallets); }, ); diff --git a/lib/settings/settings_page.dart b/lib/settings/settings_page.dart index 0ea1206e8..5fe6802e8 100644 --- a/lib/settings/settings_page.dart +++ b/lib/settings/settings_page.dart @@ -1,11 +1,11 @@ import 'package:bb_mobile/_pkg/consts/keys.dart'; import 'package:bb_mobile/_pkg/launcher.dart'; +import 'package:bb_mobile/_repository/app_wallets_repository.dart'; +import 'package:bb_mobile/_repository/network_repository.dart'; import 'package:bb_mobile/_ui/app_bar.dart'; import 'package:bb_mobile/_ui/components/button.dart'; import 'package:bb_mobile/_ui/components/text.dart'; -import 'package:bb_mobile/home/bloc/home_cubit.dart'; import 'package:bb_mobile/locator.dart'; -import 'package:bb_mobile/network/bloc/network_cubit.dart'; import 'package:bb_mobile/settings/bloc/settings_cubit.dart'; import 'package:bb_mobile/styles.dart'; import 'package:flutter/material.dart'; @@ -171,9 +171,10 @@ class BackupBullButton extends StatelessWidget { return BBButton.textWithStatusAndRightArrow( label: 'BackupBull', onPressed: () { - final network = context.read().state.getBBNetwork(); - final wallets = - context.read().state.walletBlocsFromNetwork(network); + final network = context.read().getBBNetwork; + final wallets = context + .read() + .walletNotMainFromNetwork(network); context.push('/backupbull', extra: wallets); }, ); @@ -188,9 +189,10 @@ class RecoverBullButton extends StatelessWidget { return BBButton.textWithStatusAndRightArrow( label: 'RecoverBull', onPressed: () { - final network = context.read().state.getBBNetwork(); - final wallets = - context.read().state.walletBlocsFromNetwork(network); + final network = context.read().getBBNetwork; + final wallets = context + .read() + .walletNotMainFromNetwork(network); context.push('/recoverbull', extra: wallets); }, ); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 008e559d3..407c9d317 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -10,7 +10,6 @@ list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST bip85 lwk - nostr_sdk ) set(PLUGIN_BUNDLED_LIBRARIES) diff --git a/pubspec.lock b/pubspec.lock index 8a8ed10d6..a977fbe91 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -83,7 +83,7 @@ packages: description: path: "bindings/dart-bip85" ref: master - resolved-ref: "2321b17f3e1c74f15bdb95638bb8c65bbbe2a2af" + resolved-ref: e1bea65d63c8b9a97d1fd77034ca11304c08ae96 url: "https://github.com/ethicnology/rust-bip85.git" source: git version: "1.0.2" @@ -1327,23 +1327,23 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.0" - rxdart: - dependency: "direct main" - description: - name: rxdart - sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" - url: "https://pub.dev" - source: hosted - version: "0.28.0" recoverbull_dart: dependency: "direct main" description: path: "." ref: HEAD - resolved-ref: d3b856d0cafea2b96c784eb229466a8b057c07f6 + resolved-ref: "828a84ff723849d2f58eb607fcf1171348d0de6b" url: "https://github.com/StaxoLotl/recoverbull-dart.git" source: git version: "0.0.1" + rxdart: + dependency: "direct main" + description: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + url: "https://pub.dev" + source: hosted + version: "0.28.0" share_plus: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 87fadca99..08401fba9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -99,7 +99,7 @@ dependencies: url: https://github.com/ethicnology/rust-bip85.git path: bindings/dart-bip85 ref: master - web_socket_channel: ^2.4.5 + web_socket_channel: ^3.0.1 flutter_speed_dial: ^7.0.0 googleapis: ^13.2.0 google_sign_in: ^6.2.2 From 9f210d790c5cd09a754014263a99e12f4c8c1e19 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Wed, 15 Jan 2025 12:24:42 -0500 Subject: [PATCH 057/401] fix: update wallet retrieval method in settings --- lib/settings/settings_page.dart | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/settings/settings_page.dart b/lib/settings/settings_page.dart index 5fe6802e8..1a542e9f4 100644 --- a/lib/settings/settings_page.dart +++ b/lib/settings/settings_page.dart @@ -172,9 +172,8 @@ class BackupBullButton extends StatelessWidget { label: 'BackupBull', onPressed: () { final network = context.read().getBBNetwork; - final wallets = context - .read() - .walletNotMainFromNetwork(network); + final wallets = + context.read().walletsFromNetwork(network); context.push('/backupbull', extra: wallets); }, ); From b6d7b2db22a196901c6640920205c0532760f052 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Wed, 15 Jan 2025 17:52:25 -0500 Subject: [PATCH 058/401] feat(GoogleDriveApi): enhance backup functionality with error handling and folder management --- lib/_pkg/gdrive.dart | 198 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 161 insertions(+), 37 deletions(-) diff --git a/lib/_pkg/gdrive.dart b/lib/_pkg/gdrive.dart index 827659714..8eb5982d8 100644 --- a/lib/_pkg/gdrive.dart +++ b/lib/_pkg/gdrive.dart @@ -1,4 +1,8 @@ +import 'dart:async'; +import 'package:bb_mobile/_pkg/consts/configs.dart'; +import 'package:bb_mobile/_pkg/error.dart'; import 'package:extension_google_sign_in_as_googleapis_auth/extension_google_sign_in_as_googleapis_auth.dart'; +import 'package:flutter/rendering.dart'; import 'package:google_sign_in/google_sign_in.dart'; import 'package:googleapis/drive/v3.dart'; @@ -7,51 +11,171 @@ class GoogleDriveApi { final DriveApi api; final GoogleSignInAccount account; + String? _backupFolderId; GoogleDriveApi._(this.api, this.account); - static Future connect() async { - final account = await google.signIn(); - if (account == null) { - print("User not signed in"); - return null; + static Future<(GoogleDriveApi?, Err?)> connect() async { + try { + final account = await google.signIn(); + if (account == null) { + return ( + null, + Err('Google Sign-In was cancelled or failed. Please try again.') + ); + } + + final client = await google.authenticatedClient(); + if (client == null) { + return ( + null, + Err('Failed to authenticate with Google. Please check your internet connection and try again.') + ); + } + + final api = DriveApi(client); + final service = GoogleDriveApi._(api, account); + final (success, err) = await service._setupBackupFolder(); + if (err != null) { + return (null, err); + } + return (service, null); + } catch (e) { + return (null, Err('An unexpected error occurred: $e')); } + } + + static Future disconnect() async => google.disconnect(); + + Future<(bool, Err?)> _setupBackupFolder() async { + try { + const folderName = '.$defaultBackupPath'; + final existing = await api.files.list( + q: "name = '$folderName' and mimeType = 'application/vnd.google-apps.folder' and trashed = false", + spaces: 'drive', + $fields: 'files(id)', + ); + if (existing.files?.isNotEmpty == true) { + _backupFolderId = existing.files!.first.id; + return (true, null); + } + + final folderMetadata = File() + ..name = folderName + ..mimeType = 'application/vnd.google-apps.folder' + ..appProperties = { + 'created': DateTime.now().toIso8601String(), + }; + final folder = await api.files.create(folderMetadata); + _backupFolderId = folder.id; + + final (success, err) = await _applyFolderPermissions(folder.id!); + if (err != null) { + return (false, err); + } - final client = await google.authenticatedClient(); - if (client == null) { - print("Client is null"); - return null; + debugPrint('Initialized secure backup folder: $folderName'); + return (true, null); + } catch (e) { + return (false, Err('Failed to initialize backup folder: $e')); } + } - final api = DriveApi(client); - return GoogleDriveApi._(api, account); + Future<(bool, Err?)> _applyFolderPermissions(String folderId) async { + try { + await api.permissions.create( + Permission() + ..role = 'owner' + ..type = 'user' + ..emailAddress = account.email, + folderId, + transferOwnership: true, + ); + return (true, null); + } catch (e) { + return (false, Err('Failed to set folder permissions: $e')); + } } - static Future disconnect() async => google.disconnect(); + Future<(File?, Err?)> _uploadBackupFile( + String fileName, + List data, + ) async { + try { + final file = File() + ..name = fileName + ..parents = [_backupFolderId!] + ..appProperties = { + 'timestamp': DateTime.now().toIso8601String(), + }; + final createdFile = await api.files.create( + file, + uploadMedia: Media(Stream.value(data), data.length), + ); + return (createdFile, null); + } catch (e) { + return (null, Err('Failed to create backup: $e')); + } + } - // Future write({required String filename, required Map content}) async { - // try { - // // Create an empty file in the appDataFolder - // final file = File()..name = filename; - // final createdFile = await api.files.create(file); - // - // if (createdFile.id == null) { - // print("Failed to create file."); - // return false; - // } - // - // // Update the file with content - // final media = Media( - // Stream.value(utf8.encode(jsonEncode(content))), - // utf8.encode(jsonEncode(content)).length, - // ); - // await api.files.update(file, createdFile.id!, uploadMedia: media); - // - // print("File created"); - // return true; - // } catch (e) { - // print("Error: $e"); - // return false; - // } - // } + Future<(bool, Err?)> saveBackup(List data, String fileName) async { + final (file, err) = await _uploadBackupFile(fileName, data); + if (err != null) { + return (false, err); + } + debugPrint('Successfully saved backup: $fileName'); + return (true, null); + } + + Future<(List?, Err?)> loadBackupContent(File file) async { + try { + final media = await api.files.get( + file.id!, + downloadOptions: DownloadOptions.fullMedia, + ) as Media; + + final data = await _fetchMediaStream(media); + return (data, null); + } catch (e) { + return (null, Err('Error reading backup content: $e')); + } + } + + Future> _fetchMediaStream(Media media) async { + final completer = Completer>(); + final bytes = []; + + media.stream.listen( + bytes.addAll, + onError: (error) => completer + .completeError(Exception('Error streaming backup data: $error')), + onDone: () => completer.complete(bytes), + cancelOnError: true, + ); + + return completer.future; + } + + Future<(List, Err?)> listAllBackupFiles() async { + if (_backupFolderId == null) { + return ([], Err('Backup folder not initialized')); + } + try { + final response = await api.files.list( + q: "'$_backupFolderId' in parents and trashed = false", + spaces: 'drive', + $fields: 'files(id, name, createdTime, modifiedTime, appProperties)', + orderBy: 'modifiedTime desc', + ); + + if (response.files == null || response.files!.isEmpty) { + debugPrint('No metadata files found'); + return ([], null); + } + + return (response.files ?? [], null); + } catch (e) { + return ([], Err('Failed to list backups: $e')); + } + } } From 6fd0557d8b7d3e0bb953f0e0bacb157b5bff78d2 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Wed, 15 Jan 2025 17:52:43 -0500 Subject: [PATCH 059/401] refactor: replace GoogleDriveStorage with GoogleDriveApi in state management --- lib/backup/bloc/cloud_state.dart | 4 ++-- lib/recover/bloc/manual_state.dart | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/backup/bloc/cloud_state.dart b/lib/backup/bloc/cloud_state.dart index 7d76dcbe3..c1192ffa7 100644 --- a/lib/backup/bloc/cloud_state.dart +++ b/lib/backup/bloc/cloud_state.dart @@ -1,6 +1,6 @@ +import 'package:bb_mobile/_pkg/gdrive.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:googleapis/drive/v3.dart'; -import 'package:recoverbull_dart/recoverbull_dart.dart'; part 'cloud_state.freezed.dart'; @@ -8,7 +8,7 @@ part 'cloud_state.freezed.dart'; class CloudState with _$CloudState { const factory CloudState({ @Default(false) bool loading, - GoogleDriveStorage? googleDriveStorage, + GoogleDriveApi? googleDriveApi, @Default([]) List availableBackups, @Default(('', '')) (String, String) selectedBackup, @Default('') String toast, diff --git a/lib/recover/bloc/manual_state.dart b/lib/recover/bloc/manual_state.dart index 3ed05dc7e..171f92474 100644 --- a/lib/recover/bloc/manual_state.dart +++ b/lib/recover/bloc/manual_state.dart @@ -1,6 +1,6 @@ +import 'package:bb_mobile/_pkg/gdrive.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:googleapis/drive/v3.dart'; -import 'package:recoverbull_dart/recoverbull_dart.dart'; part 'manual_state.freezed.dart'; @@ -9,7 +9,7 @@ class ManualState with _$ManualState { const factory ManualState({ @Default('') String error, @Default(false) bool loading, - GoogleDriveStorage? googleDriveStorage, + GoogleDriveApi? googleDriveApi, @Default([]) List availableBackups, @Default(false) bool recovered, @Default('') String backupKey, From 874052b8ec1f4d02b56262ad3c53e416c5aab35d Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Wed, 15 Jan 2025 17:54:22 -0500 Subject: [PATCH 060/401] feat(GoogleDriveApi): refactor backup and cloud management to use GoogleDriveApi --- lib/backup/bloc/backup_cubit.dart | 66 +++---------------- lib/backup/bloc/cloud_cubit.dart | 103 ++++++++++++++++-------------- 2 files changed, 64 insertions(+), 105 deletions(-) diff --git a/lib/backup/bloc/backup_cubit.dart b/lib/backup/bloc/backup_cubit.dart index dde0be18d..943b49818 100644 --- a/lib/backup/bloc/backup_cubit.dart +++ b/lib/backup/bloc/backup_cubit.dart @@ -3,13 +3,10 @@ import 'dart:io'; import 'package:bb_mobile/_model/backup.dart'; import 'package:bb_mobile/_model/wallet.dart'; -import 'package:bb_mobile/_pkg/crypto.dart'; import 'package:bb_mobile/_pkg/file_storage.dart'; import 'package:bb_mobile/_pkg/wallet/labels.dart'; import 'package:bb_mobile/_repository/wallet/sensitive_wallet_storage.dart'; import 'package:bb_mobile/backup/bloc/backup_state.dart'; -import 'package:bdk_flutter/bdk_flutter.dart' as bdk; -import 'package:bip85/bip85.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:hex/hex.dart'; import 'package:recoverbull_dart/recoverbull_dart.dart'; @@ -73,7 +70,6 @@ class BackupCubit extends Cubit { backup = backup.copyWith(descriptors: descriptors); } - // why backup the labels since in restore we are not using them _recoverBackup() if (confirmedBackups["labels"] == true) { final walletLabels = WalletLabels(); final labels = await walletLabels.txsToBip329( @@ -138,25 +134,19 @@ class BackupCubit extends Cubit { emit(state.copyWith(error: 'No backup data available.')); return; } - // TODO; Implement a proper backup key generation logic in case the user has not provided a mnemonic - final firstMnemonic = backups.first.mnemonic; + final firstMnemonic = backups.first.mnemonic.join(' '); try { - final backupKey = await _createBackupKey( - firstMnemonic, - // TODO; Implement a proper network selection logic - bdk.Network.bitcoin, - ); - final backupId = HEX.encode(Crypto.generateRandomBytes(32)); - final plaintext = json.encode(backups.map((i) => i.toJson()).toList()); - final encrypted = - await BackupService.createBackup(backupId, plaintext, backupKey); - final now = DateTime.now(); - //TODO; Find a better filename format. - final formattedDate = now.millisecondsSinceEpoch; + const String derivationPath = "m/1608'/0'"; + final (backupKey, encrypted) = await BackupService.createBackupWithBIP85( + plaintext: plaintext, + mnemonic: firstMnemonic, + derivationPath: derivationPath, + ); + final backupId = jsonDecode(encrypted)["backupId"] as String; + final formattedDate = jsonDecode(encrypted)["createdAt"]; final filename = '${formattedDate}_$backupId.json'; - final (appDir, errDir) = await fileStorage.getAppDirectory(); if (errDir != null) { emit(state.copyWith(error: 'Failed to get application directory.')); @@ -192,41 +182,3 @@ class BackupCubit extends Cubit { void clearError() => emit(state.copyWith(error: '')); } - -Future _createBackupKey( - List? mnemonicWords, - bdk.Network network, -) async { - late final bdk.Mnemonic bdkMnemonic; - - if (mnemonicWords != null && mnemonicWords.isNotEmpty) { - // Mnemonic is present: Use it - final mnemonicString = mnemonicWords.join(' '); - bdkMnemonic = await bdk.Mnemonic.fromString(mnemonicString); - } else { - // Mnemonic is absent: Generate a new random one - bdkMnemonic = await bdk.Mnemonic.create(bdk.WordCount.words12); - } - final descriptorSecretKey = await bdk.DescriptorSecretKey.create( - network: network, - mnemonic: bdkMnemonic, - //TODO: Implement actual password logic - password: '', // Passphrase (if any) - ); - - final extendedPrivateKey = descriptorSecretKey.asString().substring(0, 111); - const String derivationPath = "m/1608'/0'"; - final derivedKeyBytes = - _deriveBip85(xprv: extendedPrivateKey, path: derivationPath); - final backupKeyHex = HEX.encode(derivedKeyBytes.sublist(0, 32)); - return backupKeyHex; -} - -List _deriveBip85({required String xprv, required String path}) { - //TODO: Implement actual derivation logic - // This is a dummy implementation for demonstration purposes. - // Replace this with your actual derivation logic. - print("Deriving with xprv: $xprv and path: $path"); - final derived = derive(xprv: xprv, path: path); - return derived.sublist(0, 64); // Dummy 64-byte result -} diff --git a/lib/backup/bloc/cloud_cubit.dart b/lib/backup/bloc/cloud_cubit.dart index 3f1bb3e36..2b135b19d 100644 --- a/lib/backup/bloc/cloud_cubit.dart +++ b/lib/backup/bloc/cloud_cubit.dart @@ -1,135 +1,142 @@ import 'dart:convert'; import 'dart:io' as io; - -import 'package:bb_mobile/_pkg/consts/configs.dart'; import 'package:bb_mobile/_pkg/gdrive.dart'; import 'package:bb_mobile/backup/bloc/cloud_state.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:googleapis/drive/v3.dart'; import 'package:hex/hex.dart'; -import 'package:recoverbull_dart/recoverbull_dart.dart'; -//TODO; Move this google drive cubit and share it with recover and backup cubit class CloudCubit extends Cubit { CloudCubit() : super(const CloudState()); void clearToast() => emit(state.copyWith(toast: '', loading: false)); void clearError() => emit(state.copyWith(error: '', loading: false)); - //move to a google drive repository + Future driveConnect() async { try { emit(state.copyWith(loading: true)); - final googleSignInAccount = await GoogleDriveApi.google.signIn(); - final googleDriveStorage = await GoogleDriveStorage.connect( - googleSignInAccount, - defaultBackupPath, - ); - if (googleSignInAccount != null) { + final (googleDriveApi, err) = await GoogleDriveApi.connect(); + + if (googleDriveApi != null) { emit( state.copyWith( - googleDriveStorage: googleDriveStorage, + googleDriveApi: googleDriveApi, loading: false, ), ); - } else { + } else if (err != null) { emit( state.copyWith( - error: "Google drive user has not authenticated", + error: err.message, loading: false, ), ); - return; } } catch (e) { - debugPrint("GoogleDriveStorage.connect Error: $e"); emit( state.copyWith( - error: "GoogleDriveStorage.connect Error: $e", + error: "GoogleDrive Error: $e", loading: false, ), ); - return; } } Future uploadBackup(String backupPath, String backupName) async { - if (state.googleDriveStorage == null) await driveConnect(); + if (state.googleDriveApi == null) await driveConnect(); final backup = io.File(backupPath); final content = await backup.readAsString(); final decoded = HEX.decode(content); - final isCreated = - await state.googleDriveStorage?.writeMetaData(decoded, backupName); + final (isCreated, err) = + await state.googleDriveApi?.saveBackup(decoded, backupName) ?? + (false, 'Google Drive API is not available.'); if (isCreated == false) { emit( state.copyWith( - error: "Failed to backup file to google drive.", + error: "Failed to backup file to Google Drive: $err", loading: false, ), ); - return; } else { emit( state.copyWith( - toast: "Google drive backup successful", + toast: "Google Drive backup successful", loading: false, ), ); - return; } } Future readAllBackups() async { try { - if (state.googleDriveStorage == null) driveConnect(); emit(state.copyWith(loading: true)); - final availableBackups = - await state.googleDriveStorage?.readAllMetaDataFiles(); - if (availableBackups != null) { + final api = state.googleDriveApi; + if (api == null) { + await driveConnect(); + } + + if (api == null) { emit( state.copyWith( loading: false, - toast: "Found ${availableBackups.length} backups files", - availableBackups: availableBackups, + error: "Google Drive API is not available.", ), ); - } else { + return; + } + + final (availableBackups, err) = await api.listAllBackupFiles(); + if (err != null) { emit( state.copyWith( loading: false, - error: "No backup files found", + error: "Failed to list backup files: ${err.message}", ), ); + return; + } + + if (availableBackups.isNotEmpty) { + emit( + state.copyWith(loading: false, availableBackups: availableBackups), + ); + } else { + emit(state.copyWith(loading: false, error: "No backup files found")); } } catch (e) { emit( - state.copyWith( - loading: false, - error: "Failed to read all backups: $e", - ), + state.copyWith(loading: false, error: "Failed to read all backups: $e"), ); } } Future readCloudBackup(File file) async { try { - if (state.googleDriveStorage == null) driveConnect(); - emit( - state.copyWith( - loading: true, - ), - ); - final metaData = - await state.googleDriveStorage!.readMetaDataContent(file); - final decodeEncryptedFile = utf8.decode(metaData); + if (state.googleDriveApi == null) await driveConnect(); + emit(state.copyWith(loading: true)); + + final (metaData, err) = + await state.googleDriveApi!.loadBackupContent(file); + if (err != null) { + emit( + state.copyWith( + loading: false, + error: "Failed to read backup: ${err.message}", + ), + ); + return; + } + + final decodeEncryptedFile = utf8.decode(metaData!); final id = jsonDecode(decodeEncryptedFile)['backupId']?.toString() ?? ''; if (decodeEncryptedFile.isEmpty || id.isEmpty) { - emit(state.copyWith(error: 'Invalid backup data')); + emit(state.copyWith(loading: false, error: 'Invalid backup data')); return; } + emit( state.copyWith( loading: false, From 87af2bec198eab3fa87a1b51fe27c53e131fec08 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Wed, 15 Jan 2025 17:54:35 -0500 Subject: [PATCH 061/401] fix: update cloud page to use GoogleDriveApi instead of GoogleDriveStorage --- lib/backup/cloud_page.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/backup/cloud_page.dart b/lib/backup/cloud_page.dart index d5a171d1e..15115e400 100644 --- a/lib/backup/cloud_page.dart +++ b/lib/backup/cloud_page.dart @@ -73,7 +73,7 @@ class _CloudPageState extends State { }, ), const Gap(10), - if (state.googleDriveStorage != null) + if (state.googleDriveApi != null) BBButton.big( onPressed: cubit.disconnect, label: "LOGOUT", From a5e52e0b0c94ee8213933aef47abcec913e4a99a Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Wed, 15 Jan 2025 17:58:00 -0500 Subject: [PATCH 062/401] chore: update dependencies in pubspec.lock --- pubspec.lock | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index a977fbe91..171ce20a1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -78,6 +78,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.31.2" + bip32: + dependency: transitive + description: + name: bip32 + sha256: "54787cd7a111e9d37394aabbf53d1fc5e2e0e0af2cd01c459147a97c0e3f8a97" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + bip39: + dependency: transitive + description: + name: bip39 + sha256: de1ee27ebe7d96b84bb3a04a4132a0a3007dcdd5ad27dd14aa87a29d97c45edc + url: "https://pub.dev" + source: hosted + version: "1.0.6" bip85: dependency: "direct main" description: @@ -966,14 +982,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" - logger: - dependency: transitive - description: - name: logger - sha256: be4b23575aac7ebf01f225a241eb7f6b5641eeaf43c6a8613510fc2f8cf187d1 - url: "https://pub.dev" - source: hosted - version: "2.5.0" logging: dependency: transitive description: @@ -1332,7 +1340,7 @@ packages: description: path: "." ref: HEAD - resolved-ref: "828a84ff723849d2f58eb607fcf1171348d0de6b" + resolved-ref: c49577fdf1c21c6fcded217dfc76c34e3b5520f2 url: "https://github.com/StaxoLotl/recoverbull-dart.git" source: git version: "0.0.1" From 0094106145d9bb357630bf475e31656eb8e6635e Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Wed, 15 Jan 2025 17:58:24 -0500 Subject: [PATCH 063/401] chore: update Podfile.lock with new dependency checksums --- ios/Podfile.lock | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index b2ec154d8..90744a7f7 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -179,8 +179,8 @@ SPEC CHECKSUMS: boltz: 6388ec2412f3753b63a9e65c97f87ea26f43bddc DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 - document_file_save_plus: 47b52a647efb29f7d431af45a856ce1a841f32fc - file_picker: 9b3292d7c8bc68c8a7bf8eb78f730e49c8efc517 + document_file_save_plus: 913d440d8b611ae19add4522ed578e3ed1483a2f + file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 flutter_barcode_scanner: 7a1144744c28dc0c57a8de7218ffe5ec59a9e4bf flutter_file_dialog: 4c014a45b105709a27391e266c277d7e588e9299 @@ -189,15 +189,15 @@ SPEC CHECKSUMS: flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12 integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573 lwk: 22e06bc5664247d6b2dac91cfe209b63b70dd580 - no_screenshot: 6d183496405a3ab709a67a54e5cd0f639e94729e - path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + no_screenshot: 67d110f12466f4913b488803d4e498d03ef2889e + path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 payjoin_flutter: 6397d7b698cdad6453be4949ab6aca1863f6c5e5 - permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d + permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 ScreenProtectorKit: 83a6281b02c7a5902ee6eac4f5045f674e902ae4 SDWebImage: 8a6b7b160b4d710e2a22b6900e25301075c34cb3 - share_plus: 011d6fb4f9d2576b83179a3a5c5e323202cdabcf + share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 - url_launcher_ios: 694010445543906933d732453a59da0a173ae33d + url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe PODFILE CHECKSUM: 819463e6a0290f5a72f145ba7cde16e8b6ef0796 From 27b5f03085169f1a27aaa72dcd51567e30bfbbf7 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 20 Jan 2025 18:15:21 -0500 Subject: [PATCH 064/401] chore: remove deprecated GoogleDriveApi implementation --- lib/_pkg/gdrive.dart | 181 ------------------------------------------- 1 file changed, 181 deletions(-) delete mode 100644 lib/_pkg/gdrive.dart diff --git a/lib/_pkg/gdrive.dart b/lib/_pkg/gdrive.dart deleted file mode 100644 index 8eb5982d8..000000000 --- a/lib/_pkg/gdrive.dart +++ /dev/null @@ -1,181 +0,0 @@ -import 'dart:async'; -import 'package:bb_mobile/_pkg/consts/configs.dart'; -import 'package:bb_mobile/_pkg/error.dart'; -import 'package:extension_google_sign_in_as_googleapis_auth/extension_google_sign_in_as_googleapis_auth.dart'; -import 'package:flutter/rendering.dart'; -import 'package:google_sign_in/google_sign_in.dart'; -import 'package:googleapis/drive/v3.dart'; - -class GoogleDriveApi { - static final google = GoogleSignIn(scopes: [DriveApi.driveFileScope]); - - final DriveApi api; - final GoogleSignInAccount account; - String? _backupFolderId; - - GoogleDriveApi._(this.api, this.account); - - static Future<(GoogleDriveApi?, Err?)> connect() async { - try { - final account = await google.signIn(); - if (account == null) { - return ( - null, - Err('Google Sign-In was cancelled or failed. Please try again.') - ); - } - - final client = await google.authenticatedClient(); - if (client == null) { - return ( - null, - Err('Failed to authenticate with Google. Please check your internet connection and try again.') - ); - } - - final api = DriveApi(client); - final service = GoogleDriveApi._(api, account); - final (success, err) = await service._setupBackupFolder(); - if (err != null) { - return (null, err); - } - return (service, null); - } catch (e) { - return (null, Err('An unexpected error occurred: $e')); - } - } - - static Future disconnect() async => google.disconnect(); - - Future<(bool, Err?)> _setupBackupFolder() async { - try { - const folderName = '.$defaultBackupPath'; - final existing = await api.files.list( - q: "name = '$folderName' and mimeType = 'application/vnd.google-apps.folder' and trashed = false", - spaces: 'drive', - $fields: 'files(id)', - ); - if (existing.files?.isNotEmpty == true) { - _backupFolderId = existing.files!.first.id; - return (true, null); - } - - final folderMetadata = File() - ..name = folderName - ..mimeType = 'application/vnd.google-apps.folder' - ..appProperties = { - 'created': DateTime.now().toIso8601String(), - }; - final folder = await api.files.create(folderMetadata); - _backupFolderId = folder.id; - - final (success, err) = await _applyFolderPermissions(folder.id!); - if (err != null) { - return (false, err); - } - - debugPrint('Initialized secure backup folder: $folderName'); - return (true, null); - } catch (e) { - return (false, Err('Failed to initialize backup folder: $e')); - } - } - - Future<(bool, Err?)> _applyFolderPermissions(String folderId) async { - try { - await api.permissions.create( - Permission() - ..role = 'owner' - ..type = 'user' - ..emailAddress = account.email, - folderId, - transferOwnership: true, - ); - return (true, null); - } catch (e) { - return (false, Err('Failed to set folder permissions: $e')); - } - } - - Future<(File?, Err?)> _uploadBackupFile( - String fileName, - List data, - ) async { - try { - final file = File() - ..name = fileName - ..parents = [_backupFolderId!] - ..appProperties = { - 'timestamp': DateTime.now().toIso8601String(), - }; - final createdFile = await api.files.create( - file, - uploadMedia: Media(Stream.value(data), data.length), - ); - return (createdFile, null); - } catch (e) { - return (null, Err('Failed to create backup: $e')); - } - } - - Future<(bool, Err?)> saveBackup(List data, String fileName) async { - final (file, err) = await _uploadBackupFile(fileName, data); - if (err != null) { - return (false, err); - } - debugPrint('Successfully saved backup: $fileName'); - return (true, null); - } - - Future<(List?, Err?)> loadBackupContent(File file) async { - try { - final media = await api.files.get( - file.id!, - downloadOptions: DownloadOptions.fullMedia, - ) as Media; - - final data = await _fetchMediaStream(media); - return (data, null); - } catch (e) { - return (null, Err('Error reading backup content: $e')); - } - } - - Future> _fetchMediaStream(Media media) async { - final completer = Completer>(); - final bytes = []; - - media.stream.listen( - bytes.addAll, - onError: (error) => completer - .completeError(Exception('Error streaming backup data: $error')), - onDone: () => completer.complete(bytes), - cancelOnError: true, - ); - - return completer.future; - } - - Future<(List, Err?)> listAllBackupFiles() async { - if (_backupFolderId == null) { - return ([], Err('Backup folder not initialized')); - } - try { - final response = await api.files.list( - q: "'$_backupFolderId' in parents and trashed = false", - spaces: 'drive', - $fields: 'files(id, name, createdTime, modifiedTime, appProperties)', - orderBy: 'modifiedTime desc', - ); - - if (response.files == null || response.files!.isEmpty) { - debugPrint('No metadata files found'); - return ([], null); - } - - return (response.files ?? [], null); - } catch (e) { - return ([], Err('Failed to list backups: $e')); - } - } -} From cc0a131fa2d0516457c24f2a54af9b331d75c7c5 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 20 Jan 2025 18:20:40 -0500 Subject: [PATCH 065/401] feat(backup): implement backups manager with Google Drive and local storage support --- lib/_pkg/backup/_interface.dart | 67 ++++++++++ lib/_pkg/backup/google_drive.dart | 214 ++++++++++++++++++++++++++++++ lib/_pkg/backup/local.dart | 88 ++++++++++++ 3 files changed, 369 insertions(+) create mode 100644 lib/_pkg/backup/_interface.dart create mode 100644 lib/_pkg/backup/google_drive.dart create mode 100644 lib/_pkg/backup/local.dart diff --git a/lib/_pkg/backup/_interface.dart b/lib/_pkg/backup/_interface.dart new file mode 100644 index 000000000..e05e3745f --- /dev/null +++ b/lib/_pkg/backup/_interface.dart @@ -0,0 +1,67 @@ +import 'dart:convert'; + +import 'package:bb_mobile/_model/backup.dart'; +import 'package:bb_mobile/_pkg/consts/configs.dart'; +import 'package:bb_mobile/_pkg/error.dart'; +import 'package:recoverbull_dart/recoverbull_dart.dart'; + +abstract class IBackupManager { + /// Encrypts a list of backups using BIP85. + /// Returns a tuple containing the backupKey and encrypted data or an error. + Future<((String, String)?, Err?)> encryptBackups({ + required List backups, + required String derivationPath, + required String backupKeyMnemonic, + }) async { + try { + final plaintext = json.encode(backups.map((i) => i.toJson()).toList()); + final (backupKey, encrypted) = await BackupService.createBackupWithBIP85( + plaintext: plaintext, + mnemonic: backupKeyMnemonic, + network: backups.first.network.toLowerCase(), + derivationPath: derivationPath, + ); + return ((backupKey, encrypted), null); + } catch (e) { + return (null, Err('Failed to encrypt backups: $e')); + } + } + + /// Decrypts an encrypted backup using the provided backup key. + /// Returns a list of backups. + Future<(List?, Err?)> decryptBackups({ + required String encrypted, + required String backupKey, + }) async { + try { + final plaintext = await BackupService.restoreBackup(encrypted, backupKey); + final decodedJson = jsonDecode(plaintext) as List; + final backups = decodedJson + .map((item) => Backup.fromJson(item as Map)) + .toList(); + return (backups, null); + } catch (e) { + return (null, Err('Failed to decrypt backups: $e')); + } + } + + /// Writes the encrypted backup to a storage medium. + /// Returns the path to the written backup or an error. + Future<(String?, Err?)> saveEncryptedBackup({ + required String encrypted, + String backupFolder = defaultBackupPath, + }); + + /// Reads the encrypted backup from a storage medium. + /// Returns a map containing the backup data and the backup ID, or an error. + Future<(Map?, Err?)> loadEncryptedBackup({ + required String encrypted, + }); + + /// Deletes the encrypted backup from a storage medium. + /// Returns the path to the deleted backup or an error. + Future<(String?, Err?)> removeEncryptedBackup({ + required String backupName, + String backupFolder = defaultBackupPath, + }); +} diff --git a/lib/_pkg/backup/google_drive.dart b/lib/_pkg/backup/google_drive.dart new file mode 100644 index 000000000..d89502216 --- /dev/null +++ b/lib/_pkg/backup/google_drive.dart @@ -0,0 +1,214 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:bb_mobile/_pkg/backup/_interface.dart'; +import 'package:bb_mobile/_pkg/consts/configs.dart'; +import 'package:bb_mobile/_pkg/error.dart'; +import 'package:extension_google_sign_in_as_googleapis_auth/extension_google_sign_in_as_googleapis_auth.dart'; +import 'package:google_sign_in/google_sign_in.dart'; +import 'package:googleapis/drive/v3.dart'; +import 'package:hex/hex.dart'; + +///TODO; Update it to select the cloud backup provider +class GoogleDriveBackupManager extends IBackupManager { + static final _google = GoogleSignIn(scopes: [DriveApi.driveFileScope]); + + DriveApi? _api; + GoogleSignInAccount? _account; + + Future<(String?, Err?)> connect() async { + try { + final account = await _google.signIn(); + if (account == null) { + return ( + null, + Err('Google Sign-In was cancelled or failed. Please try again.') + ); + } + + final client = await _google.authenticatedClient(); + if (client == null) { + return (null, Err('Failed to authenticate with Google.')); + } + + _api = DriveApi(client); + _account = account; + + final (folderId, err) = await _setupBackupFolder(); + if (err != null) { + return (null, err); + } + return (folderId, null); + } catch (e) { + return (null, Err('An unexpected error occurred: $e')); + } + } + + Future disconnect() async => _google.disconnect(); + + Future<(String?, Err?)> _setupBackupFolder() async { + try { + const folderName = '.$defaultBackupPath'; + final existing = await _api!.files.list( + q: "name = '$folderName' and mimeType = 'application/vnd.google-apps.folder' and trashed = false", + spaces: 'drive', + $fields: 'files(id)', + ); + if (existing.files?.isNotEmpty == true) { + final backupFolderId = existing.files!.first.id; + return (backupFolderId, null); + } + + final folderMetadata = File() + ..name = folderName + ..mimeType = 'application/vnd.google-apps.folder' + ..appProperties = {'created': DateTime.now().toIso8601String()}; + final folder = await _api!.files.create(folderMetadata); + + final (success, err) = await _applyFolderPermissions(folder.id!); + if (!success) { + return (null, err); + } + + return (folder.id, null); + } catch (e) { + return (null, Err('Failed to initialize backup folder: $e')); + } + } + + Future<(bool, Err?)> _applyFolderPermissions(String folderId) async { + try { + await _api!.permissions.create( + Permission() + ..role = 'owner' + ..type = 'user' + ..emailAddress = _account!.email, + folderId, + transferOwnership: true, + ); + return (true, null); + } catch (e) { + return (false, Err('Failed to set folder permissions: $e')); + } + } + + @override + Future<(String?, Err?)> saveEncryptedBackup({ + required String encrypted, + String backupFolder = defaultBackupPath, + }) async { + if (_api == null) return (null, Err('Not connected to Google Drive')); + + try { + final decodeEncryptedFile = jsonDecode(utf8.decode(HEX.decode(encrypted))) + as Map; + final backupId = decodeEncryptedFile['backupId']?.toString() ?? ''; + final now = DateTime.now(); + final formattedDate = now.millisecondsSinceEpoch; + final filename = '${formattedDate}_$backupId.json'; + final file = File() + ..name = filename + ..parents = [backupFolder]; + + final data = encrypted.codeUnits; + await _api!.files.create( + file, + uploadMedia: Media(Stream.value(data), data.length), + ); + + return (filename, null); + } catch (e) { + return (null, Err('Failed to create backup: $e')); + } + } + + @override + Future<(Map?, Err?)> loadEncryptedBackup({ + required String encrypted, + }) async { + try { + final decodeEncryptedFile = jsonDecode(utf8.decode(HEX.decode(encrypted))) + as Map; + final id = decodeEncryptedFile['backupId']; + if (id == null) { + return (null, Err("Corrupted backup file")); + } + return (decodeEncryptedFile, null); + } catch (e) { + return (null, Err('Failed to read encrypted backup: $e')); + } + } + + Future<(Map?, Err?)> loadAllEncryptedBackupFiles({ + required String backupFolder, + }) async { + if (_api == null) return (null, Err('Not connected to Google Drive')); + + try { + final response = await _api!.files.list( + q: "'$backupFolder' in parents and trashed = false", + spaces: 'drive', + $fields: 'files(id, name, createdTime)', + orderBy: 'createdTime desc', + ); + + if (response.files == null || response.files!.isEmpty) { + return (null, Err('No backups found')); + } + + final backups = {}; + for (final file in response.files!) { + backups[file.name!] = file; + } + + return (backups, null); + } catch (e) { + return (null, Err('Failed to load backups: $e')); + } + } + + @override + Future<(String?, Err?)> removeEncryptedBackup({ + required String backupName, + String backupFolder = defaultBackupPath, + }) async { + if (_api == null) return (null, Err('Not connected to Google Drive')); + + try { + final response = await _api!.files.list( + q: "'$backupFolder' in parents and name = '$backupName' and trashed = false", + spaces: 'drive', + $fields: 'files(id)', + ); + + if (response.files == null || response.files!.isEmpty) { + return (null, Err('Backup not found')); + } + + await _api!.files.delete(response.files!.first.id!); + return (backupName, null); + } catch (e) { + return (null, Err('Failed to remove backup: $e')); + } + } + + Future> fetchMediaStream({required File file}) async { + final media = await _api!.files.get( + file.id!, + downloadOptions: DownloadOptions.fullMedia, + ) as Media; + + final completer = Completer>(); + final bytes = []; + + media.stream.listen( + bytes.addAll, + onError: (error) => completer.completeError( + Exception('Error streaming backup data: $error'), + ), + onDone: () => completer.complete(bytes), + cancelOnError: true, + ); + return completer.future; + } +} diff --git a/lib/_pkg/backup/local.dart b/lib/_pkg/backup/local.dart new file mode 100644 index 000000000..0e072dae0 --- /dev/null +++ b/lib/_pkg/backup/local.dart @@ -0,0 +1,88 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:bb_mobile/_pkg/backup/_interface.dart'; +import 'package:bb_mobile/_pkg/consts/configs.dart'; +import 'package:bb_mobile/_pkg/error.dart'; +import 'package:bb_mobile/_pkg/file_storage.dart'; +import 'package:bb_mobile/locator.dart'; +import 'package:hex/hex.dart'; + +class FileSystemBackupManager extends IBackupManager { + final FileStorage fileStorage = locator(); + + FileSystemBackupManager(); + + /// Deletes the encrypted backup from the specified directory. + /// Returns the path to the deleted backup or an error message. + @override + Future<(String?, Err?)> removeEncryptedBackup({ + required String backupName, + String backupFolder = defaultBackupPath, + }) async { + try { + final result = await fileStorage.deleteFile(backupName); + if (result == null) return (null, Err('Failed to delete file.')); + return (result.message, null); + } catch (e) { + return (null, Err('Failed to delete encrypted backup: $e')); + } + } + + /// Reads the encrypted backup from the specified file. + /// Returns a map containing the backup data and the backup ID, or an error message. + @override + Future<(Map?, Err?)> loadEncryptedBackup({ + required String encrypted, + }) async { + try { + final decodeEncryptedFile = + jsonDecode(utf8.decode(HEX.decode(encrypted))) as Map; + + final id = decodeEncryptedFile['backupId']?.toString() ?? ''; + if (id.isEmpty) { + return (null, Err("Corrupted backup file")); + } + return (decodeEncryptedFile, null); + } catch (e) { + return (null, Err('Failed to read encrypted backup: $e')); + } + } + + /// Writes the encrypted backup with backupId, to a storage medium. + /// Returns the path to the written backup or an error message. + @override + Future<(String?, Err?)> saveEncryptedBackup({ + required String encrypted, + String backupFolder = defaultBackupPath, + }) async { + try { + final decodeEncryptedFile = jsonDecode(encrypted) as Map; + + final backupId = decodeEncryptedFile['backupId']?.toString() ?? ''; + final now = DateTime.now(); + final formattedDate = now.millisecondsSinceEpoch; + final filename = '${formattedDate}_$backupId.json'; + + final (appDir, errDir) = await fileStorage.getAppDirectory(); + if (errDir != null) { + return (null, Err('Failed to get application directory.')); + } + + final backupDir = + await Directory('${appDir!}/$backupFolder').create(recursive: true); + final file = File('${backupDir.path}/$filename'); + + final (f, errSave) = await fileStorage.saveToFile( + file, + HEX.encode(utf8.encode(encrypted)), + ); + if (errSave != null) { + return (null, Err(errSave.message)); + } + return (file.path, null); + } catch (e) { + return (null, Err('Failed to write encrypted backup: $e')); + } + } +} From c0bd90d44874a91d2c419ec804b585112009469f Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 20 Jan 2025 18:21:27 -0500 Subject: [PATCH 066/401] feat(locator): register FileSystemBackupManager and GoogleDriveBackupManager in locator --- lib/locator.dart | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/locator.dart b/lib/locator.dart index fe6c8c8d2..9d6924710 100644 --- a/lib/locator.dart +++ b/lib/locator.dart @@ -1,3 +1,5 @@ +import 'package:bb_mobile/_pkg/backup/google_drive.dart'; +import 'package:bb_mobile/_pkg/backup/local.dart'; import 'package:bb_mobile/_pkg/barcode.dart'; import 'package:bb_mobile/_pkg/boltz/swap.dart'; import 'package:bb_mobile/_pkg/bull_bitcoin_api.dart'; @@ -132,6 +134,9 @@ Future _setupAppServices() async { ); locator.registerSingleton(FileStorage()); + locator.registerSingleton(FileSystemBackupManager()); + locator + .registerSingleton(GoogleDriveBackupManager()); } Future _setupWalletServices() async { From c02dcbb8cdcea1b852db666ec4461df0ef0c6215 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 20 Jan 2025 18:22:17 -0500 Subject: [PATCH 067/401] refactor(routes): update imports and modify onBackupSelected callback --- lib/routes.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/routes.dart b/lib/routes.dart index 93ede3df0..f5ddee211 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -6,8 +6,6 @@ import 'package:bb_mobile/_model/wallet.dart'; import 'package:bb_mobile/_ui/logger_page.dart'; import 'package:bb_mobile/auth/page.dart'; import 'package:bb_mobile/backup/backup_page.dart'; -import 'package:bb_mobile/backup/bloc/cloud_cubit.dart'; -import 'package:bb_mobile/backup/cloud_page.dart'; import 'package:bb_mobile/backup/keychain_page.dart'; import 'package:bb_mobile/create/page.dart'; import 'package:bb_mobile/home/home_page.dart'; @@ -17,6 +15,8 @@ import 'package:bb_mobile/import/hardware_page.dart'; import 'package:bb_mobile/import/page.dart'; import 'package:bb_mobile/locator.dart'; import 'package:bb_mobile/receive/receive_page.dart'; +import 'package:bb_mobile/recover/bloc/cloud_cubit.dart'; +import 'package:bb_mobile/recover/cloud_page.dart'; import 'package:bb_mobile/recover/keychain_page.dart'; import 'package:bb_mobile/recover/manual_page.dart'; import 'package:bb_mobile/send/bloc/send_cubit.dart'; @@ -226,9 +226,9 @@ GoRouter setupRouter() => GoRouter( builder: (context, state) { final data = state.extra! as Map; final cloudCubit = data['cubit'] as CloudCubit; - Function(String, String)? onBackupSelected; + Function(String)? onBackupSelected; if (data['callback'] != null) { - onBackupSelected = data['callback']! as Function(String, String)?; + onBackupSelected = data['callback']! as Function(String)?; } return BlocProvider.value( From b567298bfc7562423d0ac34e88d864c749f0c0be Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 20 Jan 2025 18:28:46 -0500 Subject: [PATCH 068/401] feat(backup): replace BackupCubit with ManualCubit and ManualState --- lib/backup/bloc/backup_cubit.dart | 184 -------------------------- lib/backup/bloc/backup_state.dart | 18 --- lib/backup/bloc/manual_cubit.dart | 209 ++++++++++++++++++++++++++++++ lib/backup/bloc/manual_state.dart | 28 ++++ 4 files changed, 237 insertions(+), 202 deletions(-) delete mode 100644 lib/backup/bloc/backup_cubit.dart delete mode 100644 lib/backup/bloc/backup_state.dart create mode 100644 lib/backup/bloc/manual_cubit.dart create mode 100644 lib/backup/bloc/manual_state.dart diff --git a/lib/backup/bloc/backup_cubit.dart b/lib/backup/bloc/backup_cubit.dart deleted file mode 100644 index 943b49818..000000000 --- a/lib/backup/bloc/backup_cubit.dart +++ /dev/null @@ -1,184 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; - -import 'package:bb_mobile/_model/backup.dart'; -import 'package:bb_mobile/_model/wallet.dart'; -import 'package:bb_mobile/_pkg/file_storage.dart'; -import 'package:bb_mobile/_pkg/wallet/labels.dart'; -import 'package:bb_mobile/_repository/wallet/sensitive_wallet_storage.dart'; -import 'package:bb_mobile/backup/bloc/backup_state.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:hex/hex.dart'; -import 'package:recoverbull_dart/recoverbull_dart.dart'; - -class BackupCubit extends Cubit { - BackupCubit({ - required this.wallets, - required this.walletSensitiveStorage, - required this.fileStorage, - }) : super(const BackupState()); - - final FileStorage fileStorage; - final List wallets; - final WalletSensitiveStorageRepository walletSensitiveStorage; - - Future loadBackupData() async { - emit(state.copyWith(loading: true, error: '')); - final backups = []; - final confirmedBackups = state.confirmedBackups; - - for (final wallet in wallets) { - var backup = Backup( - name: wallet.name ?? '', - network: wallet.network.name.toLowerCase(), - layer: wallet.baseWalletType.name.toLowerCase(), - script: wallet.scriptType.name.toLowerCase(), - type: wallet.type.name.toLowerCase(), - mnemonicFingerPrint: wallet.mnemonicFingerprint, - ); - - final seedStorageString = wallet.getRelatedSeedStorageString(); - - if (confirmedBackups["mnemonic"] == true && - confirmedBackups["passphrase"] == true) { - final (seed, error) = await walletSensitiveStorage.readSeed( - fingerprintIndex: seedStorageString, - ); - if (error != null) { - emit(state.copyWith(error: 'Error reading seed: ${error.message}')); - return; - } - if (seed == null) { - emit(state.copyWith(error: 'Seed data is missing.')); - return; - } - - final mnemonic = seed.mnemonic.split(' '); - - final passphrase = wallet.hasPassphrase() - ? seed.passphrases - .firstWhere( - (e) => e.sourceFingerprint == wallet.sourceFingerprint, - ) - .passphrase - : ''; - backup = backup.copyWith(mnemonic: mnemonic, passphrase: passphrase); - } - // why backup the descriptors since in _recoverBackup() calling [oneFromBIP39] to generate the descriptors again - if (confirmedBackups["descriptors"] == true) { - final descriptors = [wallet.getDescriptorCombined()]; - backup = backup.copyWith(descriptors: descriptors); - } - - if (confirmedBackups["labels"] == true) { - final walletLabels = WalletLabels(); - final labels = await walletLabels.txsToBip329( - wallet.transactions, - wallet.originString(), - ) - ..addAll( - await walletLabels.addressesToBip329( - wallet.myAddressBook, - wallet.originString(), - ), - ); - backup = backup.copyWith(labels: labels); - } - backups.add(backup); - } - - emit(state.copyWith(loadedBackups: backups, loading: false)); - } - - Future loadConfirmedBackups() async { - const confirmedBackups = { - "mnemonic": true, - "passphrase": true, - "descriptors": true, - "labels": true, - "script": true, - }; - emit(state.copyWith(confirmedBackups: confirmedBackups, loading: false)); - } - - void toggleDescriptors() { - _toggleBackupOption("descriptors"); - } - - void toggleLabels() { - _toggleBackupOption("labels"); - } - - void _toggleBackupOption(String option) { - final confirmedBackups = Map.from(state.confirmedBackups); - final confirmed = confirmedBackups[option] ?? false; - confirmedBackups[option] = !confirmed; - emit(state.copyWith(confirmedBackups: confirmedBackups)); - } - - void toggleAllMnemonicAndPassphrase() { - final confirmedBackups = Map.from(state.confirmedBackups); - final areBothConfirmed = confirmedBackups["mnemonic"] == true && - confirmedBackups["passphrase"] == true; - final newConfirmed = !areBothConfirmed; - confirmedBackups["mnemonic"] = newConfirmed; - confirmedBackups["passphrase"] = newConfirmed; - emit(state.copyWith(confirmedBackups: confirmedBackups)); - } - - Future writeEncryptedBackup() async { - emit(state.copyWith(loading: true, error: '')); - await loadBackupData(); - final backups = state.loadedBackups; - if (backups.isEmpty) { - emit(state.copyWith(error: 'No backup data available.')); - return; - } - final firstMnemonic = backups.first.mnemonic.join(' '); - - try { - final plaintext = json.encode(backups.map((i) => i.toJson()).toList()); - const String derivationPath = "m/1608'/0'"; - final (backupKey, encrypted) = await BackupService.createBackupWithBIP85( - plaintext: plaintext, - mnemonic: firstMnemonic, - derivationPath: derivationPath, - ); - final backupId = jsonDecode(encrypted)["backupId"] as String; - final formattedDate = jsonDecode(encrypted)["createdAt"]; - final filename = '${formattedDate}_$backupId.json'; - final (appDir, errDir) = await fileStorage.getAppDirectory(); - if (errDir != null) { - emit(state.copyWith(error: 'Failed to get application directory.')); - return; - } - - final backupDir = - await Directory('${appDir!}/backups/').create(recursive: true); - final file = File(backupDir.path + filename); - - final (f, errSave) = await fileStorage.saveToFile( - file, - HEX.encode(utf8.encode(encrypted)), - ); - if (errSave != null) { - emit(state.copyWith(error: 'Failed to save backup file.')); - return; - } - - emit( - state.copyWith( - backupId: backupId, - backupKey: backupKey, - backupPath: file.path, - backupName: filename, - loading: false, - ), - ); - } catch (e) { - emit(state.copyWith(error: 'An unexpected error occurred: $e')); - } - } - - void clearError() => emit(state.copyWith(error: '')); -} diff --git a/lib/backup/bloc/backup_state.dart b/lib/backup/bloc/backup_state.dart deleted file mode 100644 index 6657f90be..000000000 --- a/lib/backup/bloc/backup_state.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:bb_mobile/_model/backup.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'backup_state.freezed.dart'; - -@freezed -class BackupState with _$BackupState { - const factory BackupState({ - @Default(true) bool loading, - @Default([]) List loadedBackups, - @Default({}) Map confirmedBackups, - @Default('') String backupId, - @Default('') String backupPath, - @Default('') String backupName, - @Default('') String backupKey, - @Default('') String error, - }) = _BackupState; -} diff --git a/lib/backup/bloc/manual_cubit.dart b/lib/backup/bloc/manual_cubit.dart new file mode 100644 index 000000000..4bed5de78 --- /dev/null +++ b/lib/backup/bloc/manual_cubit.dart @@ -0,0 +1,209 @@ +import 'package:bb_mobile/_model/backup.dart'; +import 'package:bb_mobile/_model/wallet.dart'; +import 'package:bb_mobile/_pkg/backup/local.dart'; +import 'package:bb_mobile/_pkg/wallet/labels.dart'; +import 'package:bb_mobile/_repository/wallet/sensitive_wallet_storage.dart'; +import 'package:bb_mobile/backup/bloc/manual_state.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class ManualCubit extends Cubit { + ManualCubit({ + required this.wallets, + required this.walletSensitiveStorage, + required this.manager, + }) : super(const ManualState()); + + final FileSystemBackupManager manager; + final List wallets; + final WalletSensitiveStorageRepository walletSensitiveStorage; + + Future loadBackupData() async { + emit(state.copyWith(loading: true, error: '')); + final backups = []; + final selectedBackupOptions = state.selectedBackupOptions; + String? backupKeyMnemonic; + for (final wallet in wallets) { + var backup = Backup( + name: wallet.name ?? '', + network: wallet.network.name.toLowerCase(), + layer: wallet.baseWalletType.name.toLowerCase(), + script: wallet.scriptType.name.toLowerCase(), + type: wallet.type.name.toLowerCase(), + mnemonicFingerPrint: wallet.mnemonicFingerprint, + ); + final (seed, error) = await walletSensitiveStorage.readSeed( + fingerprintIndex: wallet.getRelatedSeedStorageString(), + ); + if (error != null) { + emit(state.copyWith(error: 'Error reading seed: ${error.message}')); + return; + } + + if (seed == null) { + emit(state.copyWith(error: 'Seed data is missing.')); + return; + } + if (selectedBackupOptions["mnemonic"] == true && + selectedBackupOptions["passphrase"] == true) { + if (wallet.hasPassphrase()) { + final sourceSeedPassphrases = seed.passphrases + .where((e) => e.sourceFingerprint == wallet.sourceFingerprint) + .toList(); + if (sourceSeedPassphrases.isEmpty) { + emit( + state.copyWith( + error: 'Passphrase not found for the wallet source fingerprint', + ), + ); + return; + } else { + backup = backup.copyWith( + mnemonic: seed.mnemonic.split(' '), + passphrase: sourceSeedPassphrases.first.passphrase, + ); + } + } else { + backup = backup.copyWith( + mnemonic: seed.mnemonic.split(' '), + passphrase: '', + ); + } + backupKeyMnemonic ??= seed.mnemonic; + } else { + backupKeyMnemonic ??= seed.mnemonic; + } + + if (selectedBackupOptions["descriptors"] == true) { + backup = backup.copyWith(descriptors: [wallet.getDescriptorCombined()]); + } + + if (selectedBackupOptions["labels"] == true) { + final walletLabels = WalletLabels(); + final labels = await walletLabels.txsToBip329( + wallet.transactions, + wallet.originString(), + ) + ..addAll( + await walletLabels.addressesToBip329( + wallet.myAddressBook, + wallet.originString(), + ), + ); + backup = backup.copyWith(labels: labels); + } + backups.add(backup); + } + emit( + state.copyWith( + loadedBackups: backups, + loading: false, + backupKeyMnemonic: backupKeyMnemonic ?? '', + ), + ); + } + + void toggleDescriptors() => _toggleBackupOption("descriptors"); + + void toggleLabels() => _toggleBackupOption("labels"); + + void _toggleBackupOption(String option) { + final selectedBackupOptions = + Map.from(state.selectedBackupOptions); + selectedBackupOptions[option] = !(selectedBackupOptions[option] ?? false); + emit(state.copyWith(selectedBackupOptions: selectedBackupOptions)); + } + + void toggleAllMnemonicAndPassphrase() { + final selectedBackupOptions = + Map.from(state.selectedBackupOptions); + final areBothConfirmed = selectedBackupOptions["mnemonic"] == true && + selectedBackupOptions["passphrase"] == true; + final newConfirmed = !areBothConfirmed; + selectedBackupOptions["mnemonic"] = newConfirmed; + selectedBackupOptions["passphrase"] = newConfirmed; + emit(state.copyWith(selectedBackupOptions: selectedBackupOptions)); + } + + Future saveEncryptedBackup() async { + emit(state.copyWith(loading: true, error: '')); + await loadBackupData(); + final backups = state.loadedBackups; + + if (backups.isEmpty) { + emit( + state.copyWith( + loading: false, + error: + 'No wallet details found. Please ensure your wallets have the necessary data available.', + ), + ); + return; + } + + try { + const String derivationPath = "m/1608'/0'"; + final (encData, err) = await manager.encryptBackups( + backups: backups, + derivationPath: derivationPath, + backupKeyMnemonic: state.backupKeyMnemonic, + ); + if (err != null) { + emit( + state.copyWith( + loading: false, + error: 'Failed to encrypt backups: ${err.message}', + ), + ); + return; + } + + final (filePath, errSave) = + await manager.saveEncryptedBackup(encrypted: encData!.$2); + if (errSave != null) { + emit( + state.copyWith( + loading: false, + error: 'Failed to save backup file:', + ), + ); + return; + } + final fileName = filePath?.split('/').last; + final backupId = fileName?.split('_').last.split('.').first; + emit( + state.copyWith( + backupId: backupId ?? '', + backupKey: encData.$1, + backupPath: filePath ?? '', + backupName: fileName ?? '', + loading: false, + ), + ); + } catch (e) { + emit( + state.copyWith( + loading: false, + error: 'An unexpected error occurred: $e', + ), + ); + } + } + + void clearError() => emit(state.copyWith(error: '')); + Future clearAndClose() async { + emit( + state.copyWith( + loadedBackups: [], + backupKeyMnemonic: '', + backupKey: '', + backupId: '', + backupPath: '', + backupName: '', + error: '', + loading: false, + selectedBackupOptions: {}, + ), + ); + await close(); + } +} diff --git a/lib/backup/bloc/manual_state.dart b/lib/backup/bloc/manual_state.dart new file mode 100644 index 000000000..5a650af72 --- /dev/null +++ b/lib/backup/bloc/manual_state.dart @@ -0,0 +1,28 @@ +import 'package:bb_mobile/_model/backup.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'manual_state.freezed.dart'; + +@freezed +class ManualState with _$ManualState { + const factory ManualState({ + @Default(false) bool loading, + @Default([]) List loadedBackups, + @Default({ + "mnemonic": true, + "passphrase": true, + "descriptors": true, + "labels": true, + "script": true, + }) + Map selectedBackupOptions, + @Default('') String backupKeyMnemonic, + @Default('') String backupId, + @Default('') String backupPath, + @Default('') String backupName, + @Default('') String backupKey, + // // To avoid multiple backups being created when the user clicks the button multiple times + // @Default(false) bool isBackupSaved, + @Default('') String error, + }) = _ManualState; +} From 91d2ca30abfbdd42a0b68decfae00647087e2d26 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 20 Jan 2025 18:33:14 -0500 Subject: [PATCH 069/401] refactor(cloud): update CloudCubit to use GoogleDriveBackupManager --- lib/backup/bloc/cloud_cubit.dart | 123 +++++++------------------------ lib/backup/bloc/cloud_state.dart | 6 +- 2 files changed, 29 insertions(+), 100 deletions(-) diff --git a/lib/backup/bloc/cloud_cubit.dart b/lib/backup/bloc/cloud_cubit.dart index 2b135b19d..e27744562 100644 --- a/lib/backup/bloc/cloud_cubit.dart +++ b/lib/backup/bloc/cloud_cubit.dart @@ -1,27 +1,27 @@ -import 'dart:convert'; import 'dart:io' as io; -import 'package:bb_mobile/_pkg/gdrive.dart'; + +import 'package:bb_mobile/_pkg/backup/google_drive.dart'; import 'package:bb_mobile/backup/bloc/cloud_state.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:googleapis/drive/v3.dart'; -import 'package:hex/hex.dart'; class CloudCubit extends Cubit { - CloudCubit() : super(const CloudState()); + final GoogleDriveBackupManager manager; + CloudCubit({required this.manager}) : super(const CloudState()); void clearToast() => emit(state.copyWith(toast: '', loading: false)); void clearError() => emit(state.copyWith(error: '', loading: false)); - Future driveConnect() async { + Future connect() async { try { emit(state.copyWith(loading: true)); - final (googleDriveApi, err) = await GoogleDriveApi.connect(); + final (folderId, err) = await manager.connect(); - if (googleDriveApi != null) { + if (folderId != null) { emit( state.copyWith( - googleDriveApi: googleDriveApi, + backupFolderId: folderId, loading: false, ), ); @@ -43,115 +43,48 @@ class CloudCubit extends Cubit { } } - Future uploadBackup(String backupPath, String backupName) async { - if (state.googleDriveApi == null) await driveConnect(); - final backup = io.File(backupPath); - - final content = await backup.readAsString(); + Future uploadBackup({ + required String fileSystemBackupPath, + }) async { + if (state.backupFolderId.isEmpty) await connect(); + emit(state.copyWith(loading: true)); - final decoded = HEX.decode(content); - final (isCreated, err) = - await state.googleDriveApi?.saveBackup(decoded, backupName) ?? - (false, 'Google Drive API is not available.'); - if (isCreated == false) { - emit( - state.copyWith( - error: "Failed to backup file to Google Drive: $err", - loading: false, - ), - ); - } else { - emit( - state.copyWith( - toast: "Google Drive backup successful", - loading: false, - ), - ); - } - } - - Future readAllBackups() async { try { - emit(state.copyWith(loading: true)); - final api = state.googleDriveApi; - if (api == null) { - await driveConnect(); - } - - if (api == null) { - emit( - state.copyWith( - loading: false, - error: "Google Drive API is not available.", - ), - ); - return; - } + final backup = io.File(fileSystemBackupPath); + final content = await backup.readAsString(); + final (fileName, err) = await manager.saveEncryptedBackup( + encrypted: content, + backupFolder: state.backupFolderId, + ); - final (availableBackups, err) = await api.listAllBackupFiles(); if (err != null) { + debugPrint("Failed to backup file to Google Drive: ${err.message}"); emit( state.copyWith( + error: "Failed to backup file to Google Drive", loading: false, - error: "Failed to list backup files: ${err.message}", ), ); - return; - } - - if (availableBackups.isNotEmpty) { - emit( - state.copyWith(loading: false, availableBackups: availableBackups), - ); } else { - emit(state.copyWith(loading: false, error: "No backup files found")); - } - } catch (e) { - emit( - state.copyWith(loading: false, error: "Failed to read all backups: $e"), - ); - } - } - - Future readCloudBackup(File file) async { - try { - if (state.googleDriveApi == null) await driveConnect(); - emit(state.copyWith(loading: true)); - - final (metaData, err) = - await state.googleDriveApi!.loadBackupContent(file); - if (err != null) { emit( state.copyWith( + toast: "Successfully backed up to Google Drive", loading: false, - error: "Failed to read backup: ${err.message}", ), ); - return; } - - final decodeEncryptedFile = utf8.decode(metaData!); - final id = jsonDecode(decodeEncryptedFile)['backupId']?.toString() ?? ''; - if (decodeEncryptedFile.isEmpty || id.isEmpty) { - emit(state.copyWith(loading: false, error: 'Invalid backup data')); - return; - } - - emit( - state.copyWith( - loading: false, - selectedBackup: (id, decodeEncryptedFile), - ), - ); } catch (e) { emit( state.copyWith( + error: "Failed to backup file: $e", loading: false, - error: "Failed to read backup: $e", ), ); } } - void disconnect() => GoogleDriveApi.disconnect(); + void disconnect() { + manager.disconnect(); + emit(state.copyWith(backupFolderId: '', loading: false)); + } } diff --git a/lib/backup/bloc/cloud_state.dart b/lib/backup/bloc/cloud_state.dart index c1192ffa7..26baa48e6 100644 --- a/lib/backup/bloc/cloud_state.dart +++ b/lib/backup/bloc/cloud_state.dart @@ -1,6 +1,4 @@ -import 'package:bb_mobile/_pkg/gdrive.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:googleapis/drive/v3.dart'; part 'cloud_state.freezed.dart'; @@ -8,9 +6,7 @@ part 'cloud_state.freezed.dart'; class CloudState with _$CloudState { const factory CloudState({ @Default(false) bool loading, - GoogleDriveApi? googleDriveApi, - @Default([]) List availableBackups, - @Default(('', '')) (String, String) selectedBackup, + @Default('') String backupFolderId, @Default('') String toast, @Default('') String error, }) = _CloudState; From ded0a96264c34c5414680f79646d2622b4309d9d Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 20 Jan 2025 18:40:59 -0500 Subject: [PATCH 070/401] refactor(keychain): clean up backup logic and update state.completed on store backup success --- lib/backup/bloc/keychain_cubit.dart | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/lib/backup/bloc/keychain_cubit.dart b/lib/backup/bloc/keychain_cubit.dart index edea42641..584eb2776 100644 --- a/lib/backup/bloc/keychain_cubit.dart +++ b/lib/backup/bloc/keychain_cubit.dart @@ -9,7 +9,6 @@ class KeychainCubit extends Cubit { void clearError() => state.copyWith(error: ''); void updateSecret(String value) => emit(state.copyWith(secret: value)); - void confirmSecret(String value) => emit(state.copyWith(secretConfirmed: state.secret == value)); @@ -19,11 +18,15 @@ class KeychainCubit extends Cubit { return; } - if (keychainapi.isEmpty) { - emit(state.copyWith(error: 'keychain api is not set')); - return; - } + ///TODO: check if the backup is already saved + ///TODO: if it is, then show a toast + + // if (keychainapi.isEmpty) { + // emit(state.copyWith(error: 'keychain api is not set')); + // return; + // } await _storeBackupKey(backupId, backupKey); + emit(state.copyWith(completed: true)); } Future _storeBackupKey(String backupId, String backupKey) async { @@ -31,8 +34,7 @@ class KeychainCubit extends Cubit { await KeyManagementService(keychainapi: keychainapi) .storeBackupKey(backupId, backupKey, state.secret); } catch (e) { - print(e); - emit(state.copyWith(error: 'Server Inaccessible')); + emit(state.copyWith(error: 'Failed to store backup key on server')); } } } From 9e52999c2121d35f6567f137724fa2b394fdb1e3 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 20 Jan 2025 18:41:32 -0500 Subject: [PATCH 071/401] refactor(backup): rename setSelectedBack to setSelectedBackup and remove unused GoogleDriveApi reference --- lib/recover/bloc/manual_cubit.dart | 4 ++-- lib/recover/bloc/manual_state.dart | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/recover/bloc/manual_cubit.dart b/lib/recover/bloc/manual_cubit.dart index dd65d7dad..29d2a6033 100644 --- a/lib/recover/bloc/manual_cubit.dart +++ b/lib/recover/bloc/manual_cubit.dart @@ -59,10 +59,10 @@ class ManualCubit extends Cubit { emit(state.copyWith(error: 'Invalid backup')); return; } - setSelectedBack(id, decodeEncryptedFile); + setSelectedBackup(id, decodeEncryptedFile); } - void setSelectedBack(String id, String encrypted) => + void setSelectedBackup(String id, String encrypted) => emit(state.copyWith(backupId: id, encrypted: encrypted)); Future clickRecover() async { diff --git a/lib/recover/bloc/manual_state.dart b/lib/recover/bloc/manual_state.dart index 171f92474..eea3a7a8c 100644 --- a/lib/recover/bloc/manual_state.dart +++ b/lib/recover/bloc/manual_state.dart @@ -1,4 +1,3 @@ -import 'package:bb_mobile/_pkg/gdrive.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:googleapis/drive/v3.dart'; @@ -9,7 +8,6 @@ class ManualState with _$ManualState { const factory ManualState({ @Default('') String error, @Default(false) bool loading, - GoogleDriveApi? googleDriveApi, @Default([]) List availableBackups, @Default(false) bool recovered, @Default('') String backupKey, From 6bddc6ea3f7e11332257d744326cb65422cd37ae Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 20 Jan 2025 18:42:09 -0500 Subject: [PATCH 072/401] feat(cloud): implement CloudCubit and CloudState for Google Drive backup management --- lib/recover/bloc/cloud_cubit.dart | 140 ++++++++++++++++++++++++++++++ lib/recover/bloc/cloud_state.dart | 20 +++++ 2 files changed, 160 insertions(+) create mode 100644 lib/recover/bloc/cloud_cubit.dart create mode 100644 lib/recover/bloc/cloud_state.dart diff --git a/lib/recover/bloc/cloud_cubit.dart b/lib/recover/bloc/cloud_cubit.dart new file mode 100644 index 000000000..f0c60e2d2 --- /dev/null +++ b/lib/recover/bloc/cloud_cubit.dart @@ -0,0 +1,140 @@ +import 'dart:convert'; + +import 'package:bb_mobile/_pkg/backup/google_drive.dart'; +import 'package:bloc/bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:googleapis/drive/v3.dart'; + +part 'cloud_cubit.freezed.dart'; +part 'cloud_state.dart'; + +class CloudCubit extends Cubit { + final GoogleDriveBackupManager manager; + CloudCubit({required this.manager}) : super(CloudState()); + + void clearToast() => emit(state.copyWith(toast: '', loading: false)); + + void clearError() => emit(state.copyWith(error: '', loading: false)); + + Future connect() async { + try { + emit(state.copyWith(loading: true)); + final (folderId, err) = await manager.connect(); + + if (folderId != null) { + emit( + state.copyWith( + backupFolderId: folderId, + loading: false, + ), + ); + } else if (err != null) { + emit( + state.copyWith( + error: err.message, + loading: false, + ), + ); + } + } catch (e) { + emit( + state.copyWith( + error: "GoogleDrive Error: $e", + loading: false, + ), + ); + } + } + + Future readAllBackups({bool forceRefresh = false}) async { + try { + if (!forceRefresh && + state.availableBackups.isNotEmpty && + state.isCacheValid) { + emit(state.copyWith(loading: false)); + return; + } + if (state.backupFolderId.isEmpty) { + await connect(); + } + emit(state.copyWith(loading: true)); + if (state.backupFolderId.isEmpty) { + emit( + state.copyWith( + loading: false, + error: "Google Drive connection failed.", + ), + ); + return; + } + final (availableBackups, err) = await manager.loadAllEncryptedBackupFiles( + backupFolder: state.backupFolderId, + ); + + if (err != null) { + emit( + state.copyWith( + loading: false, + error: "Failed to list backup files: ${err.message}", + ), + ); + return; + } + + if (availableBackups != null && availableBackups.isNotEmpty) { + emit( + state.copyWith( + loading: false, + availableBackups: availableBackups, + lastFetchTime: DateTime.now(), + ), + ); + } else { + emit(state.copyWith(loading: false, error: "No backup files found")); + } + } catch (e) { + emit( + state.copyWith(loading: false, error: "Failed to read all backups: $e"), + ); + } + } + + void setCacheValidityDuration(Duration duration) { + emit(state.copyWith(cacheValidityDuration: duration)); + } + + Future refreshBackups() => readAllBackups(forceRefresh: true); + + Future loadEncrypted(String fileName) async { + if (state.backupFolderId.isEmpty) await connect(); + emit(state.copyWith(loading: true)); + final metaData = + await manager.fetchMediaStream(file: state.availableBackups[fileName]!); + final (loadEncryptedBackup, err) = await manager.loadEncryptedBackup( + encrypted: utf8.decode(metaData), + ); + if (err != null) { + emit( + state.copyWith( + loading: false, + error: "Failed to read backup: ${err.message}", + ), + ); + return; + } + emit( + state.copyWith( + toast: "Successfully loaded backup", + selectedBackup: ( + loadEncryptedBackup?['backupId'] ?? '', + jsonEncode(loadEncryptedBackup) + ), + ), + ); + } + + void disconnect() { + manager.disconnect(); + emit(state.copyWith(backupFolderId: '')); + } +} diff --git a/lib/recover/bloc/cloud_state.dart b/lib/recover/bloc/cloud_state.dart new file mode 100644 index 000000000..649ba6d7a --- /dev/null +++ b/lib/recover/bloc/cloud_state.dart @@ -0,0 +1,20 @@ +part of 'cloud_cubit.dart'; + +@freezed +class CloudState with _$CloudState { + factory CloudState({ + @Default(false) bool loading, + @Default('') String backupFolderId, + @Default({}) Map availableBackups, + @Default(('', '')) (String, String) selectedBackup, + DateTime? lastFetchTime, + @Default(Duration(minutes: 5)) Duration cacheValidityDuration, + @Default('') String toast, + @Default('') String error, + }) = _CloudState; + const CloudState._(); + bool get isCacheValid { + if (lastFetchTime == null) return false; + return DateTime.now().difference(lastFetchTime!) < cacheValidityDuration; + } +} From e061c5f0a8998b94952e8c8ea8c6ae4f2592851e Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 20 Jan 2025 18:43:27 -0500 Subject: [PATCH 073/401] refactor(cloud): migrate CloudPage from backup to recover, updating UI and state management for backup selection --- lib/backup/cloud_page.dart | 175 ------------------------------------ lib/recover/cloud_page.dart | 165 ++++++++++++++++++++++++++++++++++ 2 files changed, 165 insertions(+), 175 deletions(-) delete mode 100644 lib/backup/cloud_page.dart create mode 100644 lib/recover/cloud_page.dart diff --git a/lib/backup/cloud_page.dart b/lib/backup/cloud_page.dart deleted file mode 100644 index 15115e400..000000000 --- a/lib/backup/cloud_page.dart +++ /dev/null @@ -1,175 +0,0 @@ -import 'package:bb_mobile/_ui/app_bar.dart'; -import 'package:bb_mobile/_ui/components/button.dart'; -import 'package:bb_mobile/_ui/components/text.dart'; -import 'package:bb_mobile/backup/bloc/cloud_cubit.dart'; -import 'package:bb_mobile/backup/bloc/cloud_state.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:gap/gap.dart'; -import 'package:go_router/go_router.dart'; -import 'package:googleapis/drive/v3.dart'; -import 'package:intl/intl.dart'; - -class CloudPage extends StatefulWidget { - final Function(String, String)? onBackupSelected; - const CloudPage({this.onBackupSelected}); - - @override - State createState() => _CloudPageState(); -} - -class _CloudPageState extends State { - @override - void initState() { - final cubit = context.read(); - cubit.readAllBackups(); - super.initState(); - } - - @override - Widget build(BuildContext context) { - return BlocConsumer( - listener: (context, state) { - if (widget.onBackupSelected != null) { - if (state.selectedBackup.$1.isNotEmpty && - state.selectedBackup.$2.isNotEmpty) { - widget.onBackupSelected!( - state.selectedBackup.$1, - state.selectedBackup.$2, - ); - context.pop(); - } - } - - if (state.toast.isNotEmpty && state.toast != '') { - _showSnackBar(context, state.toast, Colors.green); - context.read().clearToast(); - } - if (state.error.isNotEmpty && state.error != '') { - _showSnackBar(context, state.error, Colors.red); - context.read().clearError(); - } - }, - builder: (context, state) { - final cubit = context.read(); - return Scaffold( - appBar: AppBar( - elevation: 0, - automaticallyImplyLeading: false, - flexibleSpace: BBAppBar( - text: 'Cloud Backup', - onBack: () => context.pop(), - ), - ), - body: Center( - child: state.loading - ? const CircularProgressIndicator() - : Column( - children: [ - const Gap(50), - AvailableBackups( - onFileSelected: (file) { - cubit.readCloudBackup(file); - }, - ), - const Gap(10), - if (state.googleDriveApi != null) - BBButton.big( - onPressed: cubit.disconnect, - label: "LOGOUT", - ), - ], - ), - ), - ); - }, - ); - } - - void _showSnackBar(BuildContext context, String message, Color color) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(message), - backgroundColor: color, - ), - ); - } -} - -class AvailableBackups extends StatelessWidget { - const AvailableBackups({super.key, required this.onFileSelected}); - final void Function(File) onFileSelected; - - @override - Widget build(BuildContext context) { - final cubit = context.read(); - return SizedBox( - height: 700, - child: BlocBuilder( - builder: (context, state) { - if (state.availableBackups.isEmpty) { - return Center( - child: Column( - children: [ - const Text('No backups found'), - const Gap(10), - BBButton.big( - onPressed: () { - cubit.clearToast(); - cubit.clearError(); - cubit.readAllBackups(); - }, - label: "READ ALL BACKUPS", - ), - ], - ), - ); - } - return ListView.separated( - // Use ListView.separated for dividers - itemCount: state.availableBackups.length, - shrinkWrap: true, - separatorBuilder: (context, index) => - const Divider(), // Add dividers between items - itemBuilder: (context, index) => BackupTile( - file: state.availableBackups[index], - onFileSelected: onFileSelected, - ), - ); - }, - ), - ); - } -} - -class BackupTile extends StatelessWidget { - const BackupTile({ - super.key, - required this.file, - required this.onFileSelected, - }); - final File file; - final void Function(File) onFileSelected; // Use void Function for clarity - - @override - Widget build(BuildContext context) { - final fileName = file.name?.replaceAll(".json", ""); - final parts = fileName?.split('_'); - final backupId = parts?.last; - final dateTimeString = parts?.first; - final dateTime = - DateTime.fromMillisecondsSinceEpoch(int.parse(dateTimeString!)); - - final formattedDate = DateFormat('yyyy-MM-dd HH:mm').format(dateTime); - return ListTile( - onTap: () => onFileSelected(file), - title: BBText.body( - backupId ?? 'Unnamed File', - isBold: true, - ), - subtitle: BBText.bodySmall( - formattedDate, - ), - ); - } -} diff --git a/lib/recover/cloud_page.dart b/lib/recover/cloud_page.dart new file mode 100644 index 000000000..f148a59c1 --- /dev/null +++ b/lib/recover/cloud_page.dart @@ -0,0 +1,165 @@ +import 'package:bb_mobile/_ui/app_bar.dart'; +import 'package:bb_mobile/_ui/components/button.dart'; +import 'package:bb_mobile/_ui/components/text.dart'; +import 'package:bb_mobile/_ui/toast.dart'; +import 'package:bb_mobile/recover/bloc/cloud_cubit.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:gap/gap.dart'; +import 'package:go_router/go_router.dart'; +import 'package:intl/intl.dart'; + +class CloudPage extends StatefulWidget { + final Function(String)? onBackupSelected; + const CloudPage({this.onBackupSelected}); + @override + State createState() => _CloudPageState(); +} + +class _CloudPageState extends State { + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + context.read().readAllBackups(); + } + }); + } + + Widget buildBackupsList(CloudState state) { + if (state.loading) { + return const Center( + child: CircularProgressIndicator(), + ); + } + if (state.availableBackups.isEmpty) { + return Center( + child: BBButton.text( + onPressed: () => context.read().refreshBackups(), + label: 'Refresh', + ), + ); + } + + final backupEntries = state.availableBackups.entries.toList() + ..sort((a, b) => b.key.compareTo(a.key)); + + return Column( + children: [ + if (state.lastFetchTime != null) ...[ + Padding( + padding: const EdgeInsets.all(8.0), + child: BBText.bodySmall( + 'Last updated: ${DateFormat('MMM d, h:mm a').format(state.lastFetchTime!)}', + isBold: true, + ), + ), + ], + Expanded( + child: RefreshIndicator( + onRefresh: () => context.read().refreshBackups(), + child: ListView.separated( + itemCount: backupEntries.length, + padding: const EdgeInsets.symmetric(horizontal: 16), + separatorBuilder: (_, __) => const Divider(), + itemBuilder: (context, index) { + final entry = backupEntries[index]; + return BackupTile( + fileName: entry.key, + onFileSelected: (fileName) { + widget.onBackupSelected!(fileName); + context.pop(); + }, + ); + }, + ), + ), + ), + ], + ); + } + + @override + Widget build(BuildContext context) { + return BlocConsumer( + listener: (context, state) { + if (state.toast.isNotEmpty && state.toast != '') { + ScaffoldMessenger.of(context) + .showSnackBar(context.showToast(state.toast)); + context.read().clearToast(); + } + if (state.error.isNotEmpty && state.error != '') { + ScaffoldMessenger.of(context) + .showSnackBar(context.showToast(state.error)); + + context.read().clearError(); + } + }, + builder: (context, state) { + return Scaffold( + appBar: AppBar( + elevation: 0, + automaticallyImplyLeading: false, + flexibleSpace: BBAppBar( + text: 'Cloud Backup', + onBack: () => context.pop(), + ), + ), + body: Center( + child: state.loading + ? const CircularProgressIndicator() + : Column( + children: [ + Expanded( + child: buildBackupsList(state), + ), + const Gap(10), + BBButton.big( + onPressed: () { + context.read().disconnect(); + context.pop(); + }, + label: "Logout", + ), + const Gap(20), + ], + ), + ), + ); + }, + ); + } +} + +class BackupTile extends StatelessWidget { + const BackupTile({ + super.key, + required this.fileName, + required this.onFileSelected, + }); + + final String fileName; + final void Function(String) onFileSelected; + + @override + Widget build(BuildContext context) { + final cleanFileName = fileName.replaceAll(".json", ""); + final parts = cleanFileName.split('_'); + final backupId = parts.last; + final dateTimeString = parts.first; + final dateTime = + DateTime.fromMillisecondsSinceEpoch(int.parse(dateTimeString)); + + return ListTile( + onTap: () => onFileSelected(fileName), + title: BBText.body( + backupId, + isBold: true, + ), + subtitle: BBText.bodySmall( + 'Created at: ${DateFormat('MMM d, h:mm a').format(dateTime)}', + ), + ); + } +} From 03f65661f24fd2c623205b7fed0f9b5e080b9480 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 20 Jan 2025 18:44:16 -0500 Subject: [PATCH 074/401] refactor(keychain): replace SnackBar with custom toast for error and success --- lib/backup/keychain_page.dart | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/lib/backup/keychain_page.dart b/lib/backup/keychain_page.dart index 95a76ed1b..330a01ce0 100644 --- a/lib/backup/keychain_page.dart +++ b/lib/backup/keychain_page.dart @@ -1,4 +1,5 @@ import 'package:bb_mobile/_ui/app_bar.dart'; +import 'package:bb_mobile/_ui/toast.dart'; import 'package:bb_mobile/backup/bloc/keychain_cubit.dart'; import 'package:bb_mobile/backup/bloc/keychain_state.dart'; import 'package:flutter/material.dart'; @@ -33,22 +34,15 @@ class KeychainBackupPage extends StatelessWidget { body: BlocListener( listener: (context, state) { if (state.error.isNotEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(state.error), - backgroundColor: Colors.red, - ), - ); + ScaffoldMessenger.of(context) + .showSnackBar(context.showToast(state.error)); context.read().clearError(); } if (state.completed) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Keychain completed'), - backgroundColor: Colors.green, - ), + context.showToast('Backup key saved to keychain successfully'), ); - context.go('/home'); + context.pop(); } }, child: BlocBuilder( From 0f8d2fe6e6528d778516d21088dfca2ef81c3b99 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 20 Jan 2025 18:47:21 -0500 Subject: [PATCH 075/401] refactor(backup): replace BackupCubit with ManualCubit, update state management --- lib/backup/backup_page.dart | 201 ++++++++++++++---------------------- 1 file changed, 79 insertions(+), 122 deletions(-) diff --git a/lib/backup/backup_page.dart b/lib/backup/backup_page.dart index 4de8e7936..7f783634d 100644 --- a/lib/backup/backup_page.dart +++ b/lib/backup/backup_page.dart @@ -1,14 +1,16 @@ import 'package:bb_mobile/_model/wallet.dart'; -import 'package:bb_mobile/_pkg/file_storage.dart'; +import 'package:bb_mobile/_pkg/backup/google_drive.dart'; +import 'package:bb_mobile/_pkg/backup/local.dart'; import 'package:bb_mobile/_repository/wallet/sensitive_wallet_storage.dart'; import 'package:bb_mobile/_ui/app_bar.dart'; import 'package:bb_mobile/_ui/components/button.dart'; import 'package:bb_mobile/_ui/components/controls.dart'; import 'package:bb_mobile/_ui/components/text.dart'; -import 'package:bb_mobile/backup/bloc/backup_cubit.dart'; -import 'package:bb_mobile/backup/bloc/backup_state.dart'; +import 'package:bb_mobile/_ui/toast.dart'; import 'package:bb_mobile/backup/bloc/cloud_cubit.dart'; import 'package:bb_mobile/backup/bloc/cloud_state.dart'; +import 'package:bb_mobile/backup/bloc/manual_cubit.dart'; +import 'package:bb_mobile/backup/bloc/manual_state.dart'; import 'package:bb_mobile/locator.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -25,14 +27,29 @@ class ManualBackupPage extends StatefulWidget { } class _TheBackupPageState extends State { + late final ManualCubit _backupCubit; + + @override + void initState() { + super.initState(); + _backupCubit = ManualCubit( + wallets: widget.wallets, + walletSensitiveStorage: locator(), + manager: locator(), + ); + } + + @override + void dispose() { + _backupCubit.clearAndClose(); + //TODO: clear cloud cubit + super.dispose(); + } + @override Widget build(BuildContext context) { - return BlocProvider( - create: (_) => BackupCubit( - wallets: widget.wallets, - walletSensitiveStorage: locator(), - fileStorage: locator(), - )..loadConfirmedBackups(), + return BlocProvider.value( + value: _backupCubit, child: Scaffold( backgroundColor: Colors.white, appBar: AppBar( @@ -40,30 +57,24 @@ class _TheBackupPageState extends State { elevation: 0, flexibleSpace: BBAppBar( text: 'Backup', - onBack: () => context.pop(), + onBack: () => Navigator.of(context).pop(), ), ), - body: BlocListener( + body: BlocListener( listener: (context, state) { if (state.error.isNotEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(state.error), - backgroundColor: Colors.red, - ), - ); - context.read().clearError(); + ScaffoldMessenger.of(context) + .showSnackBar(context.showToast(state.error)); + + context.read().clearError(); } if (state.backupId.isNotEmpty && state.backupKey.isNotEmpty) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Backup completed'), - backgroundColor: Colors.green, - ), + context.showToast('Backup created successfully'), ); } }, - child: BlocBuilder( + child: BlocBuilder( builder: (context, state) { return state.loading ? const Center(child: CircularProgressIndicator()) @@ -74,33 +85,42 @@ class _TheBackupPageState extends State { children: [ BackupToggleItem( title: 'Mnemonics & Passwords', - value: state.confirmedBackups['mnemonic'] ?? false, + value: state.selectedBackupOptions['mnemonic'] ?? + false, onChanged: () { context - .read() + .read() .toggleAllMnemonicAndPassphrase(); }, ), const Gap(8), BackupToggleItem( title: 'Descriptors', - value: - state.confirmedBackups['descriptors'] ?? false, + value: state.selectedBackupOptions['descriptors'] ?? + false, onChanged: () { - context.read().toggleDescriptors(); + context.read().toggleDescriptors(); }, ), const Gap(8), BackupToggleItem( title: 'Labels', - value: state.confirmedBackups['labels'] ?? false, + value: + state.selectedBackupOptions['labels'] ?? false, onChanged: () { - context.read().toggleLabels(); + context.read().toggleLabels(); }, ), const Gap(8), if (state.backupKey.isEmpty) - Center(child: _GenerateBackupButton()), + Center( + child: BBButton.big( + onPressed: () => context + .read() + .saveEncryptedBackup(), + label: "Generate Backup", + ), + ), const Gap(20), if (state.backupKey.isNotEmpty) Column( @@ -121,43 +141,33 @@ class _TheBackupPageState extends State { '/keychain-backup', extra: (state.backupKey, state.backupId), ), - label: 'SAVE TO KEYCHAIN', + label: 'Save to Keychain', ), ], ), const Gap(50), if (state.backupPath.isNotEmpty) BlocProvider( - create: (context) => CloudCubit(), + create: (context) => CloudCubit( + manager: locator(), + ), child: Center( child: BlocConsumer( listener: (context, cloudState) { + if (cloudState.toast != '') { + ScaffoldMessenger.of(context) + .showSnackBar( + context.showToast(cloudState.toast), + ); + } else { + ScaffoldMessenger.of(context) + .showSnackBar( + context.showToast(cloudState.error), + ); + } if (!cloudState.loading) { - if (cloudState.error != '') { - ScaffoldMessenger.of(context) - .showSnackBar( - SnackBar( - content: Text( - cloudState.error, - textAlign: TextAlign.center, - ), - backgroundColor: Colors.red, - ), - ); - context.read().clearError(); - } else { - ScaffoldMessenger.of(context) - .showSnackBar( - SnackBar( - content: Text( - cloudState.toast, - textAlign: TextAlign.center, - ), - backgroundColor: Colors.green, - ), - ); - context.read().clearToast(); - } + context.read().clearToast(); + context.read().clearError(); } }, buildWhen: (p, q) => p.loading != q.loading, @@ -166,17 +176,17 @@ class _TheBackupPageState extends State { loading: cloudState.loading, onPressed: () { context.read().uploadBackup( - state.backupPath, - state.backupName, + fileSystemBackupPath: + state.backupPath, ); - context.push( - '/cloud-backup', - extra: { - 'cubit': context.read(), - }, - ); + // context.push( + // '/cloud-backup', + // extra: { + // 'cubit': context.read(), + // }, + // ); }, - label: "SAVE TO GOOGLE DRIVE", + label: "Save to Google Drive", ); }, ), @@ -207,9 +217,9 @@ class BackupToggleItem extends StatelessWidget { @override Widget build(BuildContext context) { - return BlocListener( + return BlocListener( listenWhen: (previous, current) => - previous.confirmedBackups != current.confirmedBackups, + previous.selectedBackupOptions != current.selectedBackupOptions, listener: (context, state) {}, child: Row( children: [ @@ -229,56 +239,3 @@ class BackupToggleItem extends StatelessWidget { ); } } - -class _GenerateBackupButton extends StatelessWidget { - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - final mnemonicConfirmed = state.confirmedBackups['mnemonic'] ?? false; - - return Center( - child: BBButton.big( - onPressed: () { - if (!mnemonicConfirmed) { - _showConfirmDialog(context); - } else { - context.read().writeEncryptedBackup(); - } - }, - label: "GENERATE BACKUP", - ), - ); - }, - ); - } -} - -void _showConfirmDialog(BuildContext context) { - showDialog( - context: context, - builder: (BuildContext dialogContext) { - return AlertDialog( - title: const BBText.body('Confirm New Mnemonic'), - content: const BBText.bodySmall( - 'You have not confirmed your mnemonic. Generating a backup now will create a new mnemonic for the backup key. Are you sure you want to proceed?', - ), - actions: [ - TextButton( - child: const Text('Cancel'), - onPressed: () { - Navigator.of(dialogContext).pop(); - }, - ), - TextButton( - child: const Text('Confirm'), - onPressed: () { - Navigator.of(dialogContext).pop(); - context.read().writeEncryptedBackup(); - }, - ), - ], - ); - }, - ); -} From 1ab096251e28d72772cad93ff2cef1f5dcfa2ebb Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 20 Jan 2025 18:48:16 -0500 Subject: [PATCH 076/401] refactor(recover): integrate CloudCubit, replace SnackBar with custom toast. --- lib/recover/manual_page.dart | 70 ++++++++++++++++++++++++------------ 1 file changed, 48 insertions(+), 22 deletions(-) diff --git a/lib/recover/manual_page.dart b/lib/recover/manual_page.dart index 677d7a9c2..7e7c21957 100644 --- a/lib/recover/manual_page.dart +++ b/lib/recover/manual_page.dart @@ -1,4 +1,5 @@ import 'package:bb_mobile/_model/wallet.dart'; +import 'package:bb_mobile/_pkg/backup/google_drive.dart'; import 'package:bb_mobile/_pkg/consts/keys.dart'; import 'package:bb_mobile/_pkg/file_picker.dart'; import 'package:bb_mobile/_pkg/wallet/bdk/sensitive_create.dart'; @@ -9,8 +10,9 @@ import 'package:bb_mobile/_repository/wallet/sensitive_wallet_storage.dart'; import 'package:bb_mobile/_repository/wallet/wallet_storage.dart'; import 'package:bb_mobile/_ui/app_bar.dart'; import 'package:bb_mobile/_ui/components/button.dart'; -import 'package:bb_mobile/backup/bloc/cloud_cubit.dart'; +import 'package:bb_mobile/_ui/toast.dart'; import 'package:bb_mobile/locator.dart'; +import 'package:bb_mobile/recover/bloc/cloud_cubit.dart'; import 'package:bb_mobile/recover/bloc/manual_cubit.dart'; import 'package:bb_mobile/recover/bloc/manual_state.dart'; import 'package:flutter/cupertino.dart'; @@ -52,19 +54,13 @@ class ManualRecoverPage extends StatelessWidget { listener: (context, state) async { if (state.error.isNotEmpty) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(state.error), - backgroundColor: Colors.red, - ), + context.showToast(state.error), ); context.read().clearError(); } if (state.recovered) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Recovery completed'), - backgroundColor: Colors.green, - ), + context.showToast('Recovery completed'), ); context.go('/home'); } @@ -104,19 +100,49 @@ class ManualRecoverPage extends StatelessWidget { onPressed: () => cubit.selectFileFromFs(), ), const Gap(20), - BBButton.big( - label: 'Select file from Cloud', - center: true, - onPressed: () => { - context.push( - '/cloud-backup', - extra: { - 'cubit': CloudCubit(), - 'callback': (String id, String encrypted) => - cubit.setSelectedBack(id, encrypted), - }, - ), - }, + BlocProvider( + create: (context) => CloudCubit( + manager: locator(), + ), + child: BlocConsumer( + listener: (context, state) { + if (state.error.isNotEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + context.showToast(state.error), + ); + context.read().clearError(); + } + if (state.toast.isNotEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + context.showToast(state.toast), + ); + cubit.setSelectedBackup( + state.selectedBackup.$1, + state.selectedBackup.$2, + ); + } + }, + builder: (context, state) { + return BBButton.big( + loading: state.loading, + label: 'Select file from Cloud', + center: true, + onPressed: () => { + context.push( + '/cloud-backup', + extra: { + 'callback': (String fileName) { + context + .read() + .loadEncrypted(fileName); + }, + 'cubit': context.read(), + }, + ), + }, + ); + }, + ), ), ], ), From 3a53a501897cccd511e2eeae67ff894b33c8967d Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 20 Jan 2025 18:56:38 -0500 Subject: [PATCH 077/401] feat(wallet): add BIP85 derivation support and implement next path generation logic --- lib/_model/wallet.dart | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/lib/_model/wallet.dart b/lib/_model/wallet.dart index 3dccfb469..4a426da12 100644 --- a/lib/_model/wallet.dart +++ b/lib/_model/wallet.dart @@ -61,6 +61,7 @@ class Wallet with _$Wallet { @Default('') String externalPublicDescriptor, @Default('') String internalPublicDescriptor, // public + @Default({}) Map bip85Derivations, @Default('') String mnemonicFingerprint, @Default('') String sourceFingerprint, required BBNetwork network, @@ -202,6 +203,19 @@ class Wallet with _$Wallet { return exDescDerivedKey; } +//TODO: This is a basic implementation - adjust the logic based on your needs + String generateNextBIP85Path() { + int highestIndex = -1; + for (final path in bip85Derivations.keys) { + if (path.startsWith("m/1608'/")) { + final index = + int.tryParse(path.split("'")[1].replaceAll("'", "")) ?? -1; + if (index > highestIndex) highestIndex = index; + } + } + return "m/1608'/${highestIndex + 1}'"; + } + // storage key String getRelatedSeedStorageString() { // TODO: Sai: Uncomment this (or) add :testnet while saving testnet seed (later) @@ -546,6 +560,19 @@ class Wallet with _$Wallet { String balanceStr() => ((balance ?? 0) / 100000000).toStringAsFixed(8); } +enum BIP85DerivationStatus { active, deprecated } + +@freezed +class BIP85Derivation with _$BIP85Derivation { + const factory BIP85Derivation({ + required String label, + @Default(BIP85DerivationStatus.active) BIP85DerivationStatus status, + }) = _BIP85Derivation; + + factory BIP85Derivation.fromJson(Map json) => + _$BIP85DerivationFromJson(json); +} + @freezed class Balance with _$Balance { const factory Balance({ From e6b0668712530be5da936ec84a8837eca0f31dd1 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 20 Jan 2025 18:58:51 -0500 Subject: [PATCH 078/401] feat(wallet): add support for BIP85 paths in wallet update process --- lib/_repository/wallet_service.dart | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/_repository/wallet_service.dart b/lib/_repository/wallet_service.dart index 50a4a149f..d13ad84ca 100644 --- a/lib/_repository/wallet_service.dart +++ b/lib/_repository/wallet_service.dart @@ -279,6 +279,10 @@ class WalletService { lastBackupTested: wallet.lastBackupTested, ); } + case UpdateWalletTypes.bip85Paths: + storageWallet = storageWallet!.copyWith( + bip85Derivations: wallet.bip85Derivations, + ); } final err = await _walletsStorageRepository.updateWallet( @@ -387,5 +391,6 @@ enum UpdateWalletTypes { swaps, addresses, settings, - utxos + utxos, + bip85Paths, } From 7c13621727d788e13aa8602c45a7f5e4a199d22f Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 20 Jan 2025 18:58:59 -0500 Subject: [PATCH 079/401] feat(wallet): implement updateBIP85Paths method to manage BIP85 derivations in wallet --- lib/_pkg/wallet/update.dart | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/lib/_pkg/wallet/update.dart b/lib/_pkg/wallet/update.dart index bc3b4e507..2587fa125 100644 --- a/lib/_pkg/wallet/update.dart +++ b/lib/_pkg/wallet/update.dart @@ -24,6 +24,33 @@ class WalletUpdate { } } + Wallet updateBIP85Paths( + Wallet wallet, + String bip85Path, + String label, + ) { + final updatedDerivations = + Map.from(wallet.bip85Derivations); + + // Mark any existing active derivation as deprecated + final activeBIP85Derivation = updatedDerivations.entries + .where((e) => e.value.status == BIP85DerivationStatus.active) + .firstOrNull; + + if (activeBIP85Derivation != null) { + updatedDerivations[activeBIP85Derivation.key] = BIP85Derivation( + label: activeBIP85Derivation.value.label, + status: BIP85DerivationStatus.deprecated, + ); + } + updatedDerivations[bip85Path] = BIP85Derivation( + label: label, + ); + return wallet.copyWith( + bip85Derivations: updatedDerivations, + ); + } + Future<(Wallet?, Err?)> updateAddressLabels( Wallet wallet, List
addresses, From fed06de4a940b2f7a2b1123d3bb20bd37acd877d Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 20 Jan 2025 19:00:27 -0500 Subject: [PATCH 080/401] feat(wallet): add method to handle new BIP85 backup key creation --- .../bloc/wallet_settings_cubit.dart | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/lib/wallet_settings/bloc/wallet_settings_cubit.dart b/lib/wallet_settings/bloc/wallet_settings_cubit.dart index 67ce583f5..0afb86ce6 100644 --- a/lib/wallet_settings/bloc/wallet_settings_cubit.dart +++ b/lib/wallet_settings/bloc/wallet_settings_cubit.dart @@ -491,6 +491,36 @@ class WalletSettingsCubit extends Cubit { } } + Future addNewBIP85BackupKey(String label) async { + try { + emit(state.copyWith(savingFile: true, errSavingFile: '')); + final wallet = _wallet; + final newBIP85Path = wallet.generateNextBIP85Path(); + + //TODO; find a way to get the label from the wallet + final updatedWallet = WalletUpdate().updateBIP85Paths( + wallet, + newBIP85Path, + "${label}_bip85Path_${wallet.bip85Derivations.entries.length + 1}", + ); + + await _appWalletsRepository + .getWalletServiceById(updatedWallet.id) + ?.updateWallet( + updatedWallet, + updateTypes: [ + UpdateWalletTypes.bip85Paths, + ], + ); + + emit(state.copyWith(savingFile: false, savedFile: true)); + await Future.delayed(2.seconds); + emit(state.copyWith(savedFile: false)); + } catch (e) { + emit(state.copyWith(errImporting: e.toString(), importing: false)); + } + } + Future clearSensitive() async { clearnMnemonic(); emit( From 5615c574c32cca33928233a3f236339351dd8882 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 20 Jan 2025 19:00:52 -0500 Subject: [PATCH 081/401] fix(wallet): update error handling --- lib/wallet_settings/bloc/wallet_settings_cubit.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/wallet_settings/bloc/wallet_settings_cubit.dart b/lib/wallet_settings/bloc/wallet_settings_cubit.dart index 0afb86ce6..32bc4a92a 100644 --- a/lib/wallet_settings/bloc/wallet_settings_cubit.dart +++ b/lib/wallet_settings/bloc/wallet_settings_cubit.dart @@ -517,7 +517,7 @@ class WalletSettingsCubit extends Cubit { await Future.delayed(2.seconds); emit(state.copyWith(savedFile: false)); } catch (e) { - emit(state.copyWith(errImporting: e.toString(), importing: false)); + emit(state.copyWith(errSavingFile: e.toString(), savingFile: false)); } } From 9cee5f7375593ee3e139e928a24fadfcf1d523b4 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 20 Jan 2025 19:33:01 -0500 Subject: [PATCH 082/401] code cleanup --- pubspec.lock | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 171ce20a1..9b0a9a582 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -86,14 +86,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" - bip39: + bip39_mnemonic: dependency: transitive description: - name: bip39 - sha256: de1ee27ebe7d96b84bb3a04a4132a0a3007dcdd5ad27dd14aa87a29d97c45edc + name: bip39_mnemonic + sha256: "3ae6ed74b97a0b820e71d01b75ac4bc5b036a8bb427d5ee5827427d2872eefb0" url: "https://pub.dev" source: hosted - version: "1.0.6" + version: "3.0.7" bip85: dependency: "direct main" description: @@ -1340,7 +1340,7 @@ packages: description: path: "." ref: HEAD - resolved-ref: c49577fdf1c21c6fcded217dfc76c34e3b5520f2 + resolved-ref: "498509da2a2376b1534ee49f1ca65b3d14809e59" url: "https://github.com/StaxoLotl/recoverbull-dart.git" source: git version: "0.0.1" @@ -1565,6 +1565,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.2" + unorm_dart: + dependency: transitive + description: + name: unorm_dart + sha256: "23d8bf65605401a6a32cff99435fed66ef3dab3ddcad3454059165df46496a3b" + url: "https://pub.dev" + source: hosted + version: "0.3.0" url_launcher: dependency: "direct main" description: From aaa19f2388e09705a167ba2c7fd2b8720bdadd8b Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Tue, 21 Jan 2025 14:07:16 -0500 Subject: [PATCH 083/401] fix(WalletService): optimize BIP85 derivation update logic to prevent unnecessary state changes --- lib/_repository/wallet_service.dart | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/_repository/wallet_service.dart b/lib/_repository/wallet_service.dart index d13ad84ca..3cf491703 100644 --- a/lib/_repository/wallet_service.dart +++ b/lib/_repository/wallet_service.dart @@ -280,9 +280,11 @@ class WalletService { ); } case UpdateWalletTypes.bip85Paths: - storageWallet = storageWallet!.copyWith( - bip85Derivations: wallet.bip85Derivations, - ); + if (wallet.bip85Derivations != storageWallet!.bip85Derivations) { + storageWallet = storageWallet.copyWith( + bip85Derivations: wallet.bip85Derivations, + ); + } } final err = await _walletsStorageRepository.updateWallet( From 1bf690a4e6bab96a6adb82ae12f4050f515e3e17 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Tue, 21 Jan 2025 14:10:50 -0500 Subject: [PATCH 084/401] fix(wallet): update BIP85DerivationStatus from 'deprecated' to 'revoked' --- lib/_model/wallet.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/_model/wallet.dart b/lib/_model/wallet.dart index 4a426da12..88f8bd7cf 100644 --- a/lib/_model/wallet.dart +++ b/lib/_model/wallet.dart @@ -560,7 +560,7 @@ class Wallet with _$Wallet { String balanceStr() => ((balance ?? 0) / 100000000).toStringAsFixed(8); } -enum BIP85DerivationStatus { active, deprecated } +enum BIP85DerivationStatus { active, revoked } @freezed class BIP85Derivation with _$BIP85Derivation { From f45320411f866b25459d5ce77b9fb52c2d4dab6e Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Tue, 21 Jan 2025 14:15:38 -0500 Subject: [PATCH 085/401] fix(wallet): enhance BIP85 derivation update logic to handle label updates --- lib/_pkg/wallet/update.dart | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/_pkg/wallet/update.dart b/lib/_pkg/wallet/update.dart index 2587fa125..7ad20b633 100644 --- a/lib/_pkg/wallet/update.dart +++ b/lib/_pkg/wallet/update.dart @@ -31,8 +31,14 @@ class WalletUpdate { ) { final updatedDerivations = Map.from(wallet.bip85Derivations); + final existingDerivation = updatedDerivations[bip85Path]; + if (existingDerivation != null) { + updatedDerivations[bip85Path] = existingDerivation.copyWith( + label: label, + ); + return wallet.copyWith(bip85Derivations: updatedDerivations); + } - // Mark any existing active derivation as deprecated final activeBIP85Derivation = updatedDerivations.entries .where((e) => e.value.status == BIP85DerivationStatus.active) .firstOrNull; @@ -40,7 +46,7 @@ class WalletUpdate { if (activeBIP85Derivation != null) { updatedDerivations[activeBIP85Derivation.key] = BIP85Derivation( label: activeBIP85Derivation.value.label, - status: BIP85DerivationStatus.deprecated, + status: BIP85DerivationStatus.revoked, ); } updatedDerivations[bip85Path] = BIP85Derivation( From f7ed6643903ffa3a6875ec584b66833fcb3519fc Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Tue, 21 Jan 2025 14:20:16 -0500 Subject: [PATCH 086/401] refactor(recover): removed callback from the route --- lib/recover/cloud_page.dart | 5 ++--- lib/recover/manual_page.dart | 9 +-------- lib/routes.dart | 9 ++------- 3 files changed, 5 insertions(+), 18 deletions(-) diff --git a/lib/recover/cloud_page.dart b/lib/recover/cloud_page.dart index f148a59c1..093bf3ee5 100644 --- a/lib/recover/cloud_page.dart +++ b/lib/recover/cloud_page.dart @@ -10,8 +10,7 @@ import 'package:go_router/go_router.dart'; import 'package:intl/intl.dart'; class CloudPage extends StatefulWidget { - final Function(String)? onBackupSelected; - const CloudPage({this.onBackupSelected}); + const CloudPage({super.key}); @override State createState() => _CloudPageState(); } @@ -68,7 +67,7 @@ class _CloudPageState extends State { return BackupTile( fileName: entry.key, onFileSelected: (fileName) { - widget.onBackupSelected!(fileName); + context.read().loadEncrypted(fileName); context.pop(); }, ); diff --git a/lib/recover/manual_page.dart b/lib/recover/manual_page.dart index 7e7c21957..b850620e3 100644 --- a/lib/recover/manual_page.dart +++ b/lib/recover/manual_page.dart @@ -130,14 +130,7 @@ class ManualRecoverPage extends StatelessWidget { onPressed: () => { context.push( '/cloud-backup', - extra: { - 'callback': (String fileName) { - context - .read() - .loadEncrypted(fileName); - }, - 'cubit': context.read(), - }, + extra: context.read(), ), }, ); diff --git a/lib/routes.dart b/lib/routes.dart index f5ddee211..904aac822 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -224,16 +224,11 @@ GoRouter setupRouter() => GoRouter( GoRoute( path: '/cloud-backup', builder: (context, state) { - final data = state.extra! as Map; - final cloudCubit = data['cubit'] as CloudCubit; - Function(String)? onBackupSelected; - if (data['callback'] != null) { - onBackupSelected = data['callback']! as Function(String)?; - } + final cloudCubit = state.extra! as CloudCubit; return BlocProvider.value( value: cloudCubit, - child: CloudPage(onBackupSelected: onBackupSelected), + child: const CloudPage(), ); }, ), From b4395eb42afc7f66c2a626804fce7ea4ea86b799 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Tue, 21 Jan 2025 16:57:04 -0500 Subject: [PATCH 087/401] feat(backup): implement backup throttling and updated CloudState to include lastBackupAttempt --- lib/backup/bloc/cloud_cubit.dart | 19 ++++++++++++++++++- lib/backup/bloc/cloud_state.dart | 1 + 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/lib/backup/bloc/cloud_cubit.dart b/lib/backup/bloc/cloud_cubit.dart index e27744562..c94b5649d 100644 --- a/lib/backup/bloc/cloud_cubit.dart +++ b/lib/backup/bloc/cloud_cubit.dart @@ -46,8 +46,25 @@ class CloudCubit extends Cubit { Future uploadBackup({ required String fileSystemBackupPath, }) async { + if (state.loading) { + emit(state.copyWith(error: 'Backup already in progress')); + return; + } + final now = DateTime.now(); + if (state.lastBackupAttempt != null) { + final difference = now.difference(state.lastBackupAttempt!); + if (difference.inSeconds < 30) { + emit( + state.copyWith( + error: + 'Please wait ${30 - difference.inSeconds} seconds before creating another backup', + ), + ); + return; + } + } if (state.backupFolderId.isEmpty) await connect(); - emit(state.copyWith(loading: true)); + emit(state.copyWith(loading: true, lastBackupAttempt: now)); try { final backup = io.File(fileSystemBackupPath); diff --git a/lib/backup/bloc/cloud_state.dart b/lib/backup/bloc/cloud_state.dart index 26baa48e6..41b20250e 100644 --- a/lib/backup/bloc/cloud_state.dart +++ b/lib/backup/bloc/cloud_state.dart @@ -7,6 +7,7 @@ class CloudState with _$CloudState { const factory CloudState({ @Default(false) bool loading, @Default('') String backupFolderId, + @Default(null) DateTime? lastBackupAttempt, @Default('') String toast, @Default('') String error, }) = _CloudState; From 2efbbd057e7f78b98d293c57af4bd31502842939 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Tue, 21 Jan 2025 16:58:03 -0500 Subject: [PATCH 088/401] feat(manual backup): add backup throttling to prevent multiple concurrent backups. --- lib/backup/bloc/manual_cubit.dart | 19 ++++++++++++++++++- lib/backup/bloc/manual_state.dart | 3 +-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/lib/backup/bloc/manual_cubit.dart b/lib/backup/bloc/manual_cubit.dart index 4bed5de78..3f8d063ff 100644 --- a/lib/backup/bloc/manual_cubit.dart +++ b/lib/backup/bloc/manual_cubit.dart @@ -125,7 +125,24 @@ class ManualCubit extends Cubit { } Future saveEncryptedBackup() async { - emit(state.copyWith(loading: true, error: '')); + if (state.loading) { + emit(state.copyWith(error: 'Backup already in progress')); + return; + } + final now = DateTime.now(); + if (state.lastBackupAttempt != null) { + final difference = now.difference(state.lastBackupAttempt!); + if (difference.inSeconds < 30) { + emit( + state.copyWith( + error: + 'Please wait ${30 - difference.inSeconds} seconds before creating another backup', + ), + ); + return; + } + } + emit(state.copyWith(loading: true, lastBackupAttempt: now)); await loadBackupData(); final backups = state.loadedBackups; diff --git a/lib/backup/bloc/manual_state.dart b/lib/backup/bloc/manual_state.dart index 5a650af72..e45748643 100644 --- a/lib/backup/bloc/manual_state.dart +++ b/lib/backup/bloc/manual_state.dart @@ -21,8 +21,7 @@ class ManualState with _$ManualState { @Default('') String backupPath, @Default('') String backupName, @Default('') String backupKey, - // // To avoid multiple backups being created when the user clicks the button multiple times - // @Default(false) bool isBackupSaved, + @Default(null) DateTime? lastBackupAttempt, @Default('') String error, }) = _ManualState; } From 4f7fdf04dcdd5f07d5ee129eaca06029cf3a72ad Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Tue, 21 Jan 2025 16:58:31 -0500 Subject: [PATCH 089/401] fix(cloud_cubit): update loading state to false upon successful backup load --- lib/recover/bloc/cloud_cubit.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/recover/bloc/cloud_cubit.dart b/lib/recover/bloc/cloud_cubit.dart index f0c60e2d2..b383fda39 100644 --- a/lib/recover/bloc/cloud_cubit.dart +++ b/lib/recover/bloc/cloud_cubit.dart @@ -125,6 +125,7 @@ class CloudCubit extends Cubit { emit( state.copyWith( toast: "Successfully loaded backup", + loading: false, selectedBackup: ( loadEncryptedBackup?['backupId'] ?? '', jsonEncode(loadEncryptedBackup) From 4bf0e3e7ab29deb4376fdf19b26f591e40e0bc7a Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Tue, 21 Jan 2025 21:38:05 -0500 Subject: [PATCH 090/401] fix(wallet):invalid highestIndex in generateNextBIP85Path --- lib/_model/wallet.dart | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/lib/_model/wallet.dart b/lib/_model/wallet.dart index 88f8bd7cf..4a65175e4 100644 --- a/lib/_model/wallet.dart +++ b/lib/_model/wallet.dart @@ -203,16 +203,15 @@ class Wallet with _$Wallet { return exDescDerivedKey; } -//TODO: This is a basic implementation - adjust the logic based on your needs String generateNextBIP85Path() { - int highestIndex = -1; - for (final path in bip85Derivations.keys) { - if (path.startsWith("m/1608'/")) { - final index = - int.tryParse(path.split("'")[1].replaceAll("'", "")) ?? -1; - if (index > highestIndex) highestIndex = index; - } - } + final highestIndex = bip85Derivations.keys + .where((path) => path.startsWith("m/1608'/")) + .map( + (path) => + int.tryParse(path.split('/').last.replaceAll("'", "")) ?? -1, + ) + .fold(-1, (max, index) => index > max ? index : max); + return "m/1608'/${highestIndex + 1}'"; } From 9dcc9c4a0d4ea3aeb1a91222ea612eeae3901eb8 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Tue, 21 Jan 2025 21:45:37 -0500 Subject: [PATCH 091/401] feat(wallet_settings): Implemented methods to create, load and update BIP85 derivation paths --- lib/wallet_settings/bloc/state.dart | 4 + .../bloc/wallet_settings_cubit.dart | 90 +++++++++++++++---- 2 files changed, 75 insertions(+), 19 deletions(-) diff --git a/lib/wallet_settings/bloc/state.dart b/lib/wallet_settings/bloc/state.dart index d87946bb3..98d1cdcfd 100644 --- a/lib/wallet_settings/bloc/state.dart +++ b/lib/wallet_settings/bloc/state.dart @@ -1,3 +1,4 @@ +import 'package:bb_mobile/_model/wallet.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; part 'state.freezed.dart'; @@ -24,6 +25,9 @@ class WalletSettingsState with _$WalletSettingsState { * SENSITIVE * */ + @Default({}) Map bip85Derivations, + @Default(false) bool updatingBip85Derivations, + @Default('') String errUpdatingBip85Derivations, @Default(false) bool backup, @Default(false) bool testingBackup, @Default('') String errTestingBackup, diff --git a/lib/wallet_settings/bloc/wallet_settings_cubit.dart b/lib/wallet_settings/bloc/wallet_settings_cubit.dart index 32bc4a92a..c745c4f92 100644 --- a/lib/wallet_settings/bloc/wallet_settings_cubit.dart +++ b/lib/wallet_settings/bloc/wallet_settings_cubit.dart @@ -491,33 +491,85 @@ class WalletSettingsCubit extends Cubit { } } - Future addNewBIP85BackupKey(String label) async { + void loadBIP85Derivations() { + emit(state.copyWith(bip85Derivations: _wallet.bip85Derivations)); + } + + Future createNewBIP85BackupKeyClicked(String label) async { + emit(state.copyWith(updatingBip85Derivations: true)); try { - emit(state.copyWith(savingFile: true, errSavingFile: '')); final wallet = _wallet; - final newBIP85Path = wallet.generateNextBIP85Path(); + final path = wallet.generateNextBIP85Path(); - //TODO; find a way to get the label from the wallet - final updatedWallet = WalletUpdate().updateBIP85Paths( - wallet, - newBIP85Path, - "${label}_bip85Path_${wallet.bip85Derivations.entries.length + 1}", + await _appWalletsRepository.getWalletServiceById(wallet.id)?.updateWallet( + WalletUpdate().updateBIP85Paths(wallet, path, label), + updateTypes: [UpdateWalletTypes.bip85Paths], ); + final updatedWallet = _appWalletsRepository.getWalletById(wallet.id); + if (updatedWallet == null) { + emit( + state.copyWith( + errUpdatingBip85Derivations: 'Failed to update the wallet', + updatingBip85Derivations: false, + ), + ); + return; + } - await _appWalletsRepository - .getWalletServiceById(updatedWallet.id) - ?.updateWallet( - updatedWallet, - updateTypes: [ - UpdateWalletTypes.bip85Paths, - ], + emit( + state.copyWith( + bip85Derivations: updatedWallet.bip85Derivations, + updatingBip85Derivations: false, + ), + ); + } catch (e) { + emit( + state.copyWith( + errUpdatingBip85Derivations: e.toString(), + updatingBip85Derivations: false, + ), ); + } + } - emit(state.copyWith(savingFile: false, savedFile: true)); - await Future.delayed(2.seconds); - emit(state.copyWith(savedFile: false)); + Future updateBIP85LabelClicked(String path, String newLabel) async { + emit( + state.copyWith( + updatingBip85Derivations: true, + errUpdatingBip85Derivations: '', + ), + ); + + try { + final wallet = _wallet; + await _appWalletsRepository.getWalletServiceById(wallet.id)?.updateWallet( + WalletUpdate().updateBIP85Paths(wallet, path, newLabel), + updateTypes: [UpdateWalletTypes.bip85Paths], + ); + + final updatedWallet = _appWalletsRepository.getWalletById(wallet.id); + if (updatedWallet == null) { + emit( + state.copyWith( + errUpdatingBip85Derivations: 'Failed to update the wallet', + updatingBip85Derivations: false, + ), + ); + return; + } + emit( + state.copyWith( + bip85Derivations: updatedWallet.bip85Derivations, + updatingBip85Derivations: false, + ), + ); } catch (e) { - emit(state.copyWith(errSavingFile: e.toString(), savingFile: false)); + emit( + state.copyWith( + errUpdatingBip85Derivations: e.toString(), + updatingBip85Derivations: false, + ), + ); } } From fb9112304df09edb28cd9fa9d069874eb9b5b7df Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Tue, 21 Jan 2025 21:46:11 -0500 Subject: [PATCH 092/401] feat(wallet_settings): Add BIP85PathsPage for managing BIP85 derivation paths. --- lib/routes.dart | 9 +- lib/wallet_settings/bip85_paths.dart | 259 +++++++++++++++++++++++++++ 2 files changed, 267 insertions(+), 1 deletion(-) create mode 100644 lib/wallet_settings/bip85_paths.dart diff --git a/lib/routes.dart b/lib/routes.dart index 904aac822..b668738a8 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -40,6 +40,7 @@ import 'package:bb_mobile/wallet/information_page.dart'; import 'package:bb_mobile/wallet/wallet_page.dart'; import 'package:bb_mobile/wallet_settings/accounting.dart'; import 'package:bb_mobile/wallet_settings/backup.dart'; +import 'package:bb_mobile/wallet_settings/bip85_paths.dart'; import 'package:bb_mobile/wallet_settings/test_backup.dart'; import 'package:bb_mobile/wallet_settings/wallet_settings_page.dart'; import 'package:flutter/material.dart'; @@ -356,7 +357,13 @@ GoRouter setupRouter() => GoRouter( ); }, ), - + GoRoute( + path: '/wallet-settings/bip85-paths', + builder: (context, state) { + final wallet = state.extra! as String; + return Bip85PathsPage(wallet: wallet); + }, + ), // // // diff --git a/lib/wallet_settings/bip85_paths.dart b/lib/wallet_settings/bip85_paths.dart new file mode 100644 index 000000000..3e0e34f78 --- /dev/null +++ b/lib/wallet_settings/bip85_paths.dart @@ -0,0 +1,259 @@ +import 'package:bb_mobile/_model/wallet.dart'; +import 'package:bb_mobile/_ui/app_bar.dart'; +import 'package:bb_mobile/_ui/components/text.dart'; +import 'package:bb_mobile/_ui/toast.dart'; +import 'package:bb_mobile/wallet_settings/bloc/state.dart'; +import 'package:bb_mobile/wallet_settings/bloc/wallet_settings_cubit.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:gap/gap.dart'; +import 'package:go_router/go_router.dart'; + +class Bip85PathsPage extends StatelessWidget { + const Bip85PathsPage({super.key, required this.wallet}); + + final String wallet; + + @override + Widget build(BuildContext context) { + return BlocProvider.value( + value: createWalletSettingsCubit(wallet)..loadBIP85Derivations(), + child: Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + flexibleSpace: BBAppBar( + text: 'BIP85 Paths', + onBack: context.pop, + ), + ), + body: const _Screen(), + ), + ); + } +} + +class _Screen extends StatelessWidget { + const _Screen(); + + @override + Widget build(BuildContext context) { + return BlocConsumer( + listener: (context, state) { + if (state.errUpdatingBip85Derivations.isNotEmpty) { + context.showToast(state.errUpdatingBip85Derivations); + } + }, + buildWhen: (previous, current) => + previous.bip85Derivations.length != current.bip85Derivations.length || + current.bip85Derivations.entries.any((entry) { + final prev = previous.bip85Derivations[entry.key]; + return prev == null || + prev.label != entry.value.label || + prev.status != entry.value.status; + }), + builder: (context, state) { + final activeBip85Paths = state.bip85Derivations.entries + .where( + (entry) => entry.value.status == BIP85DerivationStatus.active, + ) + .toList(); + final deprecatedBip85Paths = state.bip85Derivations.entries + .where( + (entry) => entry.value.status == BIP85DerivationStatus.revoked, + ) + .toList(); + + return state.updatingBip85Derivations + ? const Center(child: CircularProgressIndicator()) + : Column( + children: [ + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const BBText.body('Active bip85 paths'), + const Gap(8), + _buildBip85List(context, activeBip85Paths), + if (deprecatedBip85Paths.isNotEmpty) ...[ + const BBText.body('Revoked bip85 paths'), + const Gap(8), + _buildBip85List( + context, + deprecatedBip85Paths, + isDeprecated: true, + ), + ], + ], + ), + ), + ), + Padding( + padding: const EdgeInsets.all(24), + child: ElevatedButton( + onPressed: () => _showCreateDialog(context), + child: const Text('Create New Derivation Path'), + ), + ), + ], + ); + }, + ); + } + + Widget _buildBip85List( + BuildContext context, + List> paths, { + bool isDeprecated = false, + }) { + if (paths.isEmpty) { + return const Text('No paths available'); + } + + return ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: paths.length, + separatorBuilder: (_, __) => const Divider(height: 1), + itemBuilder: (context, index) { + final entry = paths[index]; + return InkWell( + onTap: isDeprecated ? null : () => _showEditDialog(context, entry), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + flex: 3, + child: Text( + entry.value.label, + style: TextStyle( + fontWeight: FontWeight.bold, + color: isDeprecated ? Colors.grey : null, + ), + ), + ), + Expanded( + child: Text( + entry.key, + style: TextStyle( + color: isDeprecated ? Colors.grey : null, + ), + ), + ), + ], + ), + ), + ); + }, + ); + } + + void _showEditDialog( + BuildContext context, + MapEntry entry, + ) { + final labelController = TextEditingController(text: entry.value.label); + final cubit = context.read(); + + showDialog( + context: context, + builder: (dialogContext) => + BlocConsumer( + bloc: cubit, + listener: (context, state) { + if (!state.updatingBip85Derivations) { + Navigator.pop(dialogContext); + } + }, + builder: (context, state) => AlertDialog( + title: const BBText.body('Edit Label'), + content: TextField( + controller: labelController, + decoration: const InputDecoration( + border: OutlineInputBorder(), + labelText: 'Label', + floatingLabelStyle: TextStyle(color: Colors.black), + floatingLabelBehavior: FloatingLabelBehavior.always, + focusedBorder: OutlineInputBorder(), + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(dialogContext), + child: const Text('Cancel'), + ), + TextButton( + onPressed: state.updatingBip85Derivations + ? null + : () { + if (labelController.text.isNotEmpty) { + cubit.updateBIP85LabelClicked( + entry.key, + labelController.text, + ); + } + }, + child: state.updatingBip85Derivations + ? const CircularProgressIndicator() + : const Text('Update'), + ), + ], + ), + ), + ); + } + + void _showCreateDialog(BuildContext context) { + final labelController = TextEditingController(); + final cubit = context.read(); + + showDialog( + context: context, + builder: (dialogContext) => + BlocConsumer( + bloc: cubit, + listener: (context, state) { + if (!state.updatingBip85Derivations) { + Navigator.pop(dialogContext); + } + }, + builder: (context, state) => AlertDialog( + title: const BBText.body('Create Derivation Path'), + content: TextField( + controller: labelController, + decoration: const InputDecoration( + border: OutlineInputBorder(), + labelText: 'Label', + floatingLabelStyle: TextStyle(color: Colors.black), + floatingLabelBehavior: FloatingLabelBehavior.always, + focusedBorder: OutlineInputBorder(), + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(dialogContext), + child: const Text('Cancel'), + ), + TextButton( + onPressed: state.updatingBip85Derivations + ? null + : () { + if (labelController.text.isNotEmpty) { + cubit.createNewBIP85BackupKeyClicked( + labelController.text, + ); + } + }, + child: state.updatingBip85Derivations + ? const CircularProgressIndicator() + : const Text('Create'), + ), + ], + ), + ), + ); + } +} From 13a0ade4098faec3aed9bee3438994c2868bf474 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Tue, 21 Jan 2025 21:46:34 -0500 Subject: [PATCH 093/401] feat(wallet_settings): Add Bip85PathsButton to wallet settings --- lib/wallet_settings/wallet_settings_page.dart | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/lib/wallet_settings/wallet_settings_page.dart b/lib/wallet_settings/wallet_settings_page.dart index 8418457a8..a712554f5 100644 --- a/lib/wallet_settings/wallet_settings_page.dart +++ b/lib/wallet_settings/wallet_settings_page.dart @@ -147,6 +147,8 @@ class _ScreenState extends State<_Screen> { // const Gap(8), const WalletDetailsButton(), const Gap(8), + const Bip85PathsButton(), + const Gap(8), const AddressesButtons(), const Gap(8), @@ -418,6 +420,24 @@ class AccountingButton extends StatelessWidget { } } +class Bip85PathsButton extends StatelessWidget { + const Bip85PathsButton({super.key}); + + @override + Widget build(BuildContext context) { + return BBButton.textWithStatusAndRightArrow( + label: 'BIP85 Paths', + onPressed: () { + final wallet = context.read().state.wallet; + context.push( + '/wallet-settings/bip85-paths', + extra: wallet.id, + ); + }, + ); + } +} + class WalletDetailsButton extends StatelessWidget { const WalletDetailsButton({super.key}); From 0eab3a717221fac68638212280171a331501f8bd Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Tue, 21 Jan 2025 21:47:38 -0500 Subject: [PATCH 094/401] chore(pubspec.lock): update resolved-ref for local package dependency --- pubspec.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.lock b/pubspec.lock index 9b0a9a582..433c41d87 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1340,7 +1340,7 @@ packages: description: path: "." ref: HEAD - resolved-ref: "498509da2a2376b1534ee49f1ca65b3d14809e59" + resolved-ref: "716b5650e0e0f9ccb12dec89ff54b0fee6461b47" url: "https://github.com/StaxoLotl/recoverbull-dart.git" source: git version: "0.0.1" From c4796ce41eade80983d30cbf7f58c1819d0fd99a Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Wed, 22 Jan 2025 10:19:40 -0500 Subject: [PATCH 095/401] refactor(wallet): rename generateNextBIP85Path to generateNextBIP85BackupKey. --- lib/_model/wallet.dart | 9 +++++---- lib/wallet_settings/bloc/wallet_settings_cubit.dart | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/_model/wallet.dart b/lib/_model/wallet.dart index 4a65175e4..c7520844e 100644 --- a/lib/_model/wallet.dart +++ b/lib/_model/wallet.dart @@ -203,16 +203,17 @@ class Wallet with _$Wallet { return exDescDerivedKey; } - String generateNextBIP85Path() { + //TODO; re-create this to derive any bip85 path + String generateNextBIP85BackupKey() { + const prefix = "m/1608'/"; final highestIndex = bip85Derivations.keys - .where((path) => path.startsWith("m/1608'/")) + .where((path) => path.startsWith(prefix)) .map( (path) => int.tryParse(path.split('/').last.replaceAll("'", "")) ?? -1, ) .fold(-1, (max, index) => index > max ? index : max); - - return "m/1608'/${highestIndex + 1}'"; + return "$prefix${highestIndex + 1}'"; } // storage key diff --git a/lib/wallet_settings/bloc/wallet_settings_cubit.dart b/lib/wallet_settings/bloc/wallet_settings_cubit.dart index c745c4f92..778ad4cee 100644 --- a/lib/wallet_settings/bloc/wallet_settings_cubit.dart +++ b/lib/wallet_settings/bloc/wallet_settings_cubit.dart @@ -499,7 +499,7 @@ class WalletSettingsCubit extends Cubit { emit(state.copyWith(updatingBip85Derivations: true)); try { final wallet = _wallet; - final path = wallet.generateNextBIP85Path(); + final path = wallet.generateNextBIP85BackupKey(); await _appWalletsRepository.getWalletServiceById(wallet.id)?.updateWallet( WalletUpdate().updateBIP85Paths(wallet, path, label), From a45b56dbf54e3e39bf6df5086ea7a3d09671999f Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Tue, 4 Feb 2025 05:31:23 -0500 Subject: [PATCH 096/401] chore(dependencies): update bip85 and recoverbull package references --- pubspec.lock | 27 +++++++++------------------ pubspec.yaml | 10 +++------- 2 files changed, 12 insertions(+), 25 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 433c41d87..a4d1db566 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -97,12 +97,11 @@ packages: bip85: dependency: "direct main" description: - path: "bindings/dart-bip85" - ref: master - resolved-ref: e1bea65d63c8b9a97d1fd77034ca11304c08ae96 - url: "https://github.com/ethicnology/rust-bip85.git" - source: git - version: "1.0.2" + name: bip85 + sha256: "1e556b32a6e2062a8e6f728bfdf1898058ecde9c9068f5272d4792af2477b10c" + url: "https://pub.dev" + source: hosted + version: "1.0.3" bitcoin_utils: dependency: "direct main" description: @@ -320,14 +319,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.6" - cryptography: - dependency: transitive - description: - name: cryptography - sha256: d146b76d33d94548cf035233fbc2f4338c1242fa119013bead807d033fc4ae05 - url: "https://pub.dev" - source: hosted - version: "2.7.0" csslib: dependency: transitive description: @@ -1335,15 +1326,15 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.0" - recoverbull_dart: + recoverbull: dependency: "direct main" description: path: "." ref: HEAD - resolved-ref: "716b5650e0e0f9ccb12dec89ff54b0fee6461b47" - url: "https://github.com/StaxoLotl/recoverbull-dart.git" + resolved-ref: "1cb9cc5e408688d00f6dbda21abd34a8b3b7f5e2" + url: "https://github.com/SatoshiPortal/recoverbull-client-dart.git" source: git - version: "0.0.1" + version: "1.0.0" rxdart: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 08401fba9..ec8fbbc12 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -94,19 +94,15 @@ dependencies: permission_handler: ^11.3.1 rxdart: ^0.28.0 synchronized: ^3.3.0+3 - bip85: - git: - url: https://github.com/ethicnology/rust-bip85.git - path: bindings/dart-bip85 - ref: master + bip85: ^1.0.3 web_socket_channel: ^3.0.1 flutter_speed_dial: ^7.0.0 googleapis: ^13.2.0 google_sign_in: ^6.2.2 extension_google_sign_in_as_googleapis_auth: ^2.0.12 - recoverbull_dart: + recoverbull: git: - url: https://github.com/StaxoLotl/recoverbull-dart.git + url: https://github.com/SatoshiPortal/recoverbull-client-dart.git dev_dependencies: From 64e8a59ad21169239927f01a900277b7b7688705 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Tue, 4 Feb 2025 05:36:23 -0500 Subject: [PATCH 097/401] refactor(wallet): rename backupTested to physicalBackupTested --- lib/_model/wallet.dart | 6 ++++-- lib/_pkg/wallet/bdk/create.dart | 16 ++++++++-------- lib/_pkg/wallet/bdk/sensitive_create.dart | 6 +++--- lib/_pkg/wallet/create.dart | 10 +++++----- lib/_repository/wallet_service.dart | 12 +++++++----- lib/home/bloc/home_state.dart | 2 +- lib/import/bloc/import_cubit.dart | 2 +- lib/wallet/wallet_page.dart | 12 ++++++------ lib/wallet/wallet_txs.dart | 6 +++--- .../wallet_sensitive_create_test.data.dart | 12 ++++++------ 10 files changed, 44 insertions(+), 40 deletions(-) diff --git a/lib/_model/wallet.dart b/lib/_model/wallet.dart index c7520844e..d4d7b0b58 100644 --- a/lib/_model/wallet.dart +++ b/lib/_model/wallet.dart @@ -71,8 +71,10 @@ class Wallet with _$Wallet { String? path, int? balance, Balance? fullBalance, - @Default(false) bool backupTested, - DateTime? lastBackupTested, + @Default(false) bool physicalBackupTested, + DateTime? lastPhysicalBackupTested, + @Default(false) bool vaultBackupTested, + @Default(false) bool lastVaultBackupTested, @Default(false) bool hide, @Default(false) bool mainWallet, required BaseWalletType baseWalletType, diff --git a/lib/_pkg/wallet/bdk/create.dart b/lib/_pkg/wallet/bdk/create.dart index 48d7d86f1..244f2ed67 100644 --- a/lib/_pkg/wallet/bdk/create.dart +++ b/lib/_pkg/wallet/bdk/create.dart @@ -122,7 +122,7 @@ class BDKCreate { network: network, type: BBWalletType.coldcard, scriptType: ScriptType.bip44, - backupTested: true, + physicalBackupTested: true, baseWalletType: BaseWalletType.Bitcoin, ); final (bdkWallet44, errBdk44) = await loadPublicBdkWallet(wallet44); @@ -154,7 +154,7 @@ class BDKCreate { network: network, type: BBWalletType.coldcard, scriptType: ScriptType.bip49, - backupTested: true, + physicalBackupTested: true, baseWalletType: BaseWalletType.Bitcoin, ); final (bdkWallet49, errBdk49) = await loadPublicBdkWallet(wallet49); @@ -186,7 +186,7 @@ class BDKCreate { network: network, type: BBWalletType.coldcard, scriptType: ScriptType.bip84, - backupTested: true, + physicalBackupTested: true, baseWalletType: BaseWalletType.Bitcoin, ); final (bdkWallet84, errBdk84) = await loadPublicBdkWallet(wallet84); @@ -289,7 +289,7 @@ class BDKCreate { network: network, type: BBWalletType.xpub, scriptType: scriptType, - backupTested: true, + physicalBackupTested: true, baseWalletType: BaseWalletType.Bitcoin, ); final (bdkWallet, errBdk) = await loadPublicBdkWallet(wallet); @@ -379,7 +379,7 @@ class BDKCreate { network: network, type: BBWalletType.xpub, scriptType: ScriptType.bip84, - backupTested: true, + physicalBackupTested: true, baseWalletType: BaseWalletType.Bitcoin, ); @@ -392,7 +392,7 @@ class BDKCreate { network: network, type: BBWalletType.xpub, scriptType: ScriptType.bip49, - backupTested: true, + physicalBackupTested: true, baseWalletType: BaseWalletType.Bitcoin, ); @@ -405,7 +405,7 @@ class BDKCreate { network: network, type: BBWalletType.xpub, scriptType: ScriptType.bip44, - backupTested: true, + physicalBackupTested: true, baseWalletType: BaseWalletType.Bitcoin, ); @@ -480,7 +480,7 @@ class BDKCreate { network: network, type: BBWalletType.xpub, scriptType: scriptType, - backupTested: true, + physicalBackupTested: true, baseWalletType: BaseWalletType.Bitcoin, ); final (bdkWallet, errBdk) = await loadPublicBdkWallet(wallet); diff --git a/lib/_pkg/wallet/bdk/sensitive_create.dart b/lib/_pkg/wallet/bdk/sensitive_create.dart index a79156df0..634f73a9f 100644 --- a/lib/_pkg/wallet/bdk/sensitive_create.dart +++ b/lib/_pkg/wallet/bdk/sensitive_create.dart @@ -163,7 +163,7 @@ class BDKSensitiveCreate { network: network, type: BBWalletType.words, scriptType: ScriptType.bip44, - backupTested: isImported, + physicalBackupTested: isImported, baseWalletType: BaseWalletType.Bitcoin, ); final (bdkWallet44, errBdk44) = @@ -195,7 +195,7 @@ class BDKSensitiveCreate { network: network, type: BBWalletType.words, scriptType: ScriptType.bip49, - backupTested: isImported, + physicalBackupTested: isImported, baseWalletType: BaseWalletType.Bitcoin, ); final (bdkWallet49, errBdk49) = @@ -226,7 +226,7 @@ class BDKSensitiveCreate { network: network, type: BBWalletType.words, scriptType: ScriptType.bip84, - backupTested: isImported, + physicalBackupTested: isImported, baseWalletType: BaseWalletType.Bitcoin, ); final (bdkWallet84, errBdk84) = diff --git a/lib/_pkg/wallet/create.dart b/lib/_pkg/wallet/create.dart index 52fe8bcc2..1e5f89096 100644 --- a/lib/_pkg/wallet/create.dart +++ b/lib/_pkg/wallet/create.dart @@ -224,7 +224,7 @@ class WalletCreate implements IWalletCreate { // network: network, // type: BBWalletType.coldcard, // scriptType: ScriptType.bip44, -// backupTested: true, +// physicalBackupTested: true, // baseWalletType: BaseWalletType.Bitcoin, // ); // final errBdk44 = await loadPublicBdkWallet(wallet44); @@ -255,7 +255,7 @@ class WalletCreate implements IWalletCreate { // network: network, // type: BBWalletType.coldcard, // scriptType: ScriptType.bip49, -// backupTested: true, +// physicalBackupTested: true, // baseWalletType: BaseWalletType.Bitcoin, // ); // final errBdk49 = await loadPublicBdkWallet(wallet49); @@ -286,7 +286,7 @@ class WalletCreate implements IWalletCreate { // network: network, // type: BBWalletType.coldcard, // scriptType: ScriptType.bip84, -// backupTested: true, +// physicalBackupTested: true, // baseWalletType: BaseWalletType.Bitcoin, // ); // final errBdk84 = await loadPublicBdkWallet(wallet84); @@ -379,7 +379,7 @@ class WalletCreate implements IWalletCreate { // network: network, // type: BBWalletType.xpub, // scriptType: scriptType, -// backupTested: true, +// physicalBackupTested: true, // baseWalletType: BaseWalletType.Bitcoin, // ); // final errBdk = await loadPublicBdkWallet(wallet); @@ -468,7 +468,7 @@ class WalletCreate implements IWalletCreate { // network: network, // type: BBWalletType.xpub, // scriptType: scriptType, -// backupTested: true, +// physicalBackupTested: true, // baseWalletType: BaseWalletType.Bitcoin, // ); // final errBdk = await loadPublicBdkWallet(wallet); diff --git a/lib/_repository/wallet_service.dart b/lib/_repository/wallet_service.dart index 3cf491703..66c552821 100644 --- a/lib/_repository/wallet_service.dart +++ b/lib/_repository/wallet_service.dart @@ -261,9 +261,10 @@ class WalletService { storageWallet = storageWallet!.copyWith(utxos: wallet.utxos); case UpdateWalletTypes.settings: - if (wallet.backupTested != storageWallet!.backupTested) { + if (wallet.physicalBackupTested != + storageWallet!.physicalBackupTested) { storageWallet = storageWallet.copyWith( - backupTested: wallet.backupTested, + physicalBackupTested: wallet.physicalBackupTested, ); } @@ -273,10 +274,11 @@ class WalletService { ); } - if (wallet.lastBackupTested != null && - wallet.lastBackupTested != storageWallet.lastBackupTested) { + if (wallet.lastPhysicalBackupTested != null && + wallet.lastPhysicalBackupTested != + storageWallet.lastPhysicalBackupTested) { storageWallet = storageWallet.copyWith( - lastBackupTested: wallet.lastBackupTested, + lastPhysicalBackupTested: wallet.lastPhysicalBackupTested, ); } case UpdateWalletTypes.bip85Paths: diff --git a/lib/home/bloc/home_state.dart b/lib/home/bloc/home_state.dart index e620631ff..81e493177 100644 --- a/lib/home/bloc/home_state.dart +++ b/lib/home/bloc/home_state.dart @@ -408,7 +408,7 @@ class HomeState with _$HomeState { return wb.balanceSats() > 100000000; } - bool backupWarning(Wallet wb) => !wb.backupTested; + bool backupWarning(Wallet wb) => !wb.physicalBackupTested; final warnings = <({String info, Wallet walletBloc})>{}; final List backupWalletFngrforBackupWarning = []; diff --git a/lib/import/bloc/import_cubit.dart b/lib/import/bloc/import_cubit.dart index 3bd19f645..63dcf2adc 100644 --- a/lib/import/bloc/import_cubit.dart +++ b/lib/import/bloc/import_cubit.dart @@ -691,7 +691,7 @@ class ImportWalletCubit extends Cubit { return null; } - wallet = wallet!.copyWith(backupTested: true); + wallet = wallet!.copyWith(physicalBackupTested: true); if (state.mainWallet) { wallet = wallet.copyWith( mainWallet: true, diff --git a/lib/wallet/wallet_page.dart b/lib/wallet/wallet_page.dart index deb21b42e..1c398934c 100644 --- a/lib/wallet/wallet_page.dart +++ b/lib/wallet/wallet_page.dart @@ -62,8 +62,8 @@ class _Screen extends StatelessWidget { @override Widget build(BuildContext context) { - final backupTested = - context.select((WalletBloc x) => x.state.wallet.backupTested); + final physicalBackupTested = + context.select((WalletBloc x) => x.state.wallet.physicalBackupTested); return RefreshIndicator( onRefresh: () async { @@ -77,7 +77,7 @@ class _Screen extends StatelessWidget { children: [ const WalletHeader(), const ActionsRow(), - if (!backupTested) ...[ + if (!physicalBackupTested) ...[ const Gap(24), const BackupAlertBanner(), // const Gap(24), @@ -114,8 +114,8 @@ class ActionsRow extends StatelessWidget { @override Widget build(BuildContext context) { - final backupTested = - context.select((WalletBloc x) => x.state.wallet.backupTested); + final physicalBackupTested = + context.select((WalletBloc x) => x.state.wallet.physicalBackupTested); final watchonly = context.select((WalletBloc x) => x.state.wallet.watchOnly()); final isInstant = @@ -136,7 +136,7 @@ class ActionsRow extends StatelessWidget { BBButton.text( label: 'Backup', isBlue: false, - isRed: !backupTested, + isRed: !physicalBackupTested, onPressed: () { final walletBloc = context.read(); context.push( diff --git a/lib/wallet/wallet_txs.dart b/lib/wallet/wallet_txs.dart index b83b33a58..d75898cfe 100644 --- a/lib/wallet/wallet_txs.dart +++ b/lib/wallet/wallet_txs.dart @@ -230,10 +230,10 @@ class BackupAlertBanner extends StatelessWidget { @override Widget build(BuildContext context) { final wallet = context.select((WalletBloc x) => x.state.wallet); - final backupTested = - context.select((WalletBloc x) => x.state.wallet.backupTested); + final physicalBackupTested = + context.select((WalletBloc x) => x.state.wallet.physicalBackupTested); - if (backupTested) return const SizedBox.shrink(); + if (physicalBackupTested) return const SizedBox.shrink(); return WarningBanner( onTap: () { diff --git a/test/_pkg/wallet/sensitive/wallet_sensitive_create_test.data.dart b/test/_pkg/wallet/sensitive/wallet_sensitive_create_test.data.dart index 1a3f6ba2a..8b41bcd66 100644 --- a/test/_pkg/wallet/sensitive/wallet_sensitive_create_test.data.dart +++ b/test/_pkg/wallet/sensitive/wallet_sensitive_create_test.data.dart @@ -27,7 +27,7 @@ // myAddressBook: [], // transactions: [], // unsignedTxs: [], -// backupTested: hasImported, +// physicalBackupTested: hasImported, // baseWalletType: BaseWalletType.Bitcoin, // ); // case ScriptType.bip49: @@ -52,7 +52,7 @@ // myAddressBook: [], // transactions: [], // unsignedTxs: [], -// backupTested: hasImported, +// physicalBackupTested: hasImported, // ); // case ScriptType.bip84: // return Wallet( @@ -76,7 +76,7 @@ // myAddressBook: [], // transactions: [], // unsignedTxs: [], -// backupTested: hasImported, +// physicalBackupTested: hasImported, // ); // } // } else { @@ -103,7 +103,7 @@ // myAddressBook: [], // transactions: [], // unsignedTxs: [], -// backupTested: hasImported, +// physicalBackupTested: hasImported, // ); // case ScriptType.bip49: // return Wallet( @@ -127,7 +127,7 @@ // myAddressBook: [], // transactions: [], // unsignedTxs: [], -// backupTested: hasImported, +// physicalBackupTested: hasImported, // ); // case ScriptType.bip84: // return Wallet( @@ -151,7 +151,7 @@ // myAddressBook: [], // transactions: [], // unsignedTxs: [], -// backupTested: hasImported, +// physicalBackupTested: hasImported, // ); // } // } From 9be10ee50ae0db1ff6ef1227dc7743dbe3d779af Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Tue, 4 Feb 2025 05:37:09 -0500 Subject: [PATCH 098/401] feat(page_template): add bottomChildHeight parameter for customizable height --- lib/_ui/page_template.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/_ui/page_template.dart b/lib/_ui/page_template.dart index 5b05ed0d7..1cb6ea3ba 100644 --- a/lib/_ui/page_template.dart +++ b/lib/_ui/page_template.dart @@ -7,10 +7,12 @@ class StackedPage extends StatelessWidget { super.key, required this.child, required this.bottomChild, + this.bottomChildHeight = 72, }); final Widget child; final Widget bottomChild; + final double bottomChildHeight; @override Widget build(BuildContext context) { @@ -20,7 +22,7 @@ class StackedPage extends StatelessWidget { BottomCenter( child: Container( width: double.infinity, - height: 72, + height: bottomChildHeight, color: context.colour.primaryContainer.withValues(alpha: 0.95), padding: const EdgeInsets.only(bottom: 16, top: 8, left: 16, right: 16), From 98692309b552323d96cba94efbb3bc48411f57d3 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Wed, 5 Feb 2025 04:22:37 -0500 Subject: [PATCH 099/401] chore(pubspec.lock): update resolved-ref for recoverbull package dependency --- pubspec.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.lock b/pubspec.lock index a4d1db566..891da3bb0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1331,7 +1331,7 @@ packages: description: path: "." ref: HEAD - resolved-ref: "1cb9cc5e408688d00f6dbda21abd34a8b3b7f5e2" + resolved-ref: "1f3a181993fa66c8ca471012a19c4537f2d66403" url: "https://github.com/SatoshiPortal/recoverbull-client-dart.git" source: git version: "1.0.0" From 863033ec4633c65d6c9725436cd537ecc9a1c679 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Wed, 5 Feb 2025 04:23:15 -0500 Subject: [PATCH 100/401] refactor(backup): removed unused fields --- lib/_model/backup.dart | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/lib/_model/backup.dart b/lib/_model/backup.dart index f051b0787..238a59a13 100644 --- a/lib/_model/backup.dart +++ b/lib/_model/backup.dart @@ -1,4 +1,3 @@ -import 'package:bb_mobile/_model/bip329_label.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; part 'backup.freezed.dart'; @@ -9,24 +8,15 @@ class Backup with _$Backup { const factory Backup({ @Default(1) int version, @Default('') String name, - @Default('') String layer, @Default('') String network, - @Default('') String script, - @Default('') String type, @Default([]) List mnemonic, @Default('') String passphrase, @Default('') String mnemonicFingerPrint, - @Default([]) List labels, - @Default([]) List descriptors, }) = _Backup; factory Backup.fromJson(Map json) => _$BackupFromJson(json); const Backup._(); - bool get isEmpty => - mnemonic.isEmpty && - passphrase.isEmpty && - labels.isEmpty && - descriptors.isEmpty; + bool get isEmpty => mnemonic.isEmpty && passphrase.isEmpty; } From 5ca2ba7f48fd5e311c8b4f66c6db3d6c22daa38a Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Wed, 5 Feb 2025 04:26:07 -0500 Subject: [PATCH 101/401] fix(toast): update SnackBar text style to bodyBold for better visibility --- lib/_ui/toast.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/_ui/toast.dart b/lib/_ui/toast.dart index 0ce9c5c9c..e0079fd84 100644 --- a/lib/_ui/toast.dart +++ b/lib/_ui/toast.dart @@ -5,7 +5,7 @@ import 'package:flutter/material.dart'; extension Xontext on BuildContext { SnackBar showToast(String text) { return SnackBar( - content: Center(child: BBText.titleLarge(text)), + content: Center(child: BBText.bodyBold(text)), backgroundColor: colour.primaryContainer, elevation: 4, ); From 30d2b59f5edcd7e94e5fb3b0926b85f762be4213 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Wed, 5 Feb 2025 04:28:56 -0500 Subject: [PATCH 102/401] refactor(backup): remove unused state and cubit files --- lib/backup/backup_page.dart | 241 ---------------------------- lib/backup/bloc/cloud_cubit.dart | 107 ------------ lib/backup/bloc/cloud_state.dart | 14 -- lib/backup/bloc/keychain_cubit.dart | 40 ----- lib/backup/bloc/keychain_state.dart | 13 -- lib/backup/bloc/manual_cubit.dart | 226 -------------------------- lib/backup/bloc/manual_state.dart | 27 ---- lib/backup/keychain_page.dart | 111 ------------- lib/settings/settings_page.dart | 39 ----- 9 files changed, 818 deletions(-) delete mode 100644 lib/backup/backup_page.dart delete mode 100644 lib/backup/bloc/cloud_cubit.dart delete mode 100644 lib/backup/bloc/cloud_state.dart delete mode 100644 lib/backup/bloc/keychain_cubit.dart delete mode 100644 lib/backup/bloc/keychain_state.dart delete mode 100644 lib/backup/bloc/manual_cubit.dart delete mode 100644 lib/backup/bloc/manual_state.dart delete mode 100644 lib/backup/keychain_page.dart diff --git a/lib/backup/backup_page.dart b/lib/backup/backup_page.dart deleted file mode 100644 index 7f783634d..000000000 --- a/lib/backup/backup_page.dart +++ /dev/null @@ -1,241 +0,0 @@ -import 'package:bb_mobile/_model/wallet.dart'; -import 'package:bb_mobile/_pkg/backup/google_drive.dart'; -import 'package:bb_mobile/_pkg/backup/local.dart'; -import 'package:bb_mobile/_repository/wallet/sensitive_wallet_storage.dart'; -import 'package:bb_mobile/_ui/app_bar.dart'; -import 'package:bb_mobile/_ui/components/button.dart'; -import 'package:bb_mobile/_ui/components/controls.dart'; -import 'package:bb_mobile/_ui/components/text.dart'; -import 'package:bb_mobile/_ui/toast.dart'; -import 'package:bb_mobile/backup/bloc/cloud_cubit.dart'; -import 'package:bb_mobile/backup/bloc/cloud_state.dart'; -import 'package:bb_mobile/backup/bloc/manual_cubit.dart'; -import 'package:bb_mobile/backup/bloc/manual_state.dart'; -import 'package:bb_mobile/locator.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:gap/gap.dart'; -import 'package:go_router/go_router.dart'; - -class ManualBackupPage extends StatefulWidget { - const ManualBackupPage({super.key, required this.wallets}); - - final List wallets; - - @override - _TheBackupPageState createState() => _TheBackupPageState(); -} - -class _TheBackupPageState extends State { - late final ManualCubit _backupCubit; - - @override - void initState() { - super.initState(); - _backupCubit = ManualCubit( - wallets: widget.wallets, - walletSensitiveStorage: locator(), - manager: locator(), - ); - } - - @override - void dispose() { - _backupCubit.clearAndClose(); - //TODO: clear cloud cubit - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return BlocProvider.value( - value: _backupCubit, - child: Scaffold( - backgroundColor: Colors.white, - appBar: AppBar( - automaticallyImplyLeading: false, - elevation: 0, - flexibleSpace: BBAppBar( - text: 'Backup', - onBack: () => Navigator.of(context).pop(), - ), - ), - body: BlocListener( - listener: (context, state) { - if (state.error.isNotEmpty) { - ScaffoldMessenger.of(context) - .showSnackBar(context.showToast(state.error)); - - context.read().clearError(); - } - if (state.backupId.isNotEmpty && state.backupKey.isNotEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - context.showToast('Backup created successfully'), - ); - } - }, - child: BlocBuilder( - builder: (context, state) { - return state.loading - ? const Center(child: CircularProgressIndicator()) - : Padding( - padding: const EdgeInsets.all(24.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - BackupToggleItem( - title: 'Mnemonics & Passwords', - value: state.selectedBackupOptions['mnemonic'] ?? - false, - onChanged: () { - context - .read() - .toggleAllMnemonicAndPassphrase(); - }, - ), - const Gap(8), - BackupToggleItem( - title: 'Descriptors', - value: state.selectedBackupOptions['descriptors'] ?? - false, - onChanged: () { - context.read().toggleDescriptors(); - }, - ), - const Gap(8), - BackupToggleItem( - title: 'Labels', - value: - state.selectedBackupOptions['labels'] ?? false, - onChanged: () { - context.read().toggleLabels(); - }, - ), - const Gap(8), - if (state.backupKey.isEmpty) - Center( - child: BBButton.big( - onPressed: () => context - .read() - .saveEncryptedBackup(), - label: "Generate Backup", - ), - ), - const Gap(20), - if (state.backupKey.isNotEmpty) - Column( - children: [ - const BBText.bodyBold("Generated Backup Key"), - const Gap(10), - SelectableText( - state.backupKey, - textAlign: TextAlign.center, - style: const TextStyle( - fontWeight: FontWeight.normal, - ), - ), - const Gap(20), - if (state.backupId.isNotEmpty) - BBButton.big( - onPressed: () => context.push( - '/keychain-backup', - extra: (state.backupKey, state.backupId), - ), - label: 'Save to Keychain', - ), - ], - ), - const Gap(50), - if (state.backupPath.isNotEmpty) - BlocProvider( - create: (context) => CloudCubit( - manager: locator(), - ), - child: Center( - child: BlocConsumer( - listener: (context, cloudState) { - if (cloudState.toast != '') { - ScaffoldMessenger.of(context) - .showSnackBar( - context.showToast(cloudState.toast), - ); - } else { - ScaffoldMessenger.of(context) - .showSnackBar( - context.showToast(cloudState.error), - ); - } - if (!cloudState.loading) { - context.read().clearToast(); - context.read().clearError(); - } - }, - buildWhen: (p, q) => p.loading != q.loading, - builder: (context, cloudState) { - return BBButton.big( - loading: cloudState.loading, - onPressed: () { - context.read().uploadBackup( - fileSystemBackupPath: - state.backupPath, - ); - // context.push( - // '/cloud-backup', - // extra: { - // 'cubit': context.read(), - // }, - // ); - }, - label: "Save to Google Drive", - ); - }, - ), - ), - ), - const Gap(10), - ], - ), - ); - }, - ), - ), - ), - ); - } -} - -class BackupToggleItem extends StatelessWidget { - final String title; - final bool value; - final VoidCallback onChanged; - const BackupToggleItem({ - super.key, - required this.title, - required this.value, - required this.onChanged, - }); - - @override - Widget build(BuildContext context) { - return BlocListener( - listenWhen: (previous, current) => - previous.selectedBackupOptions != current.selectedBackupOptions, - listener: (context, state) {}, - child: Row( - children: [ - BBText.body( - title, - ), - const Spacer(), - BBSwitch( - // key: UIKeys.settingsBackupToggleSwitch,//TODO; Add switch key - value: value, - onChanged: (e) { - onChanged(); - }, - ), - ], - ), - ); - } -} diff --git a/lib/backup/bloc/cloud_cubit.dart b/lib/backup/bloc/cloud_cubit.dart deleted file mode 100644 index c94b5649d..000000000 --- a/lib/backup/bloc/cloud_cubit.dart +++ /dev/null @@ -1,107 +0,0 @@ -import 'dart:io' as io; - -import 'package:bb_mobile/_pkg/backup/google_drive.dart'; -import 'package:bb_mobile/backup/bloc/cloud_state.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class CloudCubit extends Cubit { - final GoogleDriveBackupManager manager; - CloudCubit({required this.manager}) : super(const CloudState()); - - void clearToast() => emit(state.copyWith(toast: '', loading: false)); - - void clearError() => emit(state.copyWith(error: '', loading: false)); - - Future connect() async { - try { - emit(state.copyWith(loading: true)); - final (folderId, err) = await manager.connect(); - - if (folderId != null) { - emit( - state.copyWith( - backupFolderId: folderId, - loading: false, - ), - ); - } else if (err != null) { - emit( - state.copyWith( - error: err.message, - loading: false, - ), - ); - } - } catch (e) { - emit( - state.copyWith( - error: "GoogleDrive Error: $e", - loading: false, - ), - ); - } - } - - Future uploadBackup({ - required String fileSystemBackupPath, - }) async { - if (state.loading) { - emit(state.copyWith(error: 'Backup already in progress')); - return; - } - final now = DateTime.now(); - if (state.lastBackupAttempt != null) { - final difference = now.difference(state.lastBackupAttempt!); - if (difference.inSeconds < 30) { - emit( - state.copyWith( - error: - 'Please wait ${30 - difference.inSeconds} seconds before creating another backup', - ), - ); - return; - } - } - if (state.backupFolderId.isEmpty) await connect(); - emit(state.copyWith(loading: true, lastBackupAttempt: now)); - - try { - final backup = io.File(fileSystemBackupPath); - final content = await backup.readAsString(); - final (fileName, err) = await manager.saveEncryptedBackup( - encrypted: content, - backupFolder: state.backupFolderId, - ); - - if (err != null) { - debugPrint("Failed to backup file to Google Drive: ${err.message}"); - emit( - state.copyWith( - error: "Failed to backup file to Google Drive", - loading: false, - ), - ); - } else { - emit( - state.copyWith( - toast: "Successfully backed up to Google Drive", - loading: false, - ), - ); - } - } catch (e) { - emit( - state.copyWith( - error: "Failed to backup file: $e", - loading: false, - ), - ); - } - } - - void disconnect() { - manager.disconnect(); - emit(state.copyWith(backupFolderId: '', loading: false)); - } -} diff --git a/lib/backup/bloc/cloud_state.dart b/lib/backup/bloc/cloud_state.dart deleted file mode 100644 index 41b20250e..000000000 --- a/lib/backup/bloc/cloud_state.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'cloud_state.freezed.dart'; - -@freezed -class CloudState with _$CloudState { - const factory CloudState({ - @Default(false) bool loading, - @Default('') String backupFolderId, - @Default(null) DateTime? lastBackupAttempt, - @Default('') String toast, - @Default('') String error, - }) = _CloudState; -} diff --git a/lib/backup/bloc/keychain_cubit.dart b/lib/backup/bloc/keychain_cubit.dart deleted file mode 100644 index 584eb2776..000000000 --- a/lib/backup/bloc/keychain_cubit.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'package:bb_mobile/_pkg/consts/configs.dart'; -import 'package:bb_mobile/backup/bloc/keychain_state.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:recoverbull_dart/recoverbull_dart.dart'; - -class KeychainCubit extends Cubit { - KeychainCubit() : super(const KeychainState()); - - void clearError() => state.copyWith(error: ''); - - void updateSecret(String value) => emit(state.copyWith(secret: value)); - void confirmSecret(String value) => - emit(state.copyWith(secretConfirmed: state.secret == value)); - - Future clickSecureKey(String backupId, String backupKey) async { - if (state.secret.isEmpty || !state.secretConfirmed) { - emit(state.copyWith(error: 'confirm your secret')); - return; - } - - ///TODO: check if the backup is already saved - ///TODO: if it is, then show a toast - - // if (keychainapi.isEmpty) { - // emit(state.copyWith(error: 'keychain api is not set')); - // return; - // } - await _storeBackupKey(backupId, backupKey); - emit(state.copyWith(completed: true)); - } - - Future _storeBackupKey(String backupId, String backupKey) async { - try { - await KeyManagementService(keychainapi: keychainapi) - .storeBackupKey(backupId, backupKey, state.secret); - } catch (e) { - emit(state.copyWith(error: 'Failed to store backup key on server')); - } - } -} diff --git a/lib/backup/bloc/keychain_state.dart b/lib/backup/bloc/keychain_state.dart deleted file mode 100644 index 02a4384b0..000000000 --- a/lib/backup/bloc/keychain_state.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'keychain_state.freezed.dart'; - -@freezed -class KeychainState with _$KeychainState { - const factory KeychainState({ - @Default(false) bool completed, - @Default('') String secret, - @Default(false) bool secretConfirmed, - @Default('') String error, - }) = _KeychainState; -} diff --git a/lib/backup/bloc/manual_cubit.dart b/lib/backup/bloc/manual_cubit.dart deleted file mode 100644 index 3f8d063ff..000000000 --- a/lib/backup/bloc/manual_cubit.dart +++ /dev/null @@ -1,226 +0,0 @@ -import 'package:bb_mobile/_model/backup.dart'; -import 'package:bb_mobile/_model/wallet.dart'; -import 'package:bb_mobile/_pkg/backup/local.dart'; -import 'package:bb_mobile/_pkg/wallet/labels.dart'; -import 'package:bb_mobile/_repository/wallet/sensitive_wallet_storage.dart'; -import 'package:bb_mobile/backup/bloc/manual_state.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class ManualCubit extends Cubit { - ManualCubit({ - required this.wallets, - required this.walletSensitiveStorage, - required this.manager, - }) : super(const ManualState()); - - final FileSystemBackupManager manager; - final List wallets; - final WalletSensitiveStorageRepository walletSensitiveStorage; - - Future loadBackupData() async { - emit(state.copyWith(loading: true, error: '')); - final backups = []; - final selectedBackupOptions = state.selectedBackupOptions; - String? backupKeyMnemonic; - for (final wallet in wallets) { - var backup = Backup( - name: wallet.name ?? '', - network: wallet.network.name.toLowerCase(), - layer: wallet.baseWalletType.name.toLowerCase(), - script: wallet.scriptType.name.toLowerCase(), - type: wallet.type.name.toLowerCase(), - mnemonicFingerPrint: wallet.mnemonicFingerprint, - ); - final (seed, error) = await walletSensitiveStorage.readSeed( - fingerprintIndex: wallet.getRelatedSeedStorageString(), - ); - if (error != null) { - emit(state.copyWith(error: 'Error reading seed: ${error.message}')); - return; - } - - if (seed == null) { - emit(state.copyWith(error: 'Seed data is missing.')); - return; - } - if (selectedBackupOptions["mnemonic"] == true && - selectedBackupOptions["passphrase"] == true) { - if (wallet.hasPassphrase()) { - final sourceSeedPassphrases = seed.passphrases - .where((e) => e.sourceFingerprint == wallet.sourceFingerprint) - .toList(); - if (sourceSeedPassphrases.isEmpty) { - emit( - state.copyWith( - error: 'Passphrase not found for the wallet source fingerprint', - ), - ); - return; - } else { - backup = backup.copyWith( - mnemonic: seed.mnemonic.split(' '), - passphrase: sourceSeedPassphrases.first.passphrase, - ); - } - } else { - backup = backup.copyWith( - mnemonic: seed.mnemonic.split(' '), - passphrase: '', - ); - } - backupKeyMnemonic ??= seed.mnemonic; - } else { - backupKeyMnemonic ??= seed.mnemonic; - } - - if (selectedBackupOptions["descriptors"] == true) { - backup = backup.copyWith(descriptors: [wallet.getDescriptorCombined()]); - } - - if (selectedBackupOptions["labels"] == true) { - final walletLabels = WalletLabels(); - final labels = await walletLabels.txsToBip329( - wallet.transactions, - wallet.originString(), - ) - ..addAll( - await walletLabels.addressesToBip329( - wallet.myAddressBook, - wallet.originString(), - ), - ); - backup = backup.copyWith(labels: labels); - } - backups.add(backup); - } - emit( - state.copyWith( - loadedBackups: backups, - loading: false, - backupKeyMnemonic: backupKeyMnemonic ?? '', - ), - ); - } - - void toggleDescriptors() => _toggleBackupOption("descriptors"); - - void toggleLabels() => _toggleBackupOption("labels"); - - void _toggleBackupOption(String option) { - final selectedBackupOptions = - Map.from(state.selectedBackupOptions); - selectedBackupOptions[option] = !(selectedBackupOptions[option] ?? false); - emit(state.copyWith(selectedBackupOptions: selectedBackupOptions)); - } - - void toggleAllMnemonicAndPassphrase() { - final selectedBackupOptions = - Map.from(state.selectedBackupOptions); - final areBothConfirmed = selectedBackupOptions["mnemonic"] == true && - selectedBackupOptions["passphrase"] == true; - final newConfirmed = !areBothConfirmed; - selectedBackupOptions["mnemonic"] = newConfirmed; - selectedBackupOptions["passphrase"] = newConfirmed; - emit(state.copyWith(selectedBackupOptions: selectedBackupOptions)); - } - - Future saveEncryptedBackup() async { - if (state.loading) { - emit(state.copyWith(error: 'Backup already in progress')); - return; - } - final now = DateTime.now(); - if (state.lastBackupAttempt != null) { - final difference = now.difference(state.lastBackupAttempt!); - if (difference.inSeconds < 30) { - emit( - state.copyWith( - error: - 'Please wait ${30 - difference.inSeconds} seconds before creating another backup', - ), - ); - return; - } - } - emit(state.copyWith(loading: true, lastBackupAttempt: now)); - await loadBackupData(); - final backups = state.loadedBackups; - - if (backups.isEmpty) { - emit( - state.copyWith( - loading: false, - error: - 'No wallet details found. Please ensure your wallets have the necessary data available.', - ), - ); - return; - } - - try { - const String derivationPath = "m/1608'/0'"; - final (encData, err) = await manager.encryptBackups( - backups: backups, - derivationPath: derivationPath, - backupKeyMnemonic: state.backupKeyMnemonic, - ); - if (err != null) { - emit( - state.copyWith( - loading: false, - error: 'Failed to encrypt backups: ${err.message}', - ), - ); - return; - } - - final (filePath, errSave) = - await manager.saveEncryptedBackup(encrypted: encData!.$2); - if (errSave != null) { - emit( - state.copyWith( - loading: false, - error: 'Failed to save backup file:', - ), - ); - return; - } - final fileName = filePath?.split('/').last; - final backupId = fileName?.split('_').last.split('.').first; - emit( - state.copyWith( - backupId: backupId ?? '', - backupKey: encData.$1, - backupPath: filePath ?? '', - backupName: fileName ?? '', - loading: false, - ), - ); - } catch (e) { - emit( - state.copyWith( - loading: false, - error: 'An unexpected error occurred: $e', - ), - ); - } - } - - void clearError() => emit(state.copyWith(error: '')); - Future clearAndClose() async { - emit( - state.copyWith( - loadedBackups: [], - backupKeyMnemonic: '', - backupKey: '', - backupId: '', - backupPath: '', - backupName: '', - error: '', - loading: false, - selectedBackupOptions: {}, - ), - ); - await close(); - } -} diff --git a/lib/backup/bloc/manual_state.dart b/lib/backup/bloc/manual_state.dart deleted file mode 100644 index e45748643..000000000 --- a/lib/backup/bloc/manual_state.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:bb_mobile/_model/backup.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'manual_state.freezed.dart'; - -@freezed -class ManualState with _$ManualState { - const factory ManualState({ - @Default(false) bool loading, - @Default([]) List loadedBackups, - @Default({ - "mnemonic": true, - "passphrase": true, - "descriptors": true, - "labels": true, - "script": true, - }) - Map selectedBackupOptions, - @Default('') String backupKeyMnemonic, - @Default('') String backupId, - @Default('') String backupPath, - @Default('') String backupName, - @Default('') String backupKey, - @Default(null) DateTime? lastBackupAttempt, - @Default('') String error, - }) = _ManualState; -} diff --git a/lib/backup/keychain_page.dart b/lib/backup/keychain_page.dart deleted file mode 100644 index 330a01ce0..000000000 --- a/lib/backup/keychain_page.dart +++ /dev/null @@ -1,111 +0,0 @@ -import 'package:bb_mobile/_ui/app_bar.dart'; -import 'package:bb_mobile/_ui/toast.dart'; -import 'package:bb_mobile/backup/bloc/keychain_cubit.dart'; -import 'package:bb_mobile/backup/bloc/keychain_state.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:gap/gap.dart'; -import 'package:go_router/go_router.dart'; - -class KeychainBackupPage extends StatelessWidget { - const KeychainBackupPage({ - super.key, - required this.backupKey, - required this.backupId, - }); - - final String backupKey; - final String backupId; - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (_) => KeychainCubit(), - child: Scaffold( - backgroundColor: Colors.amber, - appBar: AppBar( - automaticallyImplyLeading: false, - elevation: 0, - flexibleSpace: BBAppBar( - text: 'Keychain Backup', - onBack: () => context.pop(), - ), - ), - body: BlocListener( - listener: (context, state) { - if (state.error.isNotEmpty) { - ScaffoldMessenger.of(context) - .showSnackBar(context.showToast(state.error)); - context.read().clearError(); - } - if (state.completed) { - ScaffoldMessenger.of(context).showSnackBar( - context.showToast('Backup key saved to keychain successfully'), - ); - context.pop(); - } - }, - child: BlocBuilder( - builder: (context, state) { - final cubit = context.read(); - return Padding( - padding: const EdgeInsets.all(20.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Column( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - SelectableText('Backup Key: $backupKey'), - const Gap(8), - SelectableText('Backup ID: $backupId'), - ], - ), - Column( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - SizedBox( - width: double.infinity, - child: TextFormField( - decoration: const InputDecoration( - labelText: 'Enter PIN', - ), - keyboardType: TextInputType.number, - maxLength: 6, - onChanged: (value) => cubit.updateSecret(value), - ), - ), - SizedBox( - width: double.infinity, - child: TextFormField( - decoration: - const InputDecoration(labelText: 'Confirm PIN'), - keyboardType: TextInputType.number, - obscureText: true, - maxLength: 6, - onChanged: (value) => cubit.confirmSecret(value), - ), - ), - ], - ), - if (state.secretConfirmed) - ElevatedButton( - onPressed: () => - cubit.clickSecureKey(backupId, backupKey), - child: const Text('Secure my backup key'), - ), - if (!state.secretConfirmed) - const Text( - 'PINs do not match! Please confirm your PIN.', - style: TextStyle(color: Colors.red), - ), - ], - ), - ); - }, - ), - ), - ), - ); - } -} diff --git a/lib/settings/settings_page.dart b/lib/settings/settings_page.dart index 1a542e9f4..a66242068 100644 --- a/lib/settings/settings_page.dart +++ b/lib/settings/settings_page.dart @@ -50,10 +50,6 @@ class _Screen extends StatelessWidget { const BitcoinSettingsButton(), const Gap(8), const ApplicationSettingsButton(), - const Gap(8), - const BackupBullButton(), - const Gap(8), - const RecoverBullButton(), const Gap(24), const Center( @@ -163,41 +159,6 @@ class SwapHistoryButton extends StatelessWidget { } } -class BackupBullButton extends StatelessWidget { - const BackupBullButton({super.key}); - - @override - Widget build(BuildContext context) { - return BBButton.textWithStatusAndRightArrow( - label: 'BackupBull', - onPressed: () { - final network = context.read().getBBNetwork; - final wallets = - context.read().walletsFromNetwork(network); - context.push('/backupbull', extra: wallets); - }, - ); - } -} - -class RecoverBullButton extends StatelessWidget { - const RecoverBullButton({super.key}); - - @override - Widget build(BuildContext context) { - return BBButton.textWithStatusAndRightArrow( - label: 'RecoverBull', - onPressed: () { - final network = context.read().getBBNetwork; - final wallets = context - .read() - .walletNotMainFromNetwork(network); - context.push('/recoverbull', extra: wallets); - }, - ); - } -} - class ApplicationSettingsButton extends StatelessWidget { const ApplicationSettingsButton({super.key}); From d5bd1b223cf8ca1d50b499bcd0b1330026a1370e Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Wed, 5 Feb 2025 04:30:16 -0500 Subject: [PATCH 103/401] refactor(wallet_settings): rename state file and clean up unused methods --- .../bloc/wallet_settings_cubit.dart | 176 +----------------- ...{state.dart => wallet_settings_state.dart} | 2 +- 2 files changed, 2 insertions(+), 176 deletions(-) rename lib/wallet_settings/bloc/{state.dart => wallet_settings_state.dart} (98%) diff --git a/lib/wallet_settings/bloc/wallet_settings_cubit.dart b/lib/wallet_settings/bloc/wallet_settings_cubit.dart index 778ad4cee..16cfaebf2 100644 --- a/lib/wallet_settings/bloc/wallet_settings_cubit.dart +++ b/lib/wallet_settings/bloc/wallet_settings_cubit.dart @@ -9,9 +9,8 @@ import 'package:bb_mobile/_repository/app_wallets_repository.dart'; import 'package:bb_mobile/_repository/wallet/sensitive_wallet_storage.dart'; import 'package:bb_mobile/_repository/wallet/wallet_storage.dart'; import 'package:bb_mobile/_repository/wallet_service.dart'; -import 'package:bb_mobile/_ui/alert.dart'; import 'package:bb_mobile/locator.dart'; -import 'package:bb_mobile/wallet_settings/bloc/state.dart'; +import 'package:bb_mobile/wallet_settings/bloc/wallet_settings_state.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:path_provider/path_provider.dart'; @@ -78,183 +77,11 @@ class WalletSettingsCubit extends Cubit { emit(state.copyWith(savedName: false)); } - Future loadBackupClicked() async { - final (seed, err) = await _walletSensRepository.readSeed( - fingerprintIndex: _wallet.getRelatedSeedStorageString(), - ); - if (err != null) { - emit(state.copyWith(errTestingBackup: err.toString())); - return; - } - - final words = seed!.mnemonic.split(' '); - final shuffled = words.toList()..shuffle(); - - emit( - state.copyWith( - testMnemonicOrder: [], - mnemonic: words, - errTestingBackup: '', - password: - seed.getPassphraseFromIndex(_wallet.sourceFingerprint).passphrase, - shuffledMnemonic: shuffled, - ), - ); - } - - void wordClicked(int shuffledIdx) { - emit(state.copyWith(errTestingBackup: '')); - final testMnemonic = state.testMnemonicOrder.toList(); - if (testMnemonic.length == 12) return; - - final (word, isSelected, actualIdx) = state.shuffleElementAt(shuffledIdx); - if (isSelected) return; - if (actualIdx != testMnemonic.length) { - invalidTestOrderClicked(); - return; - } - - testMnemonic.add( - ( - word: word, - shuffleIdx: shuffledIdx, - selectedActualIdx: actualIdx, - ), - ); - - emit(state.copyWith(testMnemonicOrder: testMnemonic)); - } - - void word24Clicked(int shuffledIdx) { - emit(state.copyWith(errTestingBackup: '')); - final testMnemonic = state.testMnemonicOrder.toList(); - if (testMnemonic.length == 24) return; - - final (word, isSelected, actualIdx) = state.shuffleElementAt(shuffledIdx); - if (isSelected) return; - if (actualIdx != testMnemonic.length) { - invalidTestOrderClicked(); - return; - } - - testMnemonic.add( - ( - word: word, - shuffleIdx: shuffledIdx, - selectedActualIdx: actualIdx, - ), - ); - - emit(state.copyWith(testMnemonicOrder: testMnemonic)); - } - - Future invalidTestOrderClicked() async { - emit( - state.copyWith( - testMnemonicOrder: [], - errTestingBackup: 'Invalid order', - ), - ); - BBAlert.showErrorAlertPopUp( - err: 'Invalid mnemonic order.', - onClose: () { - emit(state.copyWith(errTestingBackup: '')); - }, - ); - await Future.delayed(const Duration(milliseconds: 500)); - final shuffled = state.mnemonic.toList()..shuffle(); - emit( - state.copyWith( - shuffledMnemonic: shuffled, - ), - ); - } - - void changePassword(String password) { - emit( - state.copyWith( - testBackupPassword: password, - errTestingBackup: '', - ), - ); - } - - Future testBackupClicked() async { - emit(state.copyWith(testingBackup: true, errTestingBackup: '')); - final words = state.testMneString(); - final password = state.testBackupPassword; - final (seed, err) = await _walletSensRepository.readSeed( - fingerprintIndex: _wallet.getRelatedSeedStorageString(), - ); - - if (err != null) { - emit( - state.copyWith( - errTestingBackup: err.toString(), - testingBackup: false, - ), - ); - return; - } - - final mne = seed!.mnemonic == words; - - final psd = - seed.getPassphraseFromIndex(_wallet.sourceFingerprint).passphrase == - password; - if (!mne) { - { - emit( - state.copyWith( - errTestingBackup: 'Your seed words are incorrect', - testingBackup: false, - ), - ); - return; - } - } - if (!psd) { - emit( - state.copyWith( - errTestingBackup: 'Your passphrase is incorrect', - testingBackup: false, - ), - ); - return; - } - - final wallet = - _wallet.copyWith(backupTested: true, lastBackupTested: DateTime.now()); - - await _appWalletsRepository - .getWalletServiceById(wallet.id) - ?.updateWallet(wallet, updateTypes: [UpdateWalletTypes.settings]); - - emit( - state.copyWith( - backupTested: true, - testingBackup: false, - ), - ); - clearSensitive(); - } - Future resetBackupTested() async { await Future.delayed(const Duration(milliseconds: 800)); emit(state.copyWith(backupTested: false)); } - void clearnMnemonic() { - emit( - state.copyWith( - mnemonic: [ - for (var i = 0; i < 12; i++) '', - ], - testBackupPassword: '', - ), - ); - } - Future backupToSD() async { emit(state.copyWith(savingFile: true, errSavingFile: '')); final (seed, err) = await _walletSensRepository.readSeed( @@ -574,7 +401,6 @@ class WalletSettingsCubit extends Cubit { } Future clearSensitive() async { - clearnMnemonic(); emit( state.copyWith( password: '', diff --git a/lib/wallet_settings/bloc/state.dart b/lib/wallet_settings/bloc/wallet_settings_state.dart similarity index 98% rename from lib/wallet_settings/bloc/state.dart rename to lib/wallet_settings/bloc/wallet_settings_state.dart index 98d1cdcfd..64cd453b0 100644 --- a/lib/wallet_settings/bloc/state.dart +++ b/lib/wallet_settings/bloc/wallet_settings_state.dart @@ -1,7 +1,7 @@ import 'package:bb_mobile/_model/wallet.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -part 'state.freezed.dart'; +part 'wallet_settings_state.freezed.dart'; @freezed class WalletSettingsState with _$WalletSettingsState { From f222b51da979efe099d83cd8717aaff0c7eed7bc Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Wed, 5 Feb 2025 04:36:51 -0500 Subject: [PATCH 104/401] refactor(recover): remove unused state and cubit files --- lib/recover/bloc/cloud_cubit.dart | 141 ----------------- lib/recover/bloc/cloud_state.dart | 20 --- lib/recover/bloc/keychain_cubit.dart | 43 ----- lib/recover/bloc/keychain_state.dart | 13 -- lib/recover/bloc/manual_cubit.dart | 229 --------------------------- lib/recover/bloc/manual_state.dart | 17 -- lib/recover/cloud_page.dart | 164 ------------------- lib/recover/keychain_page.dart | 92 ----------- lib/recover/manual_page.dart | 175 -------------------- 9 files changed, 894 deletions(-) delete mode 100644 lib/recover/bloc/cloud_cubit.dart delete mode 100644 lib/recover/bloc/cloud_state.dart delete mode 100644 lib/recover/bloc/keychain_cubit.dart delete mode 100644 lib/recover/bloc/keychain_state.dart delete mode 100644 lib/recover/bloc/manual_cubit.dart delete mode 100644 lib/recover/bloc/manual_state.dart delete mode 100644 lib/recover/cloud_page.dart delete mode 100644 lib/recover/keychain_page.dart delete mode 100644 lib/recover/manual_page.dart diff --git a/lib/recover/bloc/cloud_cubit.dart b/lib/recover/bloc/cloud_cubit.dart deleted file mode 100644 index b383fda39..000000000 --- a/lib/recover/bloc/cloud_cubit.dart +++ /dev/null @@ -1,141 +0,0 @@ -import 'dart:convert'; - -import 'package:bb_mobile/_pkg/backup/google_drive.dart'; -import 'package:bloc/bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:googleapis/drive/v3.dart'; - -part 'cloud_cubit.freezed.dart'; -part 'cloud_state.dart'; - -class CloudCubit extends Cubit { - final GoogleDriveBackupManager manager; - CloudCubit({required this.manager}) : super(CloudState()); - - void clearToast() => emit(state.copyWith(toast: '', loading: false)); - - void clearError() => emit(state.copyWith(error: '', loading: false)); - - Future connect() async { - try { - emit(state.copyWith(loading: true)); - final (folderId, err) = await manager.connect(); - - if (folderId != null) { - emit( - state.copyWith( - backupFolderId: folderId, - loading: false, - ), - ); - } else if (err != null) { - emit( - state.copyWith( - error: err.message, - loading: false, - ), - ); - } - } catch (e) { - emit( - state.copyWith( - error: "GoogleDrive Error: $e", - loading: false, - ), - ); - } - } - - Future readAllBackups({bool forceRefresh = false}) async { - try { - if (!forceRefresh && - state.availableBackups.isNotEmpty && - state.isCacheValid) { - emit(state.copyWith(loading: false)); - return; - } - if (state.backupFolderId.isEmpty) { - await connect(); - } - emit(state.copyWith(loading: true)); - if (state.backupFolderId.isEmpty) { - emit( - state.copyWith( - loading: false, - error: "Google Drive connection failed.", - ), - ); - return; - } - final (availableBackups, err) = await manager.loadAllEncryptedBackupFiles( - backupFolder: state.backupFolderId, - ); - - if (err != null) { - emit( - state.copyWith( - loading: false, - error: "Failed to list backup files: ${err.message}", - ), - ); - return; - } - - if (availableBackups != null && availableBackups.isNotEmpty) { - emit( - state.copyWith( - loading: false, - availableBackups: availableBackups, - lastFetchTime: DateTime.now(), - ), - ); - } else { - emit(state.copyWith(loading: false, error: "No backup files found")); - } - } catch (e) { - emit( - state.copyWith(loading: false, error: "Failed to read all backups: $e"), - ); - } - } - - void setCacheValidityDuration(Duration duration) { - emit(state.copyWith(cacheValidityDuration: duration)); - } - - Future refreshBackups() => readAllBackups(forceRefresh: true); - - Future loadEncrypted(String fileName) async { - if (state.backupFolderId.isEmpty) await connect(); - emit(state.copyWith(loading: true)); - final metaData = - await manager.fetchMediaStream(file: state.availableBackups[fileName]!); - final (loadEncryptedBackup, err) = await manager.loadEncryptedBackup( - encrypted: utf8.decode(metaData), - ); - if (err != null) { - emit( - state.copyWith( - loading: false, - error: "Failed to read backup: ${err.message}", - ), - ); - return; - } - emit( - state.copyWith( - toast: "Successfully loaded backup", - loading: false, - selectedBackup: ( - loadEncryptedBackup?['backupId'] ?? '', - jsonEncode(loadEncryptedBackup) - ), - ), - ); - } - - void disconnect() { - manager.disconnect(); - emit(state.copyWith(backupFolderId: '')); - } -} diff --git a/lib/recover/bloc/cloud_state.dart b/lib/recover/bloc/cloud_state.dart deleted file mode 100644 index 649ba6d7a..000000000 --- a/lib/recover/bloc/cloud_state.dart +++ /dev/null @@ -1,20 +0,0 @@ -part of 'cloud_cubit.dart'; - -@freezed -class CloudState with _$CloudState { - factory CloudState({ - @Default(false) bool loading, - @Default('') String backupFolderId, - @Default({}) Map availableBackups, - @Default(('', '')) (String, String) selectedBackup, - DateTime? lastFetchTime, - @Default(Duration(minutes: 5)) Duration cacheValidityDuration, - @Default('') String toast, - @Default('') String error, - }) = _CloudState; - const CloudState._(); - bool get isCacheValid { - if (lastFetchTime == null) return false; - return DateTime.now().difference(lastFetchTime!) < cacheValidityDuration; - } -} diff --git a/lib/recover/bloc/keychain_cubit.dart b/lib/recover/bloc/keychain_cubit.dart deleted file mode 100644 index 0cd2b62f4..000000000 --- a/lib/recover/bloc/keychain_cubit.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'package:bb_mobile/_pkg/consts/configs.dart'; -import 'package:bb_mobile/_pkg/file_picker.dart'; -import 'package:bb_mobile/recover/bloc/keychain_state.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:recoverbull_dart/recoverbull_dart.dart'; - -class KeychainCubit extends Cubit { - KeychainCubit({required String backupId, required this.filePicker}) - : super(KeychainState(backupId: backupId)); - - final FilePick filePicker; - - void clearError() => emit(state.copyWith(error: '')); - void updateSecret(String value) => emit(state.copyWith(secret: value)); - - Future clickRecoverKey() async { - if (state.backupId.isEmpty) { - emit(state.copyWith(error: 'backup id is missing')); - return; - } - - if (state.secret.length != 6) { - emit(state.copyWith(error: 'pin should be 6 digits long')); - return; - } - - _recoverBackupKey(state.secret, state.backupId); - } - - Future _recoverBackupKey(String secret, String backupId) async { - try { - if (keychainapi.isEmpty) { - emit(state.copyWith(error: 'keychain api is not set')); - return; - } - final backupKey = await KeyManagementService(keychainapi: keychainapi) - .recoverBackupKey(backupId, secret); - emit(state.copyWith(backupKey: backupKey)); - } on KeyManagementException catch (e) { - emit(state.copyWith(error: e.message)); - } - } -} diff --git a/lib/recover/bloc/keychain_state.dart b/lib/recover/bloc/keychain_state.dart deleted file mode 100644 index d4468096f..000000000 --- a/lib/recover/bloc/keychain_state.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'keychain_state.freezed.dart'; - -@freezed -class KeychainState with _$KeychainState { - const factory KeychainState({ - @Default('') String error, - @Default('') String backupKey, - required String backupId, - @Default('') String secret, - }) = _KeychainState; -} diff --git a/lib/recover/bloc/manual_cubit.dart b/lib/recover/bloc/manual_cubit.dart deleted file mode 100644 index 29d2a6033..000000000 --- a/lib/recover/bloc/manual_cubit.dart +++ /dev/null @@ -1,229 +0,0 @@ -import 'dart:convert'; - -import 'package:bb_mobile/_model/backup.dart'; -import 'package:bb_mobile/_model/wallet.dart'; -import 'package:bb_mobile/_pkg/file_picker.dart'; -import 'package:bb_mobile/_pkg/wallet/bdk/sensitive_create.dart'; -import 'package:bb_mobile/_pkg/wallet/create.dart'; -import 'package:bb_mobile/_pkg/wallet/create_sensitive.dart'; -import 'package:bb_mobile/_pkg/wallet/lwk/sensitive_create.dart'; -import 'package:bb_mobile/_repository/wallet/sensitive_wallet_storage.dart'; -import 'package:bb_mobile/_repository/wallet/wallet_storage.dart'; -import 'package:bb_mobile/recover/bloc/manual_state.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:hex/hex.dart'; -import 'package:recoverbull_dart/recoverbull_dart.dart'; - -class ManualCubit extends Cubit { - ManualCubit({ - required this.bdkSensitiveCreate, - required this.lwkSensitiveCreate, - required this.walletSensitiveCreate, - required this.walletsStorageRepository, - required this.walletCreate, - required this.wallets, - required this.walletSensitiveStorage, - this.filePicker, - }) : super(const ManualState()); - - FilePick? filePicker; - final List wallets; - final WalletSensitiveStorageRepository walletSensitiveStorage; - final WalletsStorageRepository walletsStorageRepository; - final WalletSensitiveCreate walletSensitiveCreate; - final BDKSensitiveCreate bdkSensitiveCreate; - final WalletCreate walletCreate; - final LWKSensitiveCreate lwkSensitiveCreate; - - void updateBackupKey(String value) => emit(state.copyWith(backupKey: value)); - void clearError() => emit(state.copyWith(error: '')); - - Future selectFileFromFs() async { - if (filePicker == null) { - return; - } - final (file, error) = await filePicker!.pickFile(); - - if (error != null) { - emit(state.copyWith(error: error.toString())); - return; - } - - if (file == null || file.isEmpty) { - emit(state.copyWith(error: 'Empty file')); - return; - } - final decodeEncryptedFile = utf8.decode(HEX.decode(file)); - final id = jsonDecode(decodeEncryptedFile)['backupId']?.toString() ?? ''; - if (decodeEncryptedFile.isEmpty || id.isEmpty) { - emit(state.copyWith(error: 'Invalid backup')); - return; - } - setSelectedBackup(id, decodeEncryptedFile); - } - - void setSelectedBackup(String id, String encrypted) => - emit(state.copyWith(backupId: id, encrypted: encrypted)); - - Future clickRecover() async { - final recovered = await _recoverBackup(); - if (recovered) emit(state.copyWith(recovered: true)); - } - - Future _recoverBackup() async { - if (state.backupKey.length != 64) { - emit(state.copyWith(error: 'Backup key should be 64 chars')); - return false; - } - - try { - final plaintext = - await BackupService.restoreBackup(state.encrypted, state.backupKey); - final decodedJson = jsonDecode(plaintext) as List; - - final backups = decodedJson - .map((item) => Backup.fromJson(item as Map)) - .toList(); - for (final backup in backups) { - final network = switch (backup.network.toLowerCase()) { - 'mainnet' => BBNetwork.Mainnet, - 'testnet' => BBNetwork.Testnet, - _ => null - }; - - final layer = switch (backup.layer.toLowerCase()) { - 'bitcoin' => BaseWalletType.Bitcoin, - 'liquid' => BaseWalletType.Liquid, - _ => null - }; - - final script = switch (backup.script.toLowerCase()) { - 'bip44' => ScriptType.bip44, - 'bip49' => ScriptType.bip49, - 'bip84' => ScriptType.bip84, - _ => null - }; - - final type = switch (backup.type.toLowerCase()) { - 'main' => BBWalletType.main, - 'xpub' => BBWalletType.xpub, - 'words' => BBWalletType.words, - 'descriptors' => BBWalletType.descriptors, - 'coldcard' => BBWalletType.coldcard, - _ => null - }; - - if (network == null || - layer == null || - script == null || - type == null) { - return false; - } - - if (backup.mnemonic.isNotEmpty) { - await _addOrUpdateWallet( - network, - layer, - script, - type, - backup.mnemonic.join(' '), - backup.passphrase, - ); - } else { - //find the mnemonic & passphrase associated with the fingerprint. - - for (final wallet in wallets) { - if (wallet.mnemonicFingerprint == backup.mnemonicFingerPrint) { - final seedStorageString = wallet.getRelatedSeedStorageString(); - final (seed, error) = await walletSensitiveStorage.readSeed( - fingerprintIndex: seedStorageString, - ); - if (error != null) { - emit( - state.copyWith( - error: 'Error reading seed: ${error.message}', - ), - ); - return false; - } - if (seed == null) { - emit(state.copyWith(error: 'Seed data is missing.')); - return false; - } - final passphrase = wallet.hasPassphrase() - ? seed.passphrases - .firstWhere( - (e) => e.sourceFingerprint == wallet.sourceFingerprint, - ) - .passphrase - : ''; - await _addOrUpdateWallet( - network, - layer, - script, - type, - seed.mnemonic, - passphrase, - ); - } else { - emit(state.copyWith(error: 'Backup does not match any wallet')); - return false; - } - } - } - } - return true; - } catch (e) { - print(e); - emit(state.copyWith(error: 'Invalid backup key or file')); - return false; - } - } - - Future _addOrUpdateWallet( - BBNetwork network, - BaseWalletType layer, - ScriptType script, - BBWalletType type, - String mnemonic, - String passphrase, - ) async { - try { - final (seed, error) = - await walletSensitiveCreate.mnemonicSeed(mnemonic, network); - if (seed == null) return; - - await walletSensitiveStorage.newSeed(seed: seed); - - Wallet? wallet; - switch (layer) { - case BaseWalletType.Bitcoin: - final (btcWallet, btcError) = await bdkSensitiveCreate.oneFromBIP39( - seed: seed, - passphrase: passphrase, - scriptType: script, - network: network, - walletType: type, - walletCreate: walletCreate, - ); - wallet = btcWallet; - - case BaseWalletType.Liquid: - final (liqWallet, liqError) = - await lwkSensitiveCreate.oneLiquidFromBIP39( - seed: seed, - passphrase: passphrase, - scriptType: script, - network: network, - walletType: type, - walletCreate: walletCreate, - ); - wallet = liqWallet; - } - - await walletsStorageRepository.newWallet(wallet!); - } catch (_) { - rethrow; - } - } -} diff --git a/lib/recover/bloc/manual_state.dart b/lib/recover/bloc/manual_state.dart deleted file mode 100644 index eea3a7a8c..000000000 --- a/lib/recover/bloc/manual_state.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:googleapis/drive/v3.dart'; - -part 'manual_state.freezed.dart'; - -@freezed -class ManualState with _$ManualState { - const factory ManualState({ - @Default('') String error, - @Default(false) bool loading, - @Default([]) List availableBackups, - @Default(false) bool recovered, - @Default('') String backupKey, - @Default('') String backupId, - @Default('') String encrypted, - }) = _ManualState; -} diff --git a/lib/recover/cloud_page.dart b/lib/recover/cloud_page.dart deleted file mode 100644 index 093bf3ee5..000000000 --- a/lib/recover/cloud_page.dart +++ /dev/null @@ -1,164 +0,0 @@ -import 'package:bb_mobile/_ui/app_bar.dart'; -import 'package:bb_mobile/_ui/components/button.dart'; -import 'package:bb_mobile/_ui/components/text.dart'; -import 'package:bb_mobile/_ui/toast.dart'; -import 'package:bb_mobile/recover/bloc/cloud_cubit.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:gap/gap.dart'; -import 'package:go_router/go_router.dart'; -import 'package:intl/intl.dart'; - -class CloudPage extends StatefulWidget { - const CloudPage({super.key}); - @override - State createState() => _CloudPageState(); -} - -class _CloudPageState extends State { - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) { - context.read().readAllBackups(); - } - }); - } - - Widget buildBackupsList(CloudState state) { - if (state.loading) { - return const Center( - child: CircularProgressIndicator(), - ); - } - if (state.availableBackups.isEmpty) { - return Center( - child: BBButton.text( - onPressed: () => context.read().refreshBackups(), - label: 'Refresh', - ), - ); - } - - final backupEntries = state.availableBackups.entries.toList() - ..sort((a, b) => b.key.compareTo(a.key)); - - return Column( - children: [ - if (state.lastFetchTime != null) ...[ - Padding( - padding: const EdgeInsets.all(8.0), - child: BBText.bodySmall( - 'Last updated: ${DateFormat('MMM d, h:mm a').format(state.lastFetchTime!)}', - isBold: true, - ), - ), - ], - Expanded( - child: RefreshIndicator( - onRefresh: () => context.read().refreshBackups(), - child: ListView.separated( - itemCount: backupEntries.length, - padding: const EdgeInsets.symmetric(horizontal: 16), - separatorBuilder: (_, __) => const Divider(), - itemBuilder: (context, index) { - final entry = backupEntries[index]; - return BackupTile( - fileName: entry.key, - onFileSelected: (fileName) { - context.read().loadEncrypted(fileName); - context.pop(); - }, - ); - }, - ), - ), - ), - ], - ); - } - - @override - Widget build(BuildContext context) { - return BlocConsumer( - listener: (context, state) { - if (state.toast.isNotEmpty && state.toast != '') { - ScaffoldMessenger.of(context) - .showSnackBar(context.showToast(state.toast)); - context.read().clearToast(); - } - if (state.error.isNotEmpty && state.error != '') { - ScaffoldMessenger.of(context) - .showSnackBar(context.showToast(state.error)); - - context.read().clearError(); - } - }, - builder: (context, state) { - return Scaffold( - appBar: AppBar( - elevation: 0, - automaticallyImplyLeading: false, - flexibleSpace: BBAppBar( - text: 'Cloud Backup', - onBack: () => context.pop(), - ), - ), - body: Center( - child: state.loading - ? const CircularProgressIndicator() - : Column( - children: [ - Expanded( - child: buildBackupsList(state), - ), - const Gap(10), - BBButton.big( - onPressed: () { - context.read().disconnect(); - context.pop(); - }, - label: "Logout", - ), - const Gap(20), - ], - ), - ), - ); - }, - ); - } -} - -class BackupTile extends StatelessWidget { - const BackupTile({ - super.key, - required this.fileName, - required this.onFileSelected, - }); - - final String fileName; - final void Function(String) onFileSelected; - - @override - Widget build(BuildContext context) { - final cleanFileName = fileName.replaceAll(".json", ""); - final parts = cleanFileName.split('_'); - final backupId = parts.last; - final dateTimeString = parts.first; - final dateTime = - DateTime.fromMillisecondsSinceEpoch(int.parse(dateTimeString)); - - return ListTile( - onTap: () => onFileSelected(fileName), - title: BBText.body( - backupId, - isBold: true, - ), - subtitle: BBText.bodySmall( - 'Created at: ${DateFormat('MMM d, h:mm a').format(dateTime)}', - ), - ); - } -} diff --git a/lib/recover/keychain_page.dart b/lib/recover/keychain_page.dart deleted file mode 100644 index a8ba4bfbe..000000000 --- a/lib/recover/keychain_page.dart +++ /dev/null @@ -1,92 +0,0 @@ -import 'package:bb_mobile/_pkg/file_picker.dart'; -import 'package:bb_mobile/_ui/app_bar.dart'; -import 'package:bb_mobile/_ui/components/button.dart'; -import 'package:bb_mobile/locator.dart'; -import 'package:bb_mobile/recover/bloc/keychain_cubit.dart'; -import 'package:bb_mobile/recover/bloc/keychain_state.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:go_router/go_router.dart'; - -class KeychainRecoverPage extends StatelessWidget { - const KeychainRecoverPage({super.key, required this.backupId}); - - final String backupId; - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (_) => KeychainCubit( - filePicker: locator(), - backupId: backupId, - ), - child: Scaffold( - backgroundColor: Colors.amber, - appBar: AppBar( - automaticallyImplyLeading: false, - flexibleSpace: BBAppBar( - text: 'Recover Backup', - onBack: () => context.pop(), - ), - ), - body: BlocListener( - listener: (context, state) { - if (state.error.isNotEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(state.error), - backgroundColor: Colors.red, - ), - ); - context.read().clearError(); - } - if (state.backupKey.isNotEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Backup Key recovered'), - backgroundColor: Colors.green, - ), - ); - } - }, - child: BlocBuilder( - builder: (context, state) { - final cubit = context.read(); - - final backupKey = state.backupKey; - final secret = state.secret; - - return Column( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - if (backupKey.isEmpty && backupId.isNotEmpty) - Center( - child: SizedBox( - width: 100, - child: TextFormField( - decoration: - const InputDecoration(labelText: 'Enter PIN'), - keyboardType: TextInputType.number, - maxLength: 6, - onChanged: (value) => cubit.updateSecret(value), - ), - ), - ), - if (backupKey.isEmpty && - backupId.isNotEmpty && - secret.length == 6) - BBButton.big( - label: 'Recover Backup Key', - center: true, - onPressed: () => cubit.clickRecoverKey(), - ), - if (backupKey.isNotEmpty) SelectableText(backupKey), - ], - ); - }, - ), - ), - ), - ); - } -} diff --git a/lib/recover/manual_page.dart b/lib/recover/manual_page.dart deleted file mode 100644 index b850620e3..000000000 --- a/lib/recover/manual_page.dart +++ /dev/null @@ -1,175 +0,0 @@ -import 'package:bb_mobile/_model/wallet.dart'; -import 'package:bb_mobile/_pkg/backup/google_drive.dart'; -import 'package:bb_mobile/_pkg/consts/keys.dart'; -import 'package:bb_mobile/_pkg/file_picker.dart'; -import 'package:bb_mobile/_pkg/wallet/bdk/sensitive_create.dart'; -import 'package:bb_mobile/_pkg/wallet/create.dart'; -import 'package:bb_mobile/_pkg/wallet/create_sensitive.dart'; -import 'package:bb_mobile/_pkg/wallet/lwk/sensitive_create.dart'; -import 'package:bb_mobile/_repository/wallet/sensitive_wallet_storage.dart'; -import 'package:bb_mobile/_repository/wallet/wallet_storage.dart'; -import 'package:bb_mobile/_ui/app_bar.dart'; -import 'package:bb_mobile/_ui/components/button.dart'; -import 'package:bb_mobile/_ui/toast.dart'; -import 'package:bb_mobile/locator.dart'; -import 'package:bb_mobile/recover/bloc/cloud_cubit.dart'; -import 'package:bb_mobile/recover/bloc/manual_cubit.dart'; -import 'package:bb_mobile/recover/bloc/manual_state.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:gap/gap.dart'; -import 'package:go_router/go_router.dart'; - -class ManualRecoverPage extends StatelessWidget { - const ManualRecoverPage({super.key, required this.wallets}); - - final List wallets; - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (_) => ManualCubit( - filePicker: locator(), - walletCreate: locator(), - walletSensitiveCreate: locator(), - walletsStorageRepository: locator(), - wallets: wallets, - walletSensitiveStorage: locator(), - bdkSensitiveCreate: locator(), - lwkSensitiveCreate: locator(), - ), - child: Scaffold( - backgroundColor: Colors.amber, - appBar: AppBar( - automaticallyImplyLeading: false, - elevation: 0, - flexibleSpace: BBAppBar( - text: 'Recover Backup', - onBack: () => context.pop(), - buttonKey: UIKeys.settingsBackButton, - ), - ), - body: BlocListener( - listener: (context, state) async { - if (state.error.isNotEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - context.showToast(state.error), - ); - context.read().clearError(); - } - if (state.recovered) { - ScaffoldMessenger.of(context).showSnackBar( - context.showToast('Recovery completed'), - ); - context.go('/home'); - } - }, - child: BlocBuilder( - builder: (context, state) { - final cubit = context.read(); - - return Padding( - padding: const EdgeInsets.all(20.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - if (!state.recovered && - state.backupId.isNotEmpty && - state.backupKey.isEmpty) - ElevatedButton( - onPressed: () => context.push( - '/keychain-recover', - extra: state.backupId, - ), - child: const Text('Keychain'), - ), - if (!state.recovered && state.backupId.isNotEmpty) - TextFormField( - decoration: - const InputDecoration(labelText: 'Backup Key'), - maxLength: 64, - onChanged: (value) => cubit.updateBackupKey(value), - ), - if (!state.recovered && state.backupKey.isEmpty) - Column( - children: [ - BBButton.big( - label: 'Select file from FileSystem', - center: true, - onPressed: () => cubit.selectFileFromFs(), - ), - const Gap(20), - BlocProvider( - create: (context) => CloudCubit( - manager: locator(), - ), - child: BlocConsumer( - listener: (context, state) { - if (state.error.isNotEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - context.showToast(state.error), - ); - context.read().clearError(); - } - if (state.toast.isNotEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - context.showToast(state.toast), - ); - cubit.setSelectedBackup( - state.selectedBackup.$1, - state.selectedBackup.$2, - ); - } - }, - builder: (context, state) { - return BBButton.big( - loading: state.loading, - label: 'Select file from Cloud', - center: true, - onPressed: () => { - context.push( - '/cloud-backup', - extra: context.read(), - ), - }, - ); - }, - ), - ), - ], - ), - if (!state.recovered && state.backupKey.isNotEmpty) - BBButton.big( - label: 'Recover', - center: true, - onPressed: () => cubit.clickRecover(), - ), - ], - ), - ); - }, - ), - ), - ), - ); - } - - void showModalPopup({ - required BuildContext context, - required List children, - }) { - showCupertinoModalPopup( - context: context, - builder: (BuildContext modalContext) => Container( - height: 700, - decoration: const BoxDecoration( - color: Colors.white, - ), - child: Column( - children: children, - ), - ), - ); - } -} From 3306035c8aaae210dfd3086aa8fe9bc443c4aa76 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Wed, 5 Feb 2025 04:38:01 -0500 Subject: [PATCH 105/401] feat(backup_settings): add BackupSettingsState with verification properties --- .../bloc/backup_settings_cubit.dart | 517 ++++++++++++++++++ .../bloc/backup_settings_state.dart | 63 +++ 2 files changed, 580 insertions(+) create mode 100644 lib/wallet_settings/bloc/backup_settings_cubit.dart create mode 100644 lib/wallet_settings/bloc/backup_settings_state.dart diff --git a/lib/wallet_settings/bloc/backup_settings_cubit.dart b/lib/wallet_settings/bloc/backup_settings_cubit.dart new file mode 100644 index 000000000..389a72b3b --- /dev/null +++ b/lib/wallet_settings/bloc/backup_settings_cubit.dart @@ -0,0 +1,517 @@ +import 'dart:convert'; + +import 'package:bb_mobile/_model/backup.dart'; +import 'package:bb_mobile/_model/seed.dart'; +import 'package:bb_mobile/_model/wallet.dart'; +import 'package:bb_mobile/_pkg/backup/google_drive.dart'; +import 'package:bb_mobile/_pkg/backup/local.dart'; +import 'package:bb_mobile/_pkg/error.dart'; +import 'package:bb_mobile/_repository/app_wallets_repository.dart'; +import 'package:bb_mobile/_repository/wallet/sensitive_wallet_storage.dart'; +import 'package:bb_mobile/_repository/wallet_service.dart'; +import 'package:bb_mobile/locator.dart'; +import 'package:bb_mobile/wallet_settings/bloc/backup_settings_state.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +BackupSettingsCubit createBackupSettingsCubit(String walletId) { + final appWalletsRepo = locator(); + final activeWallet = appWalletsRepo.getWalletById(walletId); + return BackupSettingsCubit( + activeWallet: activeWallet!, + wallets: appWalletsRepo.allWallets, + appWalletsRepository: appWalletsRepo, + walletSensRepository: locator(), + manager: locator(), + driveManager: locator(), + ); +} + +class BackupSettingsCubit extends Cubit { + BackupSettingsCubit({ + required Wallet activeWallet, + required List wallets, + required AppWalletsRepository appWalletsRepository, + required WalletSensitiveStorageRepository walletSensRepository, + required FileSystemBackupManager manager, + required GoogleDriveBackupManager driveManager, + }) : _walletSensRepository = walletSensRepository, + _appWalletsRepository = appWalletsRepository, + _wallet = activeWallet, + _wallets = wallets, + _manager = manager, + _driveManager = driveManager, + super(const BackupSettingsState()); + + final WalletSensitiveStorageRepository _walletSensRepository; + final AppWalletsRepository _appWalletsRepository; + final Wallet _wallet; + final List _wallets; + final FileSystemBackupManager _manager; + final GoogleDriveBackupManager _driveManager; + static const _kDelayDuration = Duration(milliseconds: 800); + static const _kShuffleDelay = Duration(milliseconds: 500); + static const _kMinBackupInterval = Duration(seconds: 5); + static const _kDerivationPath = "m/1608'/0'"; + + // Seed loading helper + Future<(Seed?, String?)> _loadWalletSeed(Wallet wallet) async { + final (seed, err) = await _walletSensRepository.readSeed( + fingerprintIndex: wallet.getRelatedSeedStorageString(), + ); + return (seed, err?.toString()); + } + + // Backup verification methods + Future loadBackupForVerification() async { + final (seed, error) = await _loadWalletSeed(_wallet); + if (error != null || seed == null) { + emit(state.copyWith(errTestingBackup: error ?? 'Seed data not found')); + return; + } + + _emitBackupState(seed); + } + + void _emitBackupState(Seed seed) { + final words = seed.mnemonic.split(' '); + final shuffled = words.toList()..shuffle(); + emit( + state.copyWith( + testMnemonicOrder: [], + mnemonic: words, + errTestingBackup: '', + password: + seed.getPassphraseFromIndex(_wallet.sourceFingerprint).passphrase, + shuffledMnemonic: shuffled, + ), + ); + } + + Future testBackupClicked() async { + emit(state.copyWith(testingBackup: true, errTestingBackup: '')); + + final words = state.testMneString(); + final password = state.testBackupPassword; + final seed = await _loadSeedData(_wallet); + + if (seed == null) { + emit( + state.copyWith( + errTestingBackup: 'Unable to load wallet data', + testingBackup: false, + ), + ); + return; + } + + if (!_verifyWords(seed.mnemonic, words)) { + emit( + state.copyWith( + errTestingBackup: 'Invalid seed words', + testingBackup: false, + ), + ); + return; + } + + if (!_verifyPassphrase(seed, password)) { + emit( + state.copyWith( + errTestingBackup: 'Invalid passphrase', + testingBackup: false, + ), + ); + return; + } + + await _updateWalletBackupStatus(); + _emitBackupTestSuccessState(); + } + + bool _verifyWords(String seedMnemonic, String testWords) => + seedMnemonic == testWords; + + bool _verifyPassphrase(Seed seed, String password) => + seed.getPassphraseFromIndex(_wallet.sourceFingerprint).passphrase == + password; + + Future _loadSeedData(Wallet wallet) async { + final (seed, err) = await _walletSensRepository.readSeed( + fingerprintIndex: wallet.getRelatedSeedStorageString(), + ); + if (err != null) { + emit(state.copyWith(errTestingBackup: err.toString())); + return null; + } + return seed; + } + + Future _updateWalletBackupStatus() async { + final wallet = _wallet.copyWith( + physicalBackupTested: true, + lastPhysicalBackupTested: DateTime.now(), + ); + + await _appWalletsRepository + .getWalletServiceById(wallet.id) + ?.updateWallet(wallet, updateTypes: [UpdateWalletTypes.settings]); + } + + void _emitBackupTestSuccessState() { + emit( + state.copyWith( + backupTested: true, + testingBackup: false, + ), + ); + clearSensitive(); + } + + void word24Clicked(int shuffledIdx) { + emit(state.copyWith(errTestingBackup: '')); + final testMnemonic = state.testMnemonicOrder.toList(); + if (testMnemonic.length == 24) return; + + final (word, isSelected, actualIdx) = state.shuffleElementAt(shuffledIdx); + if (isSelected) return; + if (actualIdx != testMnemonic.length) { + invalidTestOrderClicked(); + return; + } + + testMnemonic.add( + ( + word: word, + shuffleIdx: shuffledIdx, + selectedActualIdx: actualIdx, + ), + ); + + emit(state.copyWith(testMnemonicOrder: testMnemonic)); + } + + Future invalidTestOrderClicked() async { + emit( + state.copyWith( + testMnemonicOrder: [], + errTestingBackup: 'Invalid mnemonic order', + ), + ); + await Future.delayed(_kShuffleDelay); + final shuffled = state.mnemonic.toList()..shuffle(); + emit( + state.copyWith( + shuffledMnemonic: shuffled, + errTestingBackup: '', + ), + ); + } + + void changePassword(String password) { + emit( + state.copyWith( + testBackupPassword: password, + errTestingBackup: '', + ), + ); + } + + void wordClicked(int shuffledIdx) { + emit(state.copyWith(errTestingBackup: '')); + final testMnemonic = state.testMnemonicOrder.toList(); + if (testMnemonic.length == 12) return; + + final (word, isSelected, actualIdx) = state.shuffleElementAt(shuffledIdx); + if (isSelected) return; + if (actualIdx != testMnemonic.length) { + invalidTestOrderClicked(); + return; + } + + testMnemonic.add( + ( + word: word, + shuffleIdx: shuffledIdx, + selectedActualIdx: actualIdx, + ), + ); + + emit(state.copyWith(testMnemonicOrder: testMnemonic)); + } + + Future resetBackupTested() async { + await Future.delayed(_kDelayDuration); + emit(state.copyWith(backupTested: false)); + } + + void clearMnemonic() { + emit( + state.copyWith( + mnemonic: List.filled(12, ''), + testBackupPassword: '', + ), + ); + } + + Future clearSensitive() async { + clearMnemonic(); + emit( + state.copyWith( + mnemonic: [], + password: '', + shuffledMnemonic: [], + testMnemonicOrder: [], + ), + ); + } + + Future saveEncryptedBackup() async { + if (!_canStartBackup()) { + emit( + state.copyWith( + errorSavingBackups: 'Please wait before attempting another backup', + savingBackups: false, + backupKey: '', + ), + ); + return; + } + + emit(state.copyWith(savingBackups: true, errorSavingBackups: '')); + + final backups = await _createBackupsForAllWallets(); + if (backups.isEmpty) { + _emitBackupError('No wallets available for backup'); + return; + } + + final (encryptedData, err) = await _encryptBackups(backups); + if (err != null || encryptedData == null) { + return; + } + + await _saveToFileSystem(encryptedData); + } + + Future saveGoogleDriveBackup() async { + if (!_canStartBackup()) { + _emitBackupError('Please wait before attempting another backup'); + return; + } + + emit(state.copyWith(savingBackups: true, errorSavingBackups: '')); + + try { + if (state.backupFolderId.isEmpty) { + await connectToGoogleDrive(); + if (state.backupFolderId.isEmpty) return; + } + + final backups = await _createBackupsForAllWallets(); + if (backups.isEmpty) { + _emitBackupError('No wallets available for backup'); + return; + } + + final (encryptedData, err) = await _encryptBackups(backups); + if (err != null || encryptedData == null) return; + + await _saveToGoogleDrive(encryptedData); + } catch (e) { + debugPrint('Error saving to Google Drive: $e'); + _emitBackupError('Failed to save Google Drive backup'); + } + } + + Future<((String, String)?, Err?)> _encryptBackups( + List backups, + ) async { + try { + final (encData, err) = await _manager.encryptBackups( + backups: backups, + derivationPath: _kDerivationPath, + ); + + if (err != null || encData == null) { + return (null, err); + } + + return (encData, null); + } catch (e) { + return (null, Err(e.toString())); + } + } + + Future _saveToFileSystem((String, String) encryptedData) async { + final (filePath, errSave) = await _manager.saveEncryptedBackup( + encrypted: encryptedData.$2, + ); + + if (errSave != null) { + _emitBackupError('Save failed: ${errSave.message}'); + return; + } + + final fileName = filePath?.split('/').last; + final backupId = fileName?.split('_').last.split('.').first; + if (backupId == null) { + _emitBackupError('Failed to extract backup ID'); + return; + } + + final backupSalt = jsonDecode(encryptedData.$2)['salt'] as String; + + emit( + state.copyWith( + backupId: backupId, + backupKey: encryptedData.$1, + backupFolderPath: filePath ?? '', + backupSalt: backupSalt, + savingBackups: false, + lastBackupAttempt: DateTime.now(), + ), + ); + } + + Future _saveToGoogleDrive((String, String) encryptedData) async { + try { + final backupSalt = jsonDecode(encryptedData.$2)['salt'] as String; + + final (filePath, error) = await _driveManager.saveEncryptedBackup( + encrypted: encryptedData.$2, + backupFolder: state.backupFolderId, + ); + + if (error != null) { + debugPrint('Error saving to Google Drive: ${error.message}'); + _emitBackupError('Failed to save to Google Drive'); + return; + } + final fileName = filePath?.split('/').last; + final backupId = fileName?.split('_').last.split('.').first; + if (backupId == null || fileName == null) { + debugPrint('Failed to extract backup ID'); + _emitBackupError('Failed to save to Google Drive'); + return; + } + emit( + state.copyWith( + backupId: backupId, + backupKey: encryptedData.$1, + backupFolderPath: fileName, + backupSalt: backupSalt, + savingBackups: false, + lastBackupAttempt: DateTime.now(), + ), + ); + } catch (e) { + _emitBackupError('Failed to save to Google Drive: $e'); + } + } + + Future connectToGoogleDrive() async { + try { + final (folderId, err) = await _driveManager.connect(); + if (err != null) { + _emitBackupError('Failed to connect to Google Drive: ${err.message}'); + return; + } + emit( + state.copyWith( + backupFolderId: folderId ?? '', + errorSavingBackups: '', + ), + ); + } catch (e) { + _emitBackupError('Google Drive connection error: $e'); + } + } + + Future> _createBackupsForAllWallets() async { + final backups = []; + + try { + for (final wallet in _wallets) { + final backup = await _createBackupForWallet(wallet); + if (backup != null) backups.add(backup); + } + return backups; + } catch (e) { + _emitBackupError('Failed to create backups: $e'); + return []; + } + } + + Future _createBackupForWallet(Wallet wallet) async { + try { + final (seed, err) = await _loadWalletSeed(wallet); + if (err != null || seed == null) { + _emitBackupError('Failed to read wallet ${wallet.name}: $err'); + return null; + } + + final backup = Backup( + name: wallet.name ?? '', + network: wallet.network.name, + mnemonicFingerPrint: wallet.mnemonicFingerprint, + ); + + if (!wallet.hasPassphrase()) { + return backup.copyWith( + mnemonic: seed.mnemonic.split(' '), + passphrase: '', + ); + } + + final passphrases = seed.passphrases + .where((e) => e.sourceFingerprint == wallet.sourceFingerprint); + + if (passphrases.isEmpty) { + _emitBackupError('No passphrase found for wallet ${wallet.name}'); + return backup; + } + + return backup.copyWith( + mnemonic: seed.mnemonic.split(' '), + passphrase: passphrases.first.passphrase, + ); + } catch (e) { + _emitBackupError('Error creating backup for ${wallet.name}: $e'); + return null; + } + } + + void _emitBackupError(String message) { + emit( + state.copyWith( + savingBackups: false, + errorSavingBackups: message, + ), + ); + } + + void disconnectGoogleDrive() { + _driveManager.disconnect(); + emit(state.copyWith(backupFolderId: '')); + } + + void clearError() => emit( + state.copyWith( + errTestingBackup: '', + errorLoadingBackups: '', + errorSavingBackups: '', + ), + ); + + bool _canStartBackup() { + final lastAttempt = state.lastBackupAttempt; + if (lastAttempt != null) { + final timeSinceLastBackup = DateTime.now().difference(lastAttempt); + if (timeSinceLastBackup < _kMinBackupInterval) { + return false; + } + } + return true; + } +} diff --git a/lib/wallet_settings/bloc/backup_settings_state.dart b/lib/wallet_settings/bloc/backup_settings_state.dart new file mode 100644 index 000000000..cba36295b --- /dev/null +++ b/lib/wallet_settings/bloc/backup_settings_state.dart @@ -0,0 +1,63 @@ +import 'package:bb_mobile/_model/backup.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +part 'backup_settings_state.freezed.dart'; + +@freezed +class BackupSettingsState with _$BackupSettingsState { + const factory BackupSettingsState({ + // Verification properties + @Default([]) List mnemonic, + @Default('') String password, + @Default([]) List shuffledMnemonic, + @Default([]) + List<({String word, int shuffleIdx, int selectedActualIdx})> + testMnemonicOrder, + @Default('') String testBackupPassword, + @Default(false) bool testingBackup, + @Default('') String errTestingBackup, + @Default(false) bool backupTested, + @Default([]) List loadedBackups, + @Default('') String errorLoadingBackups, + @Default(false) bool savingBackups, + @Default('') String errorSavingBackups, + @Default('') String backupId, + @Default('') String backupFolderPath, + @Default('') String backupFolderId, + @Default('') String backupSalt, + @Default('') String backupKey, + @Default(null) DateTime? lastBackupAttempt, + }) = _BackupSettingsState; + + const BackupSettingsState._(); + + (String word, bool isSelected, int actualIdx) shuffleElementAt( + int shuffleIdx, + ) { + try { + final word = shuffledMnemonic[shuffleIdx]; + final isSelected = _isSelected(shuffleIdx); + final actualIdx = _actualIdx(shuffleIdx); + return (word, isSelected, actualIdx); + } catch (e) { + return ('', false, 0); + } + } + + bool _isSelected(int shuffleIdx) => + testMnemonicOrder.where((w) => w.shuffleIdx == shuffleIdx).isNotEmpty; + + int _actualIdx(int shuffleIdx) { + final word = shuffledMnemonic[shuffleIdx]; + final wordCount = mnemonic.where((w) => w == word).length; + + if (wordCount == 1) return mnemonic.indexOf(word); + if (_isSelected(shuffleIdx)) { + return testMnemonicOrder + .firstWhere((w) => w.shuffleIdx == shuffleIdx) + .selectedActualIdx; + } + return mnemonic.indexOf(word, testMnemonicOrder.length - 1); + } + + String testMneString() => testMnemonicOrder.map((w) => w.word).join(' '); +} From e790702fda970959d0e09cb93ce482cbecff9f6d Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Wed, 5 Feb 2025 04:40:17 -0500 Subject: [PATCH 106/401] refactor(wallet_settings): update TestBackupListener to use BackupSettingsCubit --- lib/wallet_settings/listeners.dart | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/wallet_settings/listeners.dart b/lib/wallet_settings/listeners.dart index 939723016..11c8914d7 100644 --- a/lib/wallet_settings/listeners.dart +++ b/lib/wallet_settings/listeners.dart @@ -3,8 +3,10 @@ import 'package:bb_mobile/_repository/wallet_service.dart'; import 'package:bb_mobile/home/bloc/home_bloc.dart'; import 'package:bb_mobile/home/bloc/home_event.dart'; import 'package:bb_mobile/wallet/bloc/wallet_bloc.dart'; -import 'package:bb_mobile/wallet_settings/bloc/state.dart'; +import 'package:bb_mobile/wallet_settings/bloc/backup_settings_cubit.dart'; +import 'package:bb_mobile/wallet_settings/bloc/backup_settings_state.dart'; import 'package:bb_mobile/wallet_settings/bloc/wallet_settings_cubit.dart'; +import 'package:bb_mobile/wallet_settings/bloc/wallet_settings_state.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; @@ -53,7 +55,7 @@ class TestBackupListener extends StatelessWidget { final Widget child; @override Widget build(BuildContext context) { - return BlocListener( + return BlocListener( listenWhen: (previous, current) => previous.backupTested != current.backupTested, listener: (context, state) { @@ -68,8 +70,8 @@ class TestBackupListener extends StatelessWidget { if (walletService == null) return; final w = walletService.wallet.copyWith( - backupTested: true, - lastBackupTested: DateTime.now(), + physicalBackupTested: true, + lastPhysicalBackupTested: DateTime.now(), ); walletService.updateWallet( From 375335595f76de364359f4ff6b64c991eb25eab6 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Wed, 5 Feb 2025 04:41:02 -0500 Subject: [PATCH 107/401] refactor(wallet_settings): rename BackupPage to PhysicalBackupPage --- .../{backup.dart => physical_backup.dart} | 91 ++++++++++++------- lib/wallet_settings/test_backup.dart | 48 +++++----- 2 files changed, 80 insertions(+), 59 deletions(-) rename lib/wallet_settings/{backup.dart => physical_backup.dart} (75%) diff --git a/lib/wallet_settings/backup.dart b/lib/wallet_settings/physical_backup.dart similarity index 75% rename from lib/wallet_settings/backup.dart rename to lib/wallet_settings/physical_backup.dart index 754aa5269..0b4fc0b5b 100644 --- a/lib/wallet_settings/backup.dart +++ b/lib/wallet_settings/physical_backup.dart @@ -4,7 +4,7 @@ import 'package:bb_mobile/_ui/components/button.dart'; import 'package:bb_mobile/_ui/components/text.dart'; import 'package:bb_mobile/_ui/word_grid.dart'; import 'package:bb_mobile/wallet/bloc/wallet_bloc.dart'; -import 'package:bb_mobile/wallet_settings/bloc/wallet_settings_cubit.dart'; +import 'package:bb_mobile/wallet_settings/bloc/backup_settings_cubit.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:gap/gap.dart'; @@ -17,8 +17,8 @@ class InfoRead extends Cubit { void unread() => emit(false); } -class BackupPage extends StatefulWidget { - const BackupPage({ +class PhysicalBackupPage extends StatefulWidget { + const PhysicalBackupPage({ super.key, required this.wallet, }); @@ -26,18 +26,18 @@ class BackupPage extends StatefulWidget { final String wallet; @override - State createState() => _BackupPageState(); + State createState() => _PhysicalBackupPageState(); } -class _BackupPageState extends State { +class _PhysicalBackupPageState extends State { late WalletBloc walletBloc; - late WalletSettingsCubit walletSettings; + late BackupSettingsCubit backupSettings; @override void initState() { walletBloc = createOrRetreiveWalletBloc(widget.wallet); - walletSettings = createWalletSettingsCubit(widget.wallet); + backupSettings = createBackupSettingsCubit(widget.wallet); - walletSettings.loadBackupClicked(); + backupSettings.loadBackupForVerification(); super.initState(); } @@ -47,7 +47,7 @@ class _BackupPageState extends State { return MultiBlocProvider( providers: [ BlocProvider.value(value: walletBloc), - BlocProvider(create: (BuildContext context) => walletSettings), + BlocProvider(create: (BuildContext context) => backupSettings), BlocProvider.value(value: InfoRead()), ], child: const _Screen(), @@ -58,38 +58,55 @@ class _BackupPageState extends State { class _Screen extends StatelessWidget { const _Screen(); + Future _handleNavigationCleanup( + BuildContext context, + bool state, + ) async { + try { + if (state) { + context.read().unread(); + } + await context.read().clearSensitive(); + + if (!context.mounted) return false; + context.pop(); + //TODO: context.go('/home'); + return true; + } catch (e) { + return false; + } + } + @override Widget build(BuildContext context) { - return BlocBuilder( + return BlocConsumer( + listener: (context, state) { + // debugPrint('InfoRead state changed to: $state'); + }, builder: (context, state) { return PopScope( - canPop: false, - onPopInvokedWithResult: (canPop, _) async { - if (state) context.read().unread(); - await context.read().clearSensitive(); - - if (!context.mounted) return; - context.go('/home'); + onPopInvokedWithResult: (didPop, _) async { + if (!didPop) { + final result = await _handleNavigationCleanup(context, state); + if (!result) return; + } }, child: Scaffold( appBar: AppBar( automaticallyImplyLeading: false, + elevation: 0, flexibleSpace: BBAppBar( - text: 'Backup', + text: '', onBack: () async { - if (state) context.read().unread(); - await context.read().clearSensitive(); - // context.pop(); - context - ..pop() - ..pop(); - // context.go('/home'); + await _handleNavigationCleanup(context, state); }, ), ), body: AnimatedSwitcher( duration: const Duration(milliseconds: 300), - child: state ? const BackupScreen() : const BackUpInfoScreen(), + child: state + ? const BackupScreen(key: ValueKey('backup')) + : const BackUpInfoScreen(key: ValueKey('info')), ), ), ); @@ -103,8 +120,9 @@ class BackUpInfoScreen extends StatelessWidget { @override Widget build(BuildContext context) { - final lastBackupTested = context - .select((WalletBloc cubit) => cubit.state.wallet.lastBackupTested); + final lastPhysicalBackupTested = context.select( + (WalletBloc cubit) => cubit.state.wallet.lastPhysicalBackupTested, + ); final hasPassphrase = context .select((WalletBloc cubit) => cubit.state.wallet.hasPassphrase()); @@ -115,11 +133,14 @@ class BackUpInfoScreen extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - const BBText.titleLarge('Backup best practices'), + const BBText.titleLarge( + 'Backup best practices', + isBold: true, + ), const Gap(8), - if (lastBackupTested != null) ...[ + if (lastPhysicalBackupTested != null) ...[ BBText.bodySmall( - 'Last backup tested on ${lastBackupTested.toLocal()}', + 'Last backup tested on ${lastPhysicalBackupTested.toLocal()}', ), const Gap(8), ], @@ -166,11 +187,11 @@ class BackupScreen extends StatelessWidget { @override Widget build(BuildContext context) { final mnemonic = context.select( - (WalletSettingsCubit cubit) => cubit.state.mnemonic, + (BackupSettingsCubit cubit) => cubit.state.mnemonic, ); final password = context.select( - (WalletSettingsCubit cubit) => cubit.state.password, + (BackupSettingsCubit cubit) => cubit.state.password, ); return SingleChildScrollView( @@ -236,11 +257,11 @@ class BackupScreen extends StatelessWidget { context // ..pop() .push( - '/wallet-settings/test-backup', + '/wallet-settings/backup-settings/physical/test-backup', extra: context.read().state.wallet.id, // ( // context.read(), - // context.read(), + // context.read(), // ), ); // context.pop(); diff --git a/lib/wallet_settings/test_backup.dart b/lib/wallet_settings/test_backup.dart index 502958dd8..bfe0dd75b 100644 --- a/lib/wallet_settings/test_backup.dart +++ b/lib/wallet_settings/test_backup.dart @@ -4,7 +4,7 @@ import 'package:bb_mobile/_ui/components/text.dart'; import 'package:bb_mobile/_ui/components/text_input.dart'; import 'package:bb_mobile/styles.dart'; import 'package:bb_mobile/wallet/bloc/wallet_bloc.dart'; -import 'package:bb_mobile/wallet_settings/bloc/wallet_settings_cubit.dart'; +import 'package:bb_mobile/wallet_settings/bloc/backup_settings_cubit.dart'; import 'package:bb_mobile/wallet_settings/listeners.dart'; import 'package:extra_alignments/extra_alignments.dart'; import 'package:flutter/material.dart'; @@ -27,14 +27,14 @@ class TestBackupPage extends StatefulWidget { } class _TestBackupPageState extends State { - late WalletSettingsCubit walletSettings; + late BackupSettingsCubit backupSettings; late WalletBloc walletBloc; @override void initState() { walletBloc = createOrRetreiveWalletBloc(widget.wallet); - walletSettings = createWalletSettingsCubit(widget.wallet); + backupSettings = createBackupSettingsCubit(widget.wallet); - walletSettings.loadBackupClicked(); + backupSettings.loadBackupForVerification(); super.initState(); } @@ -44,7 +44,7 @@ class _TestBackupPageState extends State { return MultiBlocProvider( providers: [ BlocProvider.value(value: walletBloc), - BlocProvider(create: (BuildContext context) => walletSettings), + BlocProvider(create: (BuildContext context) => backupSettings), ], child: TestBackupListener( child: Builder( @@ -60,8 +60,8 @@ class _TestBackupPageState extends State { flexibleSpace: BBAppBar( text: 'Test Backup', onBack: () { + context.read().resetBackupTested(); context.pop(); - context.read().resetBackupTested(); }, ), ), @@ -81,9 +81,9 @@ class TestBackupScreen extends StatelessWidget { @override Widget build(BuildContext context) { final mnemonic = - context.select((WalletSettingsCubit cubit) => cubit.state.mnemonic); + context.select((BackupSettingsCubit cubit) => cubit.state.mnemonic); final tested = - context.select((WalletSettingsCubit cubit) => cubit.state.backupTested); + context.select((BackupSettingsCubit cubit) => cubit.state.backupTested); return SingleChildScrollView( child: Padding( @@ -105,7 +105,7 @@ class TestBackupScreen extends StatelessWidget { // child: CenterLeft( // child: BBButton.text( // label: 'Reset Order', - // onPressed: () => context.read().loadBackupClicked(), + // onPressed: () => context.read().loadBackupClicked(), // ), // ), // ), @@ -118,7 +118,7 @@ class TestBackupScreen extends StatelessWidget { BackupTestItemWord( index: i == 0 ? j : i * 2 + j, // isSelected: context - // .select() + // .select() // .state // .shuffleIsSelected(i == 0 ? j : i * 2 + j), ), @@ -132,7 +132,7 @@ class TestBackupScreen extends StatelessWidget { BackupTestItemWord( index: i == 0 ? j : i * 2 + j, // isSelected: context - // .select() + // .select() // .state // .shuffleIsSelected(i == 0 ? j : i * 2 + j), ), @@ -158,7 +158,7 @@ class TestBackupScreen extends StatelessWidget { // '/wallet-settings/backup', // extra: ( // context.read(), - // context.read(), + // context.read(), // ), // ); // // context.pop(); @@ -187,11 +187,11 @@ class BackupTestItemWord extends StatelessWidget { @override Widget build(BuildContext context) { final mnemonic = context.select( - (WalletSettingsCubit cubit) => cubit.state.mnemonic, + (BackupSettingsCubit cubit) => cubit.state.mnemonic, ); final (word, isSelected, actualIdx) = context.select( - (WalletSettingsCubit _) => _.state.shuffleElementAt(index), + (BackupSettingsCubit _) => _.state.shuffleElementAt(index), ); final padLeft = @@ -204,9 +204,9 @@ class BackupTestItemWord extends StatelessWidget { borderRadius: BorderRadius.circular(80), onTap: () { if (mnemonic.length == 12) { - context.read().wordClicked(index); + context.read().wordClicked(index); } else { - context.read().word24Clicked(index); + context.read().word24Clicked(index); } }, child: Stack( @@ -262,7 +262,7 @@ class TestBackupPassField extends HookWidget { @override Widget build(BuildContext context) { final tested = - context.select((WalletSettingsCubit cubit) => cubit.state.backupTested); + context.select((BackupSettingsCubit cubit) => cubit.state.backupTested); if (tested) return const SizedBox.shrink(); final hasPassphrase = @@ -271,7 +271,7 @@ class TestBackupPassField extends HookWidget { if (!hasPassphrase) return const SizedBox.shrink(); final text = - context.select((WalletSettingsCubit x) => x.state.testBackupPassword); + context.select((BackupSettingsCubit x) => x.state.testBackupPassword); return Padding( padding: const EdgeInsets.symmetric(horizontal: 4), @@ -286,7 +286,7 @@ class TestBackupPassField extends HookWidget { BBTextInput.big( value: text, onChanged: (t) { - context.read().changePassword(t); + context.read().changePassword(t); }, ), ], @@ -301,9 +301,9 @@ class TestBackupConfirmButton extends StatelessWidget { @override Widget build(BuildContext context) { final testing = context - .select((WalletSettingsCubit cubit) => cubit.state.testingBackup); + .select((BackupSettingsCubit cubit) => cubit.state.testingBackup); final tested = - context.select((WalletSettingsCubit cubit) => cubit.state.backupTested); + context.select((BackupSettingsCubit cubit) => cubit.state.backupTested); return Column( children: [ @@ -329,7 +329,7 @@ class TestBackupConfirmButton extends StatelessWidget { loading: testing, filled: true, onPressed: () => - context.read().testBackupClicked(), + context.read().testBackupClicked(), label: 'Test Backup', ), ), @@ -352,7 +352,7 @@ class TestBackupConfirmButton extends StatelessWidget { // @override // Widget build(BuildContext context) { // final text = context.select( -// (WalletSettingsCubit cubit) => cubit.state.mnemonic.elementAt(widget.index), +// (BackupSettingsCubit cubit) => cubit.state.mnemonic.elementAt(widget.index), // ); // return Expanded( @@ -373,7 +373,7 @@ class TestBackupConfirmButton extends StatelessWidget { // child: BBTextInput.small( // value: text, // onChanged: (value) { -// context.read().wordChanged(widget.index, value); +// context.read().wordChanged(widget.index, value); // }, // ), // ), From 5c7283d95731365aaf40d833e49a704e161aaf87 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Wed, 5 Feb 2025 04:42:32 -0500 Subject: [PATCH 108/401] feat(backup_manager): enhance backup encryption and decryption methods --- lib/_pkg/backup/_interface.dart | 84 ++++++++++++++++++++++++--------- 1 file changed, 62 insertions(+), 22 deletions(-) diff --git a/lib/_pkg/backup/_interface.dart b/lib/_pkg/backup/_interface.dart index e05e3745f..f97095ccf 100644 --- a/lib/_pkg/backup/_interface.dart +++ b/lib/_pkg/backup/_interface.dart @@ -1,65 +1,105 @@ import 'dart:convert'; - import 'package:bb_mobile/_model/backup.dart'; +import 'package:bb_mobile/_model/wallet.dart'; import 'package:bb_mobile/_pkg/consts/configs.dart'; import 'package:bb_mobile/_pkg/error.dart'; -import 'package:recoverbull_dart/recoverbull_dart.dart'; +import 'package:bdk_flutter/bdk_flutter.dart'; +import 'package:bip85/bip85.dart'; +import 'package:hex/hex.dart'; +import 'package:recoverbull/recoverbull.dart' as recoverbull; abstract class IBackupManager { - /// Encrypts a list of backups using BIP85. - /// Returns a tuple containing the backupKey and encrypted data or an error. + /// Encrypts a list of backups using BIP85 derivation Future<((String, String)?, Err?)> encryptBackups({ required List backups, required String derivationPath, - required String backupKeyMnemonic, }) async { + if (backups.isEmpty) { + return (null, Err('No backups provided')); + } + try { final plaintext = json.encode(backups.map((i) => i.toJson()).toList()); - final (backupKey, encrypted) = await BackupService.createBackupWithBIP85( - plaintext: plaintext, - mnemonic: backupKeyMnemonic, - network: backups.first.network.toLowerCase(), - derivationPath: derivationPath, + final key = await _deriveBackupKey( + mnemonic: backups.first.mnemonic.join(' '), + network: backups.first.network, + path: derivationPath, + ); + + if (key == null) { + return (null, Err('Failed to derive backup key')); + } + final encrypted = recoverbull.BackupService.createBackup( + secret: utf8.encode(plaintext), + backupKey: key, ); - return ((backupKey, encrypted), null); + return ((HEX.encode(key), encrypted), null); } catch (e) { - return (null, Err('Failed to encrypt backups: $e')); + return (null, Err('Encryption failed: $e')); } } - /// Decrypts an encrypted backup using the provided backup key. - /// Returns a list of backups. + /// Decrypts an encrypted backup using the provided key Future<(List?, Err?)> decryptBackups({ required String encrypted, required String backupKey, }) async { try { - final plaintext = await BackupService.restoreBackup(encrypted, backupKey); + final key = HEX.decode(backupKey); + final plaintext = recoverbull.BackupService.restoreBackup( + backup: encrypted, + backupKey: key, + ); + + return _parseBackups(plaintext); + } catch (e) { + return (null, Err('Decryption failed: $e')); + } + } + + Future?> _deriveBackupKey({ + required String mnemonic, + required String network, + required String path, + }) async { + try { + final mne = await Mnemonic.fromString(mnemonic); + final descriptorSecretKey = await DescriptorSecretKey.create( + network: BBNetwork.fromString(network).toBdkNetwork(), + mnemonic: mne, + ); + final res = derive( + xprv: descriptorSecretKey.toString().split('/*').first, + path: path, + ); + return res.sublist(0, 32); + } catch (e) { + return null; + } + } + + (List?, Err?) _parseBackups(String plaintext) { + try { final decodedJson = jsonDecode(plaintext) as List; final backups = decodedJson .map((item) => Backup.fromJson(item as Map)) .toList(); return (backups, null); } catch (e) { - return (null, Err('Failed to decrypt backups: $e')); + return (null, Err('Failed to parse backups: $e')); } } - /// Writes the encrypted backup to a storage medium. - /// Returns the path to the written backup or an error. + // Abstract methods to be implemented by concrete classes Future<(String?, Err?)> saveEncryptedBackup({ required String encrypted, String backupFolder = defaultBackupPath, }); - /// Reads the encrypted backup from a storage medium. - /// Returns a map containing the backup data and the backup ID, or an error. Future<(Map?, Err?)> loadEncryptedBackup({ required String encrypted, }); - /// Deletes the encrypted backup from a storage medium. - /// Returns the path to the deleted backup or an error. Future<(String?, Err?)> removeEncryptedBackup({ required String backupName, String backupFolder = defaultBackupPath, From f80332acf1f648ee8f21260b9e7617f9613cf7d5 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Wed, 5 Feb 2025 04:42:43 -0500 Subject: [PATCH 109/401] fix(backup_manager): correct backup ID key in Google Drive and local backup managers --- lib/_pkg/backup/google_drive.dart | 9 ++++----- lib/_pkg/backup/local.dart | 3 +-- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/lib/_pkg/backup/google_drive.dart b/lib/_pkg/backup/google_drive.dart index d89502216..41e672c8d 100644 --- a/lib/_pkg/backup/google_drive.dart +++ b/lib/_pkg/backup/google_drive.dart @@ -100,9 +100,8 @@ class GoogleDriveBackupManager extends IBackupManager { if (_api == null) return (null, Err('Not connected to Google Drive')); try { - final decodeEncryptedFile = jsonDecode(utf8.decode(HEX.decode(encrypted))) - as Map; - final backupId = decodeEncryptedFile['backupId']?.toString() ?? ''; + final decodeEncryptedFile = jsonDecode(encrypted) as Map; + final backupId = decodeEncryptedFile['id']?.toString() ?? ''; final now = DateTime.now(); final formattedDate = now.millisecondsSinceEpoch; final filename = '${formattedDate}_$backupId.json'; @@ -110,10 +109,10 @@ class GoogleDriveBackupManager extends IBackupManager { ..name = filename ..parents = [backupFolder]; - final data = encrypted.codeUnits; + final data = encrypted; await _api!.files.create( file, - uploadMedia: Media(Stream.value(data), data.length), + uploadMedia: Media(Stream.value(utf8.encode(data)), data.length), ); return (filename, null); diff --git a/lib/_pkg/backup/local.dart b/lib/_pkg/backup/local.dart index 0e072dae0..663c6620c 100644 --- a/lib/_pkg/backup/local.dart +++ b/lib/_pkg/backup/local.dart @@ -58,8 +58,7 @@ class FileSystemBackupManager extends IBackupManager { }) async { try { final decodeEncryptedFile = jsonDecode(encrypted) as Map; - - final backupId = decodeEncryptedFile['backupId']?.toString() ?? ''; + final backupId = decodeEncryptedFile['id']?.toString() ?? ''; final now = DateTime.now(); final formattedDate = now.millisecondsSinceEpoch; final filename = '${formattedDate}_$backupId.json'; From 046e69c78e4acc81c8d427f5ada85a365d535475 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Wed, 5 Feb 2025 04:43:03 -0500 Subject: [PATCH 110/401] feat(backup_settings): add BackupSettings screen --- lib/wallet_settings/backup_settings.dart | 282 +++++++++++++++++++++++ 1 file changed, 282 insertions(+) create mode 100644 lib/wallet_settings/backup_settings.dart diff --git a/lib/wallet_settings/backup_settings.dart b/lib/wallet_settings/backup_settings.dart new file mode 100644 index 000000000..3a610d4f1 --- /dev/null +++ b/lib/wallet_settings/backup_settings.dart @@ -0,0 +1,282 @@ +import 'package:bb_mobile/_ui/app_bar.dart'; +import 'package:bb_mobile/_ui/components/button.dart'; +import 'package:bb_mobile/_ui/components/text.dart'; +import 'package:bb_mobile/styles.dart'; +import 'package:bb_mobile/wallet/bloc/wallet_bloc.dart'; +import 'package:bb_mobile/wallet_settings/bloc/backup_settings_cubit.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:gap/gap.dart'; +import 'package:go_router/go_router.dart'; + +class BackupSettings extends StatefulWidget { + const BackupSettings({ + super.key, + required this.wallet, + }); + + final String wallet; + + @override + State createState() => _BackupSettingsState(); +} + +class _BackupSettingsState extends State { + @override + Widget build(BuildContext context) { + return MultiBlocProvider( + providers: [ + BlocProvider.value( + value: createOrRetreiveWalletBloc(widget.wallet), + ), + BlocProvider( + create: (BuildContext context) => + createBackupSettingsCubit(widget.wallet), + ), + ], + child: Scaffold( + appBar: AppBar( + backgroundColor: Colors.transparent, + elevation: 0, + automaticallyImplyLeading: false, + flexibleSpace: BBAppBar( + onBack: () { + context.pop(); + }, + text: 'Backup settings', + ), + ), + body: const Padding( + padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Gap(16), + _Screen(), + ], + ), + ), + ), + ); + } +} + +class _Screen extends StatelessWidget { + const _Screen(); + @override + Widget build(BuildContext context) { + final watchOnly = + context.select((WalletBloc cubit) => cubit.state.wallet.watchOnly()); + final isPhysicalBackupTested = + context.select((WalletBloc x) => x.state.wallet.physicalBackupTested); + final isVaultBackupTested = + context.select((WalletBloc x) => x.state.wallet.vaultBackupTested); + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: context.colour.primaryContainer, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: NewColours.lightGray.withAlpha(50), + blurRadius: 30, + spreadRadius: 2, + offset: const Offset(0, 10), + ), + ], + border: Border.all(color: NewColours.lightGray.withAlpha(50)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const BBText.titleLarge( + "Backup settings", + isBold: true, + ), + const Gap(10), + if (!watchOnly) ...[ + BBButton.textWithStatus( + label: "Physical backup", + onPressed: () {}, + statusText: isPhysicalBackupTested ? 'Tested' : 'Not Tested', + isGreen: isPhysicalBackupTested, + isRed: !isPhysicalBackupTested, + ), + ], + BBButton.textWithStatus( + label: "Encrypted vault", + onPressed: () {}, + statusText: isVaultBackupTested ? 'Tested' : 'Not Tested', + isGreen: isVaultBackupTested, + isRed: !isVaultBackupTested, + ), + BBButton.withColour( + label: "Start backup", + onPressed: () => { + context.push( + '/wallet-settings/backup-settings/backup-options', + extra: context.read().state.wallet.id, + ), + }, + fillWidth: true, + center: true, + ), + const Gap(20), + BBButton.withColour( + label: "Recover or test backup", + onPressed: () { + context.push( + '/wallet-settings/backup-settings/recover-options', + extra: context.read().state.wallet.id, + ); + }, + fillWidth: true, + center: true, + ), + ], + ), + ); + } +} + +class RecoverOptionsScreen extends StatelessWidget { + const RecoverOptionsScreen({super.key, required this.wallet}); + final String wallet; + @override + Widget build(BuildContext context) { + return const Placeholder(); + } +} + +class BackupOptionsScreen extends StatelessWidget { + const BackupOptionsScreen({super.key, required this.wallet}); + final String wallet; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: Colors.transparent, + elevation: 0, + automaticallyImplyLeading: false, + flexibleSpace: BBAppBar( + onBack: () => context.pop(), + text: '', + ), + ), + body: Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + children: [ + const BBText.titleLarge( + 'Backup you wallet', + isBold: true, + fontSize: 25, + ), + const Gap(10), + RichText( + textAlign: TextAlign.center, + text: TextSpan( + children: [ + TextSpan( + text: 'Without a backup, you', + style: context.font.bodySmall!.copyWith( + fontSize: 12, + ), + ), + TextSpan( + text: ' will ', + style: context.font.bodySmall!.copyWith( + fontWeight: FontWeight.w900, + fontSize: 12, + ), + ), + TextSpan( + text: + 'eventually lose access to your money. It is critically important to do a backup.', + style: context.font.bodySmall!.copyWith( + fontSize: 12, + ), + ), + ], + ), + ), + const Gap(20), + _renderBackupSetting( + title: 'Encrypted vault (quick and easy)', + description: + 'Your backup is encrypted with a secure key that cannot be cracked, and uploaded to your cloud account. The key to unlock your vault is stored in an anonymous password manager and accessible with your PIN.', + onTap: () => context.push( + '/wallet-settings/backup-settings/encrypted', + extra: wallet, + ), + ), + const Gap(20), + _renderBackupSetting( + title: 'Physical backup (take your time)', + description: + 'You have to write down 12 words on a piece of paper or engrave it in metal. Make sure not to lose it. If anybody ever finds those 12 words, they can steal your Bitcoin.', + onTap: () async => context.push( + '/wallet-settings/backup-settings/physical', + extra: wallet, + ), + ), + ], + ), + ), + ); + } + + Widget _renderBackupSetting({ + required String title, + required String description, + required VoidCallback onTap, + }) { + return InkWell( + onTap: onTap, + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + boxShadow: [ + BoxShadow( + color: NewColours.lightGray.withAlpha(50), + blurRadius: 30, + spreadRadius: 2, + offset: const Offset(0, 10), + ), + ], + border: Border.all(color: NewColours.lightGray.withAlpha(100)), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + flex: 6, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + BBText.title( + title, + isBold: true, + ), + const Gap(4), + BBText.bodySmall( + description, + removeColourOpacity: true, + ), + ], + ), + ), + const Expanded( + child: Icon( + Icons.arrow_forward_ios, + size: 16, + ), + ), + ], + ), + ), + ); + } +} From d468c5a3bfe55fcb8953816281fb2f2f44945125 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Wed, 5 Feb 2025 04:43:37 -0500 Subject: [PATCH 111/401] refactor(wallet_settings): rename BackupButton to BackupSettingsButton --- lib/wallet_settings/wallet_settings_page.dart | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/lib/wallet_settings/wallet_settings_page.dart b/lib/wallet_settings/wallet_settings_page.dart index a712554f5..a7bf4d97f 100644 --- a/lib/wallet_settings/wallet_settings_page.dart +++ b/lib/wallet_settings/wallet_settings_page.dart @@ -10,8 +10,8 @@ import 'package:bb_mobile/_ui/headers.dart'; import 'package:bb_mobile/currency/bloc/currency_cubit.dart'; import 'package:bb_mobile/wallet/bloc/wallet_bloc.dart'; import 'package:bb_mobile/wallet_settings/addresses.dart'; -import 'package:bb_mobile/wallet_settings/bloc/state.dart'; import 'package:bb_mobile/wallet_settings/bloc/wallet_settings_cubit.dart'; +import 'package:bb_mobile/wallet_settings/bloc/wallet_settings_state.dart'; import 'package:bb_mobile/wallet_settings/listeners.dart'; import 'package:extra_alignments/extra_alignments.dart'; import 'package:flutter/material.dart'; @@ -85,12 +85,13 @@ class _ScreenState extends State<_Screen> { super.initState(); } +//TODO; Move it to backup-settings page void _init() { scheduleMicrotask(() async { if (widget.openBackup) { // await Future.delayed(const Duration(milliseconds: 300)); await context.push( - '/wallet-settings/backup', + '/wallet-settings/backup-settings/physical', extra: context.read().state.wallet.id, // ( // context.read(), @@ -136,7 +137,7 @@ class _ScreenState extends State<_Screen> { // const Balances(), const Gap(24), if (!watchOnly) ...[ - const BackupButton(), + const BackupSettingsButton(), const Gap(8), // const TestBackupButton(), // const Gap(8), @@ -459,7 +460,7 @@ class TestBackupButton extends StatelessWidget { @override Widget build(BuildContext context) { final isTested = - context.select((WalletBloc x) => x.state.wallet.backupTested); + context.select((WalletBloc x) => x.state.wallet.physicalBackupTested); // if (isTested) return const SizedBox.shrink(); return BBButton.textWithStatusAndRightArrow( @@ -467,6 +468,7 @@ class TestBackupButton extends StatelessWidget { statusText: isTested ? 'Tested' : 'Not Tested', isRed: !isTested, onPressed: () async { + ///TODO: Move it to backup-settings page context.push( '/wallet-settings/test-backup', extra: context.read().state.wallet.id, @@ -497,18 +499,15 @@ class TestBackupButton extends StatelessWidget { } } -class BackupButton extends StatelessWidget { - const BackupButton({super.key}); +class BackupSettingsButton extends StatelessWidget { + const BackupSettingsButton({super.key}); @override Widget build(BuildContext context) { - final isTested = - context.select((WalletBloc x) => x.state.wallet.backupTested); - return BBButton.textWithStatusAndRightArrow( onPressed: () async { context.push( - '/wallet-settings/backup', + '/wallet-settings/backup-settings', extra: context.read().state.wallet.id, // ( // context.read(), @@ -517,9 +516,7 @@ class BackupButton extends StatelessWidget { ); // await BackupScreen.openPopup(context); }, - label: 'Backup', - statusText: isTested ? 'Tested' : 'Not Tested', - isRed: !isTested, + label: 'Backup Settings', ); } } @@ -559,7 +556,7 @@ class DeletePopUp extends StatelessWidget { listener: (context, state) { if (state.deleted) { // final walletBloc = settings.walletBloc; - // context.read().clearWallet(walletBloc); + context.go('/home'); } }, From 14f6a4c1bf494e8edd47dd0da1cf69f7052ffd55 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Wed, 5 Feb 2025 04:43:56 -0500 Subject: [PATCH 112/401] feat(backup_settings): add EncryptedVaultBackupPage for secure backup options --- .../encrypted_vault_backup.dart | 263 ++++++++++++++++++ 1 file changed, 263 insertions(+) create mode 100644 lib/wallet_settings/encrypted_vault_backup.dart diff --git a/lib/wallet_settings/encrypted_vault_backup.dart b/lib/wallet_settings/encrypted_vault_backup.dart new file mode 100644 index 000000000..27d2893af --- /dev/null +++ b/lib/wallet_settings/encrypted_vault_backup.dart @@ -0,0 +1,263 @@ +import 'dart:io'; + +import 'package:bb_mobile/_ui/app_bar.dart'; +import 'package:bb_mobile/_ui/components/text.dart'; +import 'package:bb_mobile/_ui/toast.dart'; +import 'package:bb_mobile/styles.dart'; +import 'package:bb_mobile/wallet_settings/bloc/backup_settings_cubit.dart'; +import 'package:bb_mobile/wallet_settings/bloc/backup_settings_state.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:gap/gap.dart'; +import 'package:go_router/go_router.dart'; + +const double _kSpacing = 15.0; + +enum BackupProvider { + googleDrive('Google Drive', 'Easy', Icons.add_to_drive_rounded), + iCloud('Apple iCloud', 'Easy', CupertinoIcons.cloud_upload), + custom('Custom location', 'Private', Icons.folder); + + final String title; + final String description; + final IconData icon; + + const BackupProvider(this.title, this.description, this.icon); +} + +class EncryptedVaultBackupPage extends StatefulWidget { + final String wallet; + const EncryptedVaultBackupPage({super.key, required this.wallet}); + + @override + State createState() => + _EncryptedVaultBackupPageState(); +} + +class _EncryptedVaultBackupPageState extends State { + late final BackupSettingsCubit _cubit; + + @override + void initState() { + super.initState(); + _cubit = createBackupSettingsCubit(widget.wallet); + } + + @override + void dispose() { + _cubit.close(); + super.dispose(); + } + + Future _handleBackup( + BuildContext context, + BackupProvider provider, + ) async { + switch (provider) { + case BackupProvider.googleDrive: + await _cubit.saveGoogleDriveBackup(); + case BackupProvider.iCloud: + debugPrint('iCloud backup'); + case BackupProvider.custom: + _cubit.saveEncryptedBackup(); + } + } + + @override + Widget build(BuildContext context) { + return BlocProvider.value( + value: _cubit, + child: BlocConsumer( + listenWhen: (previous, current) => + previous.errorSavingBackups != current.errorSavingBackups || + previous.errorLoadingBackups != current.errorLoadingBackups || + (previous.savingBackups && !current.savingBackups), + listener: (context, state) { + if (state.errorSavingBackups.isNotEmpty) { + ScaffoldMessenger.of(context) + .showSnackBar(context.showToast(state.errorSavingBackups)); + _cubit.clearError(); + return; + } + + if (state.errorLoadingBackups.isNotEmpty) { + ScaffoldMessenger.of(context) + .showSnackBar(context.showToast(state.errorLoadingBackups)); + _cubit.clearError(); + return; + } + if (!state.savingBackups && + state.backupFolderPath.isNotEmpty && + state.backupKey.isNotEmpty && + state.lastBackupAttempt != null && + state.errorSavingBackups.isEmpty) { + context.push( + '/wallet-settings/backup-settings/keychain', + extra: (state.backupId, (state.backupKey, state.backupSalt)), + ); + _cubit.clearError(); + } + }, + builder: (context, state) { + return Scaffold( + appBar: AppBar( + elevation: 0, + automaticallyImplyLeading: false, + centerTitle: true, + flexibleSpace: BBAppBar(text: '', onBack: () => context.pop()), + ), + body: state.savingBackups + ? const Center(child: CircularProgressIndicator()) + : SingleChildScrollView( + padding: const EdgeInsets.all(20), + child: Column( + children: [ + const BBText.titleLarge( + 'Choose vault location', + isBold: true, + ), + const Gap(15), + const _InfoSection(), + const Gap(20), + ...BackupProvider.values.map( + (provider) => Padding( + padding: const EdgeInsets.only(bottom: 10), + child: _StorageOptionCard( + title: provider.title, + description: provider.description, + icon: Icon(provider.icon, size: 40), + onTap: () => _handleBackup(context, provider), + ), + ), + ), + ], + ), + ), + ); + }, + ), + ); + } +} + +class _InfoSection extends StatelessWidget { + const _InfoSection(); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + const BBText.bodySmall( + textAlign: TextAlign.center, + "Cloud storage providers like Google or Apple won't have access to your backup. They won't be able to guess the password. They can only access your Bitcoin in the unlikely event they collude with the key server.", + ), + const Gap(_kSpacing), + _buildWhitepaperLink(context), + const Gap(_kSpacing), + Text( + "It's up to you, you can store your vault anywhere you like.", + textAlign: TextAlign.center, + style: context.font.bodySmall!.copyWith( + fontWeight: FontWeight.w900, + fontSize: 12, + ), + ), + ], + ); + } + + Widget _buildWhitepaperLink(BuildContext context) { + return RichText( + textAlign: TextAlign.center, + text: TextSpan( + children: [ + TextSpan( + text: 'To learn more about the tradeoffs and risks, read the', + style: context.font.bodySmall!.copyWith(fontSize: 12), + ), + TextSpan( + text: ' RecoverBull whitepaper', + style: context.font.bodySmall!.copyWith( + fontWeight: FontWeight.w900, + fontSize: 12, + decoration: TextDecoration.underline, + ), + recognizer: TapGestureRecognizer()..onTap = () {/* TODO */}, + ), + ], + ), + ); + } +} + +class _StorageOptionCard extends StatelessWidget { + final String title; + final String description; + final Widget icon; + final VoidCallback onTap; + + const _StorageOptionCard({ + required this.title, + required this.description, + required this.icon, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return Material( + color: Colors.transparent, + child: InkWell( + onTap: onTap, + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + boxShadow: [ + BoxShadow( + color: NewColours.lightGray.withAlpha(50), + blurRadius: 30, + spreadRadius: 2, + offset: const Offset(0, 10), + ), + ], + border: Border.all(color: NewColours.lightGray.withAlpha(100)), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded(flex: 2, child: icon), + Expanded( + flex: 10, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + BBText.bodySmall(title), + const Gap(4), + RichText( + text: TextSpan( + text: description, + style: context.font.bodySmall! + .copyWith(fontWeight: FontWeight.w900), + ), + ), + ], + ), + ), + Expanded( + child: Icon( + Platform.isIOS + ? Icons.arrow_forward_ios + : Icons.arrow_forward, + size: 16, + ), + ), + ], + ), + ), + ), + ); + } +} From 2bbeda5ddbd9d7cacde4f49178981f3821cfd39e Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Wed, 5 Feb 2025 04:44:12 -0500 Subject: [PATCH 113/401] feat(keychain): implement KeychainCubit and KeychainState for secure input handling --- lib/wallet_settings/bloc/keychain_cubit.dart | 201 +++++++ lib/wallet_settings/bloc/keychain_state.dart | 43 ++ lib/wallet_settings/keychain_page.dart | 536 +++++++++++++++++++ 3 files changed, 780 insertions(+) create mode 100644 lib/wallet_settings/bloc/keychain_cubit.dart create mode 100644 lib/wallet_settings/bloc/keychain_state.dart create mode 100644 lib/wallet_settings/keychain_page.dart diff --git a/lib/wallet_settings/bloc/keychain_cubit.dart b/lib/wallet_settings/bloc/keychain_cubit.dart new file mode 100644 index 000000000..35d5f3174 --- /dev/null +++ b/lib/wallet_settings/bloc/keychain_cubit.dart @@ -0,0 +1,201 @@ +import 'package:bb_mobile/_pkg/consts/configs.dart'; +import 'package:bb_mobile/wallet_settings/bloc/keychain_state.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:hex/hex.dart'; +import 'package:recoverbull/recoverbull.dart'; + +class KeychainCubit extends Cubit { + KeychainCubit() : super(const KeychainState()) { + _init(); + } + + void _init() => shuffleAndEmit(); + + void shuffleAndEmit() { + final shuffledList = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]..shuffle(); + emit( + state.copyWith( + shuffledNumbers: shuffledList, + error: '', + ), + ); + } + + void changeInputType(KeyChainInputType type) { + emit( + state.copyWith( + inputType: type, + pin: [], + password: '', + error: '', + pageState: KeyChainPageState.enter, + tempPin: '', + tempPassword: '', + pinConfirmed: false, + passwordConfirmed: false, + ), + ); + } + + void keyPressed(String key) { + if (state.pin.length >= 6) return; + emit( + state.copyWith( + pin: List.from(state.pin)..add(key), + error: '', + ), + ); + } + + void backspacePressed() { + if (state.pin.isEmpty) return; + emit( + state.copyWith( + pin: List.from(state.pin)..removeLast(), + error: '', + ), + ); + } + + void passwordChanged(String password) { + emit( + state.copyWith( + password: password, + error: '', + ), + ); + } + + void clearError() => emit( + state.copyWith( + error: '', + ), + ); + void clearSensitive() { + clearError(); + emit( + state.copyWith( + pin: [], + password: '', + tempPin: '', + tempPassword: '', + pinConfirmed: false, + passwordConfirmed: false, + ), + ); + } + + void confirmPressed() { + if (!state.showButton()) return; + + state.inputType == KeyChainInputType.pin + ? _confirmPin() + : _confirmPassword(); + } + + void _confirmPin() { + if (state.pageState == KeyChainPageState.enter) { + if (state.pin.length < 6) { + emit(state.copyWith(error: 'PIN must be at least 6 digits')); + return; + } + + emit( + state.copyWith( + pageState: KeyChainPageState.confirm, + tempPin: state.pin.join(), + pin: [], + error: '', + ), + ); + return; + } + + if (state.pin.join() != state.tempPin) { + emit( + state.copyWith( + pageState: KeyChainPageState.enter, + tempPin: '', + pin: [], + error: 'PINs do not match. Please try again.', + ), + ); + return; + } + emit( + state.copyWith( + pinConfirmed: true, + error: '', + ), + ); + } + + void _confirmPassword() { + if (!state.isPasswordValid) { + emit(state.copyWith(error: 'Password must be at least 6 characters')); + return; + } + + if (state.pageState == KeyChainPageState.enter) { + emit( + state.copyWith( + pageState: KeyChainPageState.confirm, + tempPassword: state.password, + password: '', + error: '', + ), + ); + return; + } + + if (state.password != state.tempPassword) { + emit( + state.copyWith( + pageState: KeyChainPageState.enter, + tempPassword: '', + password: '', + error: 'Passwords do not match. Please try again.', + ), + ); + return; + } + + emit( + state.copyWith( + passwordConfirmed: true, + error: '', + ), + ); + } + + Future secureKey( + String backupId, + String backupKey, + String backupSalt, + ) async { + final pinOrPassword = state.inputType == KeyChainInputType.pin + ? state.tempPin + : state.tempPassword; + try { + emit(state.copyWith(saving: true, error: '')); + await KeyService(keyServer: Uri.parse(keychainapi)).storeBackupKey( + backupId: backupId, + password: pinOrPassword, + backupKey: HEX.decode(backupKey), + salt: HEX.decode(backupSalt), + ); + + emit(state.copyWith(saved: true, saving: false)); + } catch (e) { + debugPrint('Failed to store backup key on server: $e'); + emit( + state.copyWith( + saving: false, + error: 'Failed to store backup key on server', + passwordConfirmed: false, + ), + ); + } + } +} diff --git a/lib/wallet_settings/bloc/keychain_state.dart b/lib/wallet_settings/bloc/keychain_state.dart new file mode 100644 index 000000000..3c6c51622 --- /dev/null +++ b/lib/wallet_settings/bloc/keychain_state.dart @@ -0,0 +1,43 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'keychain_state.freezed.dart'; + +enum KeyChainPageState { enter, confirm } + +enum KeyChainInputType { pin, password } + +@freezed +class KeychainState with _$KeychainState { + const factory KeychainState({ + @Default(false) bool saving, + @Default(false) bool saved, + @Default('') String error, + @Default(false) bool loading, + @Default([]) List pin, + @Default('') String password, + @Default([]) List shuffledNumbers, + @Default(KeyChainPageState.enter) KeyChainPageState pageState, + @Default(KeyChainInputType.pin) KeyChainInputType inputType, + @Default('') String tempPin, + @Default('') String tempPassword, + @Default(false) bool pinConfirmed, + @Default(false) bool passwordConfirmed, + }) = _KeychainState; + const KeychainState._(); + + String displayPin() { + final hide = List.filled(pin.length, 'x').join(''); + return hide; + } + + bool showButton() { + if (inputType == KeyChainInputType.pin) { + return pin.length >= 6; + } else { + //TODO: Implement password validation + return password.isNotEmpty && password.length >= 6; + } + } + + bool get isPasswordValid => password.length >= 6; +} diff --git a/lib/wallet_settings/keychain_page.dart b/lib/wallet_settings/keychain_page.dart new file mode 100644 index 000000000..5fa9a8c31 --- /dev/null +++ b/lib/wallet_settings/keychain_page.dart @@ -0,0 +1,536 @@ +import 'package:bb_mobile/_ui/app_bar.dart'; +import 'package:bb_mobile/_ui/components/text.dart'; +import 'package:bb_mobile/_ui/components/text_input.dart'; +import 'package:bb_mobile/_ui/page_template.dart'; +import 'package:bb_mobile/_ui/toast.dart'; +import 'package:bb_mobile/styles.dart'; +import 'package:bb_mobile/wallet_settings/bloc/keychain_cubit.dart'; +import 'package:bb_mobile/wallet_settings/bloc/keychain_state.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_animate/flutter_animate.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:gap/gap.dart'; +import 'package:go_router/go_router.dart'; + +class KeychainBackupPage extends StatelessWidget { + const KeychainBackupPage({ + super.key, + required this.backupKey, + required this.backupId, + required this.backupSalt, + }); + + final String backupSalt; + final String backupKey; + final String backupId; + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => KeychainCubit(), + child: _Screen( + backupId: backupId, + backupKey: backupKey, + backupSalt: backupSalt, + ), + ); + } +} + +class _Screen extends StatelessWidget { + const _Screen({ + required this.backupKey, + required this.backupId, + required this.backupSalt, + }); + final String backupSalt; + final String backupKey; + final String backupId; + + @override + Widget build(BuildContext context) { + return MultiBlocListener( + listeners: [ + BlocListener( + listenWhen: (previous, current) => previous.saved != current.saved, + listener: (context, state) { + if (state.saved) { + context.read().clearSensitive(); + context.go('/home'); + } + }, + ), + BlocListener( + listenWhen: (previous, current) => + previous.pinConfirmed != current.pinConfirmed || + previous.passwordConfirmed != current.passwordConfirmed, + listener: (context, state) { + if ((state.pinConfirmed || state.passwordConfirmed) && + !state.saving && + state.error.isEmpty) { + context + .read() + .secureKey(backupId, backupKey, backupSalt); + } + if (state.error.isNotEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + context.showToast(state.error), + ); + } + }, + ), + ], + child: BlocBuilder( + builder: (context, state) { + if (state.saving) return const _LoadingView(); + + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + elevation: 0, + flexibleSpace: BBAppBar( + text: 'Keychain Backup', + onBack: () { + //TODO; clear sensitive data + context.pop(); + }, + ), + ), + body: AnimatedSwitcher( + duration: 300.ms, + child: state.pageState == KeyChainPageState.enter + ? _EnterPage( + key: const ValueKey('enter'), + inputType: state.inputType, + ) + : _ConfirmPage( + key: const ValueKey('confirm'), + inputType: state.inputType, + ), + ), + ); + }, + ), + ); + } +} + +class _LoadingView extends StatelessWidget { + const _LoadingView(); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + child: CircularProgressIndicator( + color: context.colour.onSurface, + ), + ), + ); + } +} + +class _EnterPage extends StatelessWidget { + const _EnterPage({super.key, required this.inputType}); + final KeyChainInputType inputType; + + @override + Widget build(BuildContext context) { + return StackedPage( + bottomChildHeight: MediaQuery.of(context).size.height * 0.1, + bottomChild: _SetButton(inputType: inputType), + child: Padding( + key: ValueKey('enter$inputType'), + padding: const EdgeInsets.symmetric(horizontal: 32), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Gap(50), + const _TitleText(), + const Gap(8), + const _SubtitleText(), + const Gap(50), + if (inputType == KeyChainInputType.pin) ...[ + const _PinField(), + const KeyPad(), + ] else + const _PasswordField(), + const Gap(30), + ], + ), + ), + ); + } +} + +class _ConfirmPage extends StatelessWidget { + const _ConfirmPage({super.key, required this.inputType}); + final KeyChainInputType inputType; + + @override + Widget build(BuildContext context) { + return StackedPage( + bottomChild: _ConfirmButton(inputType: inputType), + child: SingleChildScrollView( + key: ValueKey('confirm$inputType'), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Gap(20), + const _ConfirmTitleText(), + const Gap(8), + const _ConfirmSubtitleText(), + const Gap(48), + if (inputType == KeyChainInputType.pin) ...[ + const _PinField(), + const KeyPad(), + ] else + const _PasswordField(), + const Gap(24), + ], + ), + ), + ), + ); + } +} + +class _PinField extends StatelessWidget { + const _PinField(); + + @override + Widget build(BuildContext context) { + final pin = context.select((KeychainCubit x) => x.state.displayPin()); + return Center( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox(width: 40), + Expanded( + child: Center( + child: BBText.titleLarge( + pin, + isBold: true, + ), + ), + ), + SizedBox( + width: 40, + child: IconButton( + iconSize: 32, + color: pin.isEmpty + ? context.colour.surface + : context.colour.onPrimaryContainer, + splashColor: Colors.transparent, + onPressed: () { + SystemSound.play(SystemSoundType.click); + HapticFeedback.mediumImpact(); + + context.read().backspacePressed(); + }, + icon: const FaIcon(FontAwesomeIcons.deleteLeft), + ), + ), + ], + ), + ); + } +} + +class _PasswordField extends StatelessWidget { + const _PasswordField(); + + @override + Widget build(BuildContext context) { + final password = context.select((KeychainCubit x) => x.state.password); + // final err = context.select((KeychainCubit x) => x.state.err); + return BBTextInput.big( + value: password, + onChanged: (value) { + context.read().passwordChanged(value); + }, + hint: 'Enter your password', + ); + } +} + +class _TitleText extends StatelessWidget { + const _TitleText(); + + @override + Widget build(BuildContext context) { + final (inputState, type) = context + .select((KeychainCubit x) => (x.state.pageState, x.state.inputType)); + final text = inputState == KeyChainPageState.enter + ? 'Choose a backup ${type == KeyChainInputType.pin ? 'PIN' : 'password'}' + : 'Confirm backup ${type == KeyChainInputType.pin ? 'PIN' : 'password'}'; + return BBText.titleLarge( + textAlign: TextAlign.center, + text, + isBold: true, + ); + } +} + +class _ConfirmTitleText extends StatelessWidget { + const _ConfirmTitleText(); + + @override + Widget build(BuildContext context) { + final (pageState, inputType) = context + .select((KeychainCubit x) => (x.state.pageState, x.state.inputType)); + final text = + 'Confirm backup ${inputType == KeyChainInputType.pin ? 'PIN' : 'password'}'; + + return BBText.titleLarge( + textAlign: TextAlign.center, + text, + isBold: true, + ); + } +} + +class _ConfirmSubtitleText extends StatelessWidget { + const _ConfirmSubtitleText(); + + @override + Widget build(BuildContext context) { + final inputType = context.select((KeychainCubit x) => x.state.inputType); + return BBText.bodySmall( + textAlign: TextAlign.center, + 'Enter the ${inputType == KeyChainInputType.pin ? 'PIN' : 'password'} again to confirm', + ); + } +} + +class _SubtitleText extends StatelessWidget { + const _SubtitleText(); + + @override + Widget build(BuildContext context) { + final inputType = context.select((KeychainCubit x) => x.state.inputType); + final text = + 'You must memorize this ${inputType == KeyChainInputType.pin ? 'PIN' : 'password'} to recover access to your wallet. It must be at least 6 digits.'; + return BBText.bodySmall( + textAlign: TextAlign.center, + text, + ); + } +} + +class _SetButton extends StatelessWidget { + const _SetButton({required this.inputType}); + final KeyChainInputType inputType; + @override + Widget build(BuildContext context) { + final showButton = + context.select((KeychainCubit x) => x.state.showButton()); + + return AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: Column( + children: [ + InkWell( + onTap: () { + final cubit = context.read(); + if (inputType == KeyChainInputType.pin) { + cubit.changeInputType(KeyChainInputType.password); + } else { + cubit.changeInputType(KeyChainInputType.pin); + } + }, + child: BBText.bodySmall( + inputType == KeyChainInputType.pin + ? 'Use a password instead of a pin' + : 'Use a PIN instead of a password', + ), + ), + const Gap(5), + FilledButton( + onPressed: () { + if (showButton) context.read().confirmPressed(); + }, + style: FilledButton.styleFrom( + backgroundColor: + showButton ? context.colour.shadow : context.colour.surface, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Set ${inputType == KeyChainInputType.pin ? 'PIN' : 'password'}', + style: context.font.bodyMedium!.copyWith( + color: Colors.white, + fontWeight: FontWeight.w900, + ), + ), + const SizedBox(width: 8), + const Icon( + Icons.arrow_forward, + color: Colors.white, + size: 16, + ), + ], + ), + ), + ], + ), + ); + } +} + +class _ConfirmButton extends StatelessWidget { + const _ConfirmButton({required this.inputType}); + final KeyChainInputType inputType; + @override + Widget build(BuildContext context) { + final showButton = + context.select((KeychainCubit x) => x.state.showButton()); + final err = context.select((KeychainCubit x) => x.state.error); + + if (err.isNotEmpty && inputType == KeyChainInputType.password) { + return Center( + child: BBText.errorSmall( + err, + ), + ); + } + return FilledButton( + onPressed: () { + if (showButton) context.read().confirmPressed(); + }, + style: FilledButton.styleFrom( + backgroundColor: + showButton ? context.colour.shadow : context.colour.surface, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Confirm ${inputType == KeyChainInputType.pin ? 'PIN' : 'password'}', + style: context.font.bodyMedium!.copyWith( + color: Colors.white, + fontWeight: FontWeight.w900, + ), + ), + const SizedBox(width: 8), + const Icon( + Icons.arrow_forward, + color: Colors.white, + size: 16, + ), + ], + ), + ); + } +} + +class NumberButton extends StatefulWidget { + const NumberButton({super.key, required this.text}); + + final String text; + + @override + State createState() => _NumberButtonState(); +} + +class _NumberButtonState extends State { + bool isRed = false; + + @override + Widget build(BuildContext context) { + OutlinedButton.styleFrom( + shape: const CircleBorder(), + backgroundColor: context.colour.onPrimaryContainer, + foregroundColor: context.colour.primary, + ); + + OutlinedButton.styleFrom( + shape: const CircleBorder(), + backgroundColor: context.colour.primary, + foregroundColor: context.colour.primaryContainer, + ); + + return Center( + child: SizedBox( + height: 80, + width: 80, + child: GestureDetector( + onTapUp: (e) { + setState(() { + isRed = false; + }); + }, + onTapDown: (e) { + setState(() { + isRed = true; + }); + }, + onTapCancel: () { + setState(() { + isRed = false; + }); + }, + child: OutlinedButton( + style: OutlinedButton.styleFrom( + splashFactory: NoSplash.splashFactory, + ), + onPressed: () { + SystemSound.play(SystemSoundType.click); + HapticFeedback.mediumImpact(); + + context.read().keyPressed(widget.text); + }, + child: BBText.titleLarge( + widget.text, + isBold: true, + ), + ).animate().blur( + begin: const Offset(1, 1), + end: isRed ? const Offset(2, 2) : Offset.zero, + ), + ), + ), + ); + } +} + +class KeyPad extends StatelessWidget { + const KeyPad({super.key}); + + @override + Widget build(BuildContext context) { + final shuffledNumbers = + context.select((KeychainCubit x) => x.state.shuffledNumbers); + final shuffledNumberButtonList = [ + for (final i in shuffledNumbers) NumberButton(text: i.toString()), + ]; + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + ), + child: GridView( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + ), + children: [ + for (var i = 0; i < 9; i = i + 1) shuffledNumberButtonList[i], + Container(), + shuffledNumberButtonList[9], + ], + ), + ); + } +} From 6928f9afe2e1d68be23fead2f95e112851839d24 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Wed, 5 Feb 2025 04:44:53 -0500 Subject: [PATCH 114/401] feat(button): add new button types withColour and textWithStatus --- lib/_ui/components/button.dart | 185 ++++++++++++++++++++++++++++++++- 1 file changed, 184 insertions(+), 1 deletion(-) diff --git a/lib/_ui/components/button.dart b/lib/_ui/components/button.dart index aa2897ded..5048c58a5 100644 --- a/lib/_ui/components/button.dart +++ b/lib/_ui/components/button.dart @@ -10,10 +10,12 @@ import 'package:gap/gap.dart'; enum _ButtonType { big, + withColour, text, textWithRightArrow, textWithLeftArrow, textWithStatusAndRightArrow, + textWithStatus, } class BBButton extends StatelessWidget { @@ -34,6 +36,29 @@ class BBButton extends StatelessWidget { }) : type = _ButtonType.big, isBlue = null, isRed = null, + isGreen = null, + statusText = null, + centered = null, + onSurface = null; + + const BBButton.withColour({ + required this.label, + required this.onPressed, + this.leftIcon, + this.buttonKey, + this.disabled = false, + this.filled = false, + this.loading = false, + this.loadingText, + this.fillWidth = false, + this.leftSvgAsset, + this.leftImage, + this.center = false, + this.fontSize, + }) : type = _ButtonType.withColour, + isBlue = null, + isRed = null, + isGreen = null, statusText = null, centered = null, onSurface = null; @@ -53,6 +78,7 @@ class BBButton extends StatelessWidget { this.center = false, }) : type = _ButtonType.text, filled = false, + isGreen = null, statusText = null, leftIcon = null, leftSvgAsset = null, @@ -70,6 +96,7 @@ class BBButton extends StatelessWidget { }) : type = _ButtonType.textWithRightArrow, filled = false, isBlue = null, + isGreen = null, isRed = null, statusText = null, centered = null, @@ -91,6 +118,7 @@ class BBButton extends StatelessWidget { }) : type = _ButtonType.textWithLeftArrow, filled = false, isBlue = null, + isGreen = null, isRed = null, statusText = null, centered = null, @@ -114,6 +142,28 @@ class BBButton extends StatelessWidget { this.center = false, }) : type = _ButtonType.textWithStatusAndRightArrow, filled = false, + isGreen = null, + centered = null, + leftIcon = null, + leftSvgAsset = null, + leftImage = null, + fillWidth = true, + onSurface = null, + fontSize = null; + const BBButton.textWithStatus({ + required this.label, + required this.onPressed, + this.disabled = false, + this.loading = false, + this.loadingText, + this.isGreen = false, + this.isRed = false, + this.statusText, + this.buttonKey, + this.center = false, + }) : type = _ButtonType.textWithStatus, + filled = false, + isBlue = null, centered = null, leftIcon = null, leftSvgAsset = null, @@ -121,11 +171,11 @@ class BBButton extends StatelessWidget { fillWidth = true, onSurface = null, fontSize = null; - final String label; final String? statusText; final bool? isRed; final bool? isBlue; + final bool? isGreen; final Function onPressed; final bool filled; final bool disabled; @@ -355,6 +405,139 @@ class BBButton extends StatelessWidget { ], ), ); + + case _ButtonType.textWithStatus: + widget = TextButton( + style: TextButton.styleFrom(padding: EdgeInsets.zero), + onPressed: disabled ? null : () => onPressed(), + child: Row( + children: [ + BBText.title( + label, isRed: true, // TODO ; Make this adaptive + ), + const Spacer(), + if (statusText != null) + AnimatedSwitcher( + duration: 600.ms, + child: !loading + ? BBText.title( + statusText!, + isBold: true, + isRed: isRed ?? false, + isGreen: isGreen ?? false, + ) + : SizedBox( + height: 8, + width: 66, + child: LinearProgressIndicator( + valueColor: AlwaysStoppedAnimation( + context.colour.primary, + ), + backgroundColor: context.colour.primaryContainer, + ), + ), + ), + const Gap(8), + ], + ), + ); + + case _ButtonType.withColour: + final style = ElevatedButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + backgroundColor: darkMode + ? context.colour.primaryContainer + : NewColours.lightGray.withValues(alpha: 0.25), + surfaceTintColor: + darkMode ? context.colour.primaryContainer : NewColours.lightGray, + // shadowColor: Colors.transparent, + elevation: 0, + padding: const EdgeInsets.symmetric(vertical: 8), + + // disabledForegroundColor: context.colour.onPrimaryContainer, + ); + + if (!loading) { + widget = ElevatedButton( + key: buttonKey, + style: style, + onPressed: disabled ? null : () => onPressed(), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (leftSvgAsset != null) ...[ + SvgPicture.asset( + leftSvgAsset!, + colorFilter: ColorFilter.mode( + context.colour.onPrimaryContainer, + BlendMode.srcIn, + ), + ), + const Gap(16), + ] else if (leftIcon != null) ...[ + Icon( + leftIcon, + color: context.colour.onPrimaryContainer, + ), + const Gap(16), + ] else if (leftImage != null) ...[ + Image.asset( + leftImage!, + height: 32, + width: 32, + ), + const Gap(16), + ], + BBText.titleLarge( + label, + fontSize: fontSize ?? 16, + isBold: true, + ), + ], + ), + ); + } else { + widget = ElevatedButton( + style: style, + onPressed: () {}, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Gap(8), + SizedBox( + height: 16, + width: 16, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: + AlwaysStoppedAnimation(context.colour.surface), + ), + ), + const Gap(8), + BBText.title( + loadingText ?? label, + // isRed: !filled, + onSurface: filled, + ), + ], + ), + ); + } + + widget = Container( + decoration: const BoxDecoration( + borderRadius: BorderRadius.all( + Radius.circular(8.0), + ), + ), + child: SizedBox( + height: 45, + width: fillWidth ? null : 225, + child: widget, + ), + ); } if (center) widget = Center(child: widget); From 7895e4f4e18839bc8083dd61ddef6dc69dfad381 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Wed, 5 Feb 2025 04:45:28 -0500 Subject: [PATCH 115/401] feat(routes): restructure backup settings routes and remove deprecated routes --- lib/routes.dart | 143 ++++++++++++++++++++++-------------------------- 1 file changed, 64 insertions(+), 79 deletions(-) diff --git a/lib/routes.dart b/lib/routes.dart index b668738a8..e04b0fddd 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -2,11 +2,8 @@ import 'dart:async'; import 'package:bb_mobile/_model/swap.dart'; import 'package:bb_mobile/_model/transaction.dart'; -import 'package:bb_mobile/_model/wallet.dart'; import 'package:bb_mobile/_ui/logger_page.dart'; import 'package:bb_mobile/auth/page.dart'; -import 'package:bb_mobile/backup/backup_page.dart'; -import 'package:bb_mobile/backup/keychain_page.dart'; import 'package:bb_mobile/create/page.dart'; import 'package:bb_mobile/home/home_page.dart'; import 'package:bb_mobile/home/market.dart'; @@ -15,10 +12,6 @@ import 'package:bb_mobile/import/hardware_page.dart'; import 'package:bb_mobile/import/page.dart'; import 'package:bb_mobile/locator.dart'; import 'package:bb_mobile/receive/receive_page.dart'; -import 'package:bb_mobile/recover/bloc/cloud_cubit.dart'; -import 'package:bb_mobile/recover/cloud_page.dart'; -import 'package:bb_mobile/recover/keychain_page.dart'; -import 'package:bb_mobile/recover/manual_page.dart'; import 'package:bb_mobile/send/bloc/send_cubit.dart'; import 'package:bb_mobile/send/send_page.dart'; import 'package:bb_mobile/settings/application_settings_page.dart'; @@ -39,8 +32,11 @@ import 'package:bb_mobile/wallet/details.dart'; import 'package:bb_mobile/wallet/information_page.dart'; import 'package:bb_mobile/wallet/wallet_page.dart'; import 'package:bb_mobile/wallet_settings/accounting.dart'; -import 'package:bb_mobile/wallet_settings/backup.dart'; +import 'package:bb_mobile/wallet_settings/backup_settings.dart'; import 'package:bb_mobile/wallet_settings/bip85_paths.dart'; +import 'package:bb_mobile/wallet_settings/encrypted_vault_backup.dart'; +import 'package:bb_mobile/wallet_settings/keychain_page.dart'; +import 'package:bb_mobile/wallet_settings/physical_backup.dart'; import 'package:bb_mobile/wallet_settings/test_backup.dart'; import 'package:bb_mobile/wallet_settings/wallet_settings_page.dart'; import 'package:flutter/material.dart'; @@ -200,54 +196,67 @@ GoRouter setupRouter() => GoRouter( final wallet = state.extra! as String; return WalletSettingsPage(wallet: wallet); }, + routes: [ + GoRoute( + path: 'backup-settings', + builder: (context, state) => BackupSettings( + wallet: state.extra! as String, + ), + routes: [ + GoRoute( + path: 'backup-options', + builder: (context, state) => BackupOptionsScreen( + wallet: state.extra! as String, + ), + ), + GoRoute( + path: 'physical', + builder: (context, state) => PhysicalBackupPage( + wallet: state.extra! as String, + ), + routes: [ + GoRoute( + path: 'test-backup', + builder: (context, state) { + final wallet = state.extra! as String; + return TestBackupPage( + wallet: wallet, + // walletSettings: blocs.$2, + ); + // const WalletSettingsPage(openTestBackup: true); + }, + ), + ], + ), + GoRoute( + path: 'encrypted', + builder: (context, state) => EncryptedVaultBackupPage( + wallet: state.extra! as String, + ), + ), + GoRoute( + path: 'keychain', + builder: (context, state) { + final (backupId, (backupKey, backupSalt)) = + state.extra! as (String, (String, String)); + return KeychainBackupPage( + backupId: backupId, + backupKey: backupKey, + backupSalt: backupSalt, + ); + }, + ), + GoRoute( + path: 'recover-options', + builder: (context, state) => RecoverOptionsScreen( + wallet: state.extra! as String, + ), + ), + ], + ), + ], ), - GoRoute( - path: '/backupbull', - builder: (context, state) { - final wallets = state.extra! as List; - return ManualBackupPage(wallets: wallets); - }, - ), - GoRoute( - path: '/recoverbull', - builder: (context, state) { - final wallets = state.extra! as List; - return ManualRecoverPage(wallets: wallets); - }, - ), - GoRoute( - path: '/keychain-backup', - builder: (context, state) { - final (backupKey, backupId) = state.extra! as (String, String); - return KeychainBackupPage(backupKey: backupKey, backupId: backupId); - }, - ), - GoRoute( - path: '/cloud-backup', - builder: (context, state) { - final cloudCubit = state.extra! as CloudCubit; - - return BlocProvider.value( - value: cloudCubit, - child: const CloudPage(), - ); - }, - ), - - GoRoute( - path: '/keychain-recover', - builder: (context, state) { - final backupId = state.extra! as String; - return KeychainRecoverPage(backupId: backupId); - }, - ), - - // GoRoute( - // path: '/wallet-settings/open-test-backup', - // builder: (context, state) { - // return const WalletSettingsPage(openTestBackup: true); - // }, - // ), + //TODO: refactor route GoRoute( path: '/wallet-settings/open-backup', builder: (context, state) { @@ -333,30 +342,6 @@ GoRouter setupRouter() => GoRouter( }, ), - // - // - - GoRoute( - path: '/wallet-settings/test-backup', - builder: (context, state) { - final wallet = state.extra! as String; - return TestBackupPage( - wallet: wallet, - // walletSettings: blocs.$2, - ); - // const WalletSettingsPage(openTestBackup: true); - }, - ), - GoRoute( - path: '/wallet-settings/backup', - builder: (context, state) { - final wallet = state.extra! as String; - - return BackupPage( - wallet: wallet, - ); - }, - ), GoRoute( path: '/wallet-settings/bip85-paths', builder: (context, state) { From acb1a6e8061d26c8c8327245a139915cec94df67 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Wed, 5 Feb 2025 19:27:24 -0500 Subject: [PATCH 116/401] code cleanup --- lib/wallet_settings/bip85_paths.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/wallet_settings/bip85_paths.dart b/lib/wallet_settings/bip85_paths.dart index 3e0e34f78..c7081e2ac 100644 --- a/lib/wallet_settings/bip85_paths.dart +++ b/lib/wallet_settings/bip85_paths.dart @@ -2,7 +2,7 @@ import 'package:bb_mobile/_model/wallet.dart'; import 'package:bb_mobile/_ui/app_bar.dart'; import 'package:bb_mobile/_ui/components/text.dart'; import 'package:bb_mobile/_ui/toast.dart'; -import 'package:bb_mobile/wallet_settings/bloc/state.dart'; +import 'package:bb_mobile/wallet_settings/bloc/wallet_settings_state.dart'; import 'package:bb_mobile/wallet_settings/bloc/wallet_settings_cubit.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; From 15678dbe6e8554c520aa25bfff50ba7527b92985 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Wed, 5 Feb 2025 19:45:32 -0500 Subject: [PATCH 117/401] fix(backup): update JSON decoding to use 'id' key for backup identification --- lib/_pkg/backup/local.dart | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/_pkg/backup/local.dart b/lib/_pkg/backup/local.dart index 663c6620c..8692416ae 100644 --- a/lib/_pkg/backup/local.dart +++ b/lib/_pkg/backup/local.dart @@ -36,10 +36,9 @@ class FileSystemBackupManager extends IBackupManager { required String encrypted, }) async { try { - final decodeEncryptedFile = - jsonDecode(utf8.decode(HEX.decode(encrypted))) as Map; - - final id = decodeEncryptedFile['backupId']?.toString() ?? ''; + final decodeEncryptedFile = jsonDecode(utf8.decode(HEX.decode(encrypted))) + as Map; + final id = decodeEncryptedFile['id']?.toString() ?? ''; if (id.isEmpty) { return (null, Err("Corrupted backup file")); } From 7530bf79626d39302ba99c85758ca981eba5de33 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Wed, 5 Feb 2025 19:47:08 -0500 Subject: [PATCH 118/401] feat(GoogleDriveBackupManager): improved error handling & connection management --- lib/_pkg/backup/google_drive.dart | 225 ++++++++++++++++-------------- 1 file changed, 118 insertions(+), 107 deletions(-) diff --git a/lib/_pkg/backup/google_drive.dart b/lib/_pkg/backup/google_drive.dart index 41e672c8d..357583c02 100644 --- a/lib/_pkg/backup/google_drive.dart +++ b/lib/_pkg/backup/google_drive.dart @@ -5,13 +5,18 @@ import 'package:bb_mobile/_pkg/backup/_interface.dart'; import 'package:bb_mobile/_pkg/consts/configs.dart'; import 'package:bb_mobile/_pkg/error.dart'; import 'package:extension_google_sign_in_as_googleapis_auth/extension_google_sign_in_as_googleapis_auth.dart'; +import 'package:flutter/foundation.dart'; import 'package:google_sign_in/google_sign_in.dart'; import 'package:googleapis/drive/v3.dart'; -import 'package:hex/hex.dart'; -///TODO; Update it to select the cloud backup provider class GoogleDriveBackupManager extends IBackupManager { static final _google = GoogleSignIn(scopes: [DriveApi.driveFileScope]); + static const _errorMessages = { + 'connection': 'Google Sign-In was cancelled or failed. Please try again.', + 'auth': 'Failed to authenticate with Google.', + 'notConnected': 'Not connected to Google Drive', + 'noBackups': 'No backups found', + }; DriveApi? _api; GoogleSignInAccount? _account; @@ -19,32 +24,74 @@ class GoogleDriveBackupManager extends IBackupManager { Future<(String?, Err?)> connect() async { try { final account = await _google.signIn(); - if (account == null) { - return ( - null, - Err('Google Sign-In was cancelled or failed. Please try again.') - ); - } + if (account == null) return (null, Err(_errorMessages['connection']!)); final client = await _google.authenticatedClient(); - if (client == null) { - return (null, Err('Failed to authenticate with Google.')); - } + if (client == null) return (null, Err(_errorMessages['auth']!)); _api = DriveApi(client); _account = account; - final (folderId, err) = await _setupBackupFolder(); - if (err != null) { - return (null, err); - } - return (folderId, null); + return await _setupBackupFolder(); } catch (e) { - return (null, Err('An unexpected error occurred: $e')); + await disconnect(); + return (null, Err('Connection error: $e')); } } - Future disconnect() async => _google.disconnect(); + Future disconnect() async { + await _google.disconnect(); + _api = null; + _account = null; + } + + Future dispose() async { + await disconnect(); + } + + // Helper method to ensure connection + Future<(T?, Err?)> _withConnection( + Future<(T?, Err?)> Function(DriveApi api) operation, + ) async { + if (_api == null) return (null, Err('Not connected')); + + try { + return await operation(_api!); + } catch (e) { + await disconnect(); + return (null, Err('Operation failed: $e')); + } + } + + @override + Future<(String?, Err?)> saveEncryptedBackup({ + required String encrypted, + String backupFolder = defaultBackupPath, + }) async { + return _withConnection((api) async { + try { + final data = jsonDecode(encrypted) as Map; + final backupId = data['id']?.toString(); + if (backupId == null) return (null, Err('Invalid backup data')); + + final filename = + '${DateTime.now().millisecondsSinceEpoch}_$backupId.json'; + final file = File() + ..name = filename + ..parents = [backupFolder]; + + await api.files.create( + file, + uploadMedia: + Media(Stream.value(utf8.encode(encrypted)), encrypted.length), + ); + + return (filename, null); + } catch (e) { + return (null, Err('Save failed: $e')); + } + }); + } Future<(String?, Err?)> _setupBackupFolder() async { try { @@ -92,78 +139,41 @@ class GoogleDriveBackupManager extends IBackupManager { } } - @override - Future<(String?, Err?)> saveEncryptedBackup({ - required String encrypted, - String backupFolder = defaultBackupPath, - }) async { - if (_api == null) return (null, Err('Not connected to Google Drive')); - - try { - final decodeEncryptedFile = jsonDecode(encrypted) as Map; - final backupId = decodeEncryptedFile['id']?.toString() ?? ''; - final now = DateTime.now(); - final formattedDate = now.millisecondsSinceEpoch; - final filename = '${formattedDate}_$backupId.json'; - final file = File() - ..name = filename - ..parents = [backupFolder]; - - final data = encrypted; - await _api!.files.create( - file, - uploadMedia: Media(Stream.value(utf8.encode(data)), data.length), - ); - - return (filename, null); - } catch (e) { - return (null, Err('Failed to create backup: $e')); - } - } - @override Future<(Map?, Err?)> loadEncryptedBackup({ required String encrypted, }) async { try { - final decodeEncryptedFile = jsonDecode(utf8.decode(HEX.decode(encrypted))) - as Map; - final id = decodeEncryptedFile['backupId']; - if (id == null) { - return (null, Err("Corrupted backup file")); - } + final decodeEncryptedFile = jsonDecode(encrypted) as Map; return (decodeEncryptedFile, null); } catch (e) { - return (null, Err('Failed to read encrypted backup: $e')); + debugPrint('Failed to decode backup: $e'); + return (null, Err('Failed to decode backup')); } } - Future<(Map?, Err?)> loadAllEncryptedBackupFiles({ + Future<(List?, Err?)> loadAllEncryptedBackupFiles({ required String backupFolder, }) async { - if (_api == null) return (null, Err('Not connected to Google Drive')); - - try { - final response = await _api!.files.list( - q: "'$backupFolder' in parents and trashed = false", - spaces: 'drive', - $fields: 'files(id, name, createdTime)', - orderBy: 'createdTime desc', - ); + return _withConnection((api) async { + try { + final response = await api.files.list( + q: "'$backupFolder' in parents and trashed = false", + spaces: 'drive', + $fields: 'files(id, name, createdTime)', + orderBy: 'createdTime desc', + ); - if (response.files == null || response.files!.isEmpty) { - return (null, Err('No backups found')); - } + final files = response.files; + if (files == null || files.isEmpty) { + return (null, Err(_errorMessages['noBackups']!)); + } - final backups = {}; - for (final file in response.files!) { - backups[file.name!] = file; + return (files, null); + } catch (e) { + return (null, Err('Failed to load backups: $e')); } - - return (backups, null); - } catch (e) { - return (null, Err('Failed to load backups: $e')); - } + }); } @override @@ -171,43 +181,44 @@ class GoogleDriveBackupManager extends IBackupManager { required String backupName, String backupFolder = defaultBackupPath, }) async { - if (_api == null) return (null, Err('Not connected to Google Drive')); + return _withConnection((api) async { + try { + final files = await api.files.list( + q: "'$backupFolder' in parents and name = '$backupName' and trashed = false", + spaces: 'drive', + $fields: 'files(id)', + ); - try { - final response = await _api!.files.list( - q: "'$backupFolder' in parents and name = '$backupName' and trashed = false", - spaces: 'drive', - $fields: 'files(id)', - ); + final firstFile = files.files?.firstOrNull; + if (firstFile == null) { + return (null, Err('Backup not found')); + } - if (response.files == null || response.files!.isEmpty) { - return (null, Err('Backup not found')); + await api.files.delete(firstFile.id!); + return (backupName, null); + } catch (e) { + return (null, Err('Failed to remove backup: $e')); } + }); + } + + Future<(List?, Err?)> fetchMediaStream({required File file}) async { + if (_api == null) return (null, Err(_errorMessages['notConnected']!)); - await _api!.files.delete(response.files!.first.id!); - return (backupName, null); + try { + final media = await _api!.files.get( + file.id!, + downloadOptions: DownloadOptions.fullMedia, + ) as Media; + + final bytes = await media.stream.fold>( + [], + (previous, element) => previous..addAll(element), + ); + + return (bytes, null); } catch (e) { - return (null, Err('Failed to remove backup: $e')); + return (null, Err('Failed to fetch backup data: $e')); } } - - Future> fetchMediaStream({required File file}) async { - final media = await _api!.files.get( - file.id!, - downloadOptions: DownloadOptions.fullMedia, - ) as Media; - - final completer = Completer>(); - final bytes = []; - - media.stream.listen( - bytes.addAll, - onError: (error) => completer.completeError( - Exception('Error streaming backup data: $error'), - ), - onDone: () => completer.complete(bytes), - cancelOnError: true, - ); - return completer.future; - } } From c649139d699be520639969edea191294235ddc1a Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Wed, 5 Feb 2025 19:47:25 -0500 Subject: [PATCH 119/401] feat(backup): add additional fields --- lib/_model/backup.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/_model/backup.dart b/lib/_model/backup.dart index 238a59a13..ca8ec7817 100644 --- a/lib/_model/backup.dart +++ b/lib/_model/backup.dart @@ -8,10 +8,13 @@ class Backup with _$Backup { const factory Backup({ @Default(1) int version, @Default('') String name, - @Default('') String network, @Default([]) List mnemonic, @Default('') String passphrase, @Default('') String mnemonicFingerPrint, + @Default('') String network, + @Default('') String layer, + @Default('') String type, + @Default('') String script, }) = _Backup; factory Backup.fromJson(Map json) => _$BackupFromJson(json); From 549905858fbeb9d58feca64ef9e2ca2bac2a8f78 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Wed, 5 Feb 2025 19:47:55 -0500 Subject: [PATCH 120/401] feat(backup): add loading state and latest recovered backup --- lib/wallet_settings/bloc/backup_settings_state.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/wallet_settings/bloc/backup_settings_state.dart b/lib/wallet_settings/bloc/backup_settings_state.dart index cba36295b..a78d1544c 100644 --- a/lib/wallet_settings/bloc/backup_settings_state.dart +++ b/lib/wallet_settings/bloc/backup_settings_state.dart @@ -16,6 +16,7 @@ class BackupSettingsState with _$BackupSettingsState { @Default(false) bool testingBackup, @Default('') String errTestingBackup, @Default(false) bool backupTested, + @Default(false) bool loadingBackups, @Default([]) List loadedBackups, @Default('') String errorLoadingBackups, @Default(false) bool savingBackups, @@ -25,6 +26,7 @@ class BackupSettingsState with _$BackupSettingsState { @Default('') String backupFolderId, @Default('') String backupSalt, @Default('') String backupKey, + @Default({}) Map latestRecoveredBackup, @Default(null) DateTime? lastBackupAttempt, }) = _BackupSettingsState; From 77ff5f8a05a13d867c8f944a2e7e2d4c108ba8f6 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Wed, 5 Feb 2025 19:49:01 -0500 Subject: [PATCH 121/401] feat(keychain): enhance state management with recovery options and validation logic --- lib/wallet_settings/bloc/keychain_state.dart | 49 +++++++++----------- 1 file changed, 23 insertions(+), 26 deletions(-) diff --git a/lib/wallet_settings/bloc/keychain_state.dart b/lib/wallet_settings/bloc/keychain_state.dart index 3c6c51622..755e8a7a0 100644 --- a/lib/wallet_settings/bloc/keychain_state.dart +++ b/lib/wallet_settings/bloc/keychain_state.dart @@ -2,42 +2,39 @@ import 'package:freezed_annotation/freezed_annotation.dart'; part 'keychain_state.freezed.dart'; -enum KeyChainPageState { enter, confirm } +enum KeyChainPageState { enter, confirm, recovery } enum KeyChainInputType { pin, password } +enum KeySecretState { saved, recovered, none } + @freezed class KeychainState with _$KeychainState { const factory KeychainState({ - @Default(false) bool saving, - @Default(false) bool saved, - @Default('') String error, @Default(false) bool loading, - @Default([]) List pin, - @Default('') String password, - @Default([]) List shuffledNumbers, @Default(KeyChainPageState.enter) KeyChainPageState pageState, @Default(KeyChainInputType.pin) KeyChainInputType inputType, - @Default('') String tempPin, - @Default('') String tempPassword, - @Default(false) bool pinConfirmed, - @Default(false) bool passwordConfirmed, + @Default(KeySecretState.none) KeySecretState keySecretState, + @Default('') String secret, + @Default('') String tempSecret, + @Default('') String backupId, + @Default('') String backupKey, + @Default([]) List backupSalt, + @Default(false) bool isSecretConfirmed, + @Default([]) List shuffledNumbers, + @Default('') String error, }) = _KeychainState; + const KeychainState._(); - String displayPin() { - final hide = List.filled(pin.length, 'x').join(''); - return hide; - } - - bool showButton() { - if (inputType == KeyChainInputType.pin) { - return pin.length >= 6; - } else { - //TODO: Implement password validation - return password.isNotEmpty && password.length >= 6; - } - } - - bool get isPasswordValid => password.length >= 6; + String displayPin() => List.filled(secret.length, 'x').join(''); + + bool get isValid => inputType == KeyChainInputType.pin + ? secret.length == 6 + : secret.length >= 6; + + bool get showButton => isValid; + bool get hasError => error.isNotEmpty; + bool get isRecovering => pageState == KeyChainPageState.recovery; + bool get canRecover => backupId.isNotEmpty && isValid && !loading; } From 48f2ee7a9901dba89a6240aa74a00929dea8b285 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Wed, 5 Feb 2025 19:50:24 -0500 Subject: [PATCH 122/401] feat(keychain): refactor state management and enhance key recovery process --- lib/wallet_settings/bloc/keychain_cubit.dart | 203 ++++++++----------- 1 file changed, 90 insertions(+), 113 deletions(-) diff --git a/lib/wallet_settings/bloc/keychain_cubit.dart b/lib/wallet_settings/bloc/keychain_cubit.dart index 35d5f3174..855600bca 100644 --- a/lib/wallet_settings/bloc/keychain_cubit.dart +++ b/lib/wallet_settings/bloc/keychain_cubit.dart @@ -7,193 +7,170 @@ import 'package:recoverbull/recoverbull.dart'; class KeychainCubit extends Cubit { KeychainCubit() : super(const KeychainState()) { - _init(); + shuffleAndEmit(); } - void _init() => shuffleAndEmit(); - void shuffleAndEmit() { final shuffledList = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]..shuffle(); - emit( - state.copyWith( - shuffledNumbers: shuffledList, - error: '', - ), - ); + emit(state.copyWith(shuffledNumbers: shuffledList)); } - void changeInputType(KeyChainInputType type) { + void setChainState( + KeyChainPageState keyChainPageState, + String backupId, + String? backupKey, + String backupSalt, + ) { emit( state.copyWith( - inputType: type, - pin: [], - password: '', - error: '', - pageState: KeyChainPageState.enter, - tempPin: '', - tempPassword: '', - pinConfirmed: false, - passwordConfirmed: false, + pageState: keyChainPageState, + backupKey: backupKey ?? '', + backupId: backupId, + backupSalt: HEX.decode(backupSalt), ), ); } - void keyPressed(String key) { - if (state.pin.length >= 6) return; + void updatePageState( + KeyChainInputType keyChainInputType, + KeyChainPageState keyChainPageState, + ) { emit( state.copyWith( - pin: List.from(state.pin)..add(key), + inputType: keyChainInputType, + pageState: keyChainPageState, error: '', + secret: '', + tempSecret: '', + isSecretConfirmed: false, ), ); } - void backspacePressed() { - if (state.pin.isEmpty) return; - emit( - state.copyWith( - pin: List.from(state.pin)..removeLast(), - error: '', - ), - ); + void updateInput(String value) { + if (state.inputType == KeyChainInputType.pin && value.length > 6) return; + emit(state.copyWith(secret: value, error: '')); } - void passwordChanged(String password) { + void backspacePressed() { + if (state.secret.isEmpty) return; emit( state.copyWith( - password: password, + secret: state.secret.substring(0, state.secret.length - 1), error: '', ), ); } - void clearError() => emit( - state.copyWith( - error: '', - ), - ); - void clearSensitive() { - clearError(); + void keyPressed(String key) { + if (state.secret.length >= 6) return; emit( state.copyWith( - pin: [], - password: '', - tempPin: '', - tempPassword: '', - pinConfirmed: false, - passwordConfirmed: false, + secret: state.secret + key, + error: '', ), ); } void confirmPressed() { - if (!state.showButton()) return; + if (!state.showButton) return; - state.inputType == KeyChainInputType.pin - ? _confirmPin() - : _confirmPassword(); - } - - void _confirmPin() { if (state.pageState == KeyChainPageState.enter) { - if (state.pin.length < 6) { - emit(state.copyWith(error: 'PIN must be at least 6 digits')); - return; - } - emit( state.copyWith( pageState: KeyChainPageState.confirm, - tempPin: state.pin.join(), - pin: [], - error: '', + tempSecret: state.secret, + secret: '', ), ); return; } - if (state.pin.join() != state.tempPin) { + if (state.secret != state.tempSecret) { emit( state.copyWith( pageState: KeyChainPageState.enter, - tempPin: '', - pin: [], - error: 'PINs do not match. Please try again.', + error: 'Values do not match. Please try again.', + secret: '', + tempSecret: '', ), ); return; } - emit( - state.copyWith( - pinConfirmed: true, - error: '', - ), - ); - } - void _confirmPassword() { - if (!state.isPasswordValid) { - emit(state.copyWith(error: 'Password must be at least 6 characters')); - return; - } + emit(state.copyWith(isSecretConfirmed: true)); + } - if (state.pageState == KeyChainPageState.enter) { + Future secureKey() async { + try { + emit(state.copyWith(loading: true, error: '')); + await KeyService(keyServer: Uri.parse(keychainapi)).storeBackupKey( + backupId: state.backupId, + password: state.tempSecret, + backupKey: HEX.decode(state.backupKey), + salt: state.backupSalt, + ); emit( - state.copyWith( - pageState: KeyChainPageState.confirm, - tempPassword: state.password, - password: '', - error: '', - ), + state.copyWith(loading: false, keySecretState: KeySecretState.saved), ); - return; - } - - if (state.password != state.tempPassword) { + } catch (e) { + debugPrint('Failed to store backup key on server: $e'); emit( state.copyWith( - pageState: KeyChainPageState.enter, - tempPassword: '', - password: '', - error: 'Passwords do not match. Please try again.', + loading: false, + error: 'Failed to store backup key on server', ), ); - return; } + } + void clearSensitive() { emit( state.copyWith( - passwordConfirmed: true, + secret: '', + tempSecret: '', + isSecretConfirmed: false, error: '', ), ); } - Future secureKey( - String backupId, - String backupKey, - String backupSalt, - ) async { - final pinOrPassword = state.inputType == KeyChainInputType.pin - ? state.tempPin - : state.tempPassword; + void setBackupId(String id) { + emit(state.copyWith(backupId: id)); + } + + Future clickRecoverKey() async { + if (state.secret.length != 6) { + emit(state.copyWith(error: 'pin should be 6 digits long')); + return; + } + try { - emit(state.copyWith(saving: true, error: '')); - await KeyService(keyServer: Uri.parse(keychainapi)).storeBackupKey( - backupId: backupId, - password: pinOrPassword, - backupKey: HEX.decode(backupKey), - salt: HEX.decode(backupSalt), - ); + emit(state.copyWith(loading: true, error: '')); - emit(state.copyWith(saved: true, saving: false)); + if (keychainapi.isEmpty) { + emit(state.copyWith(loading: false, error: 'keychain api is not set')); + return; + } + final backupKey = + await KeyService(keyServer: Uri.parse(keychainapi)).recoverBackupKey( + backupId: state.backupId, + password: state.secret, + salt: state.backupSalt, + ); + emit( + state.copyWith( + backupKey: HEX.encode(backupKey), + loading: false, + keySecretState: KeySecretState.recovered, + ), + ); } catch (e) { - debugPrint('Failed to store backup key on server: $e'); + debugPrint("Failed to recover backup key: $e"); emit( state.copyWith( - saving: false, - error: 'Failed to store backup key on server', - passwordConfirmed: false, + loading: false, + error: "Failed to recover backup key", ), ); } From 7700ae84cee0f117218ba3e9afebef7b2e92f19d Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Wed, 5 Feb 2025 19:51:48 -0500 Subject: [PATCH 123/401] feat(backup): refactor BackupSettingsCubit to support optional wallet ID and enhance wallet management --- .../bloc/backup_settings_cubit.dart | 50 ++++++++++++++++--- 1 file changed, 42 insertions(+), 8 deletions(-) diff --git a/lib/wallet_settings/bloc/backup_settings_cubit.dart b/lib/wallet_settings/bloc/backup_settings_cubit.dart index 389a72b3b..77dfc33e5 100644 --- a/lib/wallet_settings/bloc/backup_settings_cubit.dart +++ b/lib/wallet_settings/bloc/backup_settings_cubit.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:convert'; import 'package:bb_mobile/_model/backup.dart'; @@ -6,49 +7,82 @@ import 'package:bb_mobile/_model/wallet.dart'; import 'package:bb_mobile/_pkg/backup/google_drive.dart'; import 'package:bb_mobile/_pkg/backup/local.dart'; import 'package:bb_mobile/_pkg/error.dart'; +import 'package:bb_mobile/_pkg/file_picker.dart'; +import 'package:bb_mobile/_pkg/wallet/bdk/sensitive_create.dart'; +import 'package:bb_mobile/_pkg/wallet/create.dart'; +import 'package:bb_mobile/_pkg/wallet/create_sensitive.dart'; +import 'package:bb_mobile/_pkg/wallet/lwk/sensitive_create.dart'; import 'package:bb_mobile/_repository/app_wallets_repository.dart'; import 'package:bb_mobile/_repository/wallet/sensitive_wallet_storage.dart'; +import 'package:bb_mobile/_repository/wallet/wallet_storage.dart'; import 'package:bb_mobile/_repository/wallet_service.dart'; import 'package:bb_mobile/locator.dart'; import 'package:bb_mobile/wallet_settings/bloc/backup_settings_state.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -BackupSettingsCubit createBackupSettingsCubit(String walletId) { +BackupSettingsCubit createBackupSettingsCubit({String? walletId}) { final appWalletsRepo = locator(); - final activeWallet = appWalletsRepo.getWalletById(walletId); + final wallets = appWalletsRepo.allWallets; + + final currentWallet = walletId != null + ? wallets.firstWhere((w) => w.id == walletId, orElse: () => wallets.first) + : wallets.first; + return BackupSettingsCubit( - activeWallet: activeWallet!, - wallets: appWalletsRepo.allWallets, + wallets: wallets, appWalletsRepository: appWalletsRepo, walletSensRepository: locator(), manager: locator(), driveManager: locator(), + lwkSensitiveCreate: locator(), + bdkSensitiveCreate: locator(), + walletCreate: locator(), + walletSensitiveCreate: locator(), + walletsStorageRepository: locator(), + currentWallet: currentWallet, ); } class BackupSettingsCubit extends Cubit { BackupSettingsCubit({ - required Wallet activeWallet, required List wallets, required AppWalletsRepository appWalletsRepository, required WalletSensitiveStorageRepository walletSensRepository, - required FileSystemBackupManager manager, + required LWKSensitiveCreate lwkSensitiveCreate, + required BDKSensitiveCreate bdkSensitiveCreate, + required WalletCreate walletCreate, + required WalletSensitiveCreate walletSensitiveCreate, + required WalletsStorageRepository walletsStorageRepository, required GoogleDriveBackupManager driveManager, + required FileSystemBackupManager manager, + required Wallet? currentWallet, }) : _walletSensRepository = walletSensRepository, _appWalletsRepository = appWalletsRepository, - _wallet = activeWallet, _wallets = wallets, + _currentWallet = currentWallet, _manager = manager, _driveManager = driveManager, + _filePicker = locator(), + _walletSensitiveCreate = walletSensitiveCreate, + _bdkSensitiveCreate = bdkSensitiveCreate, + _walletCreate = walletCreate, + _lwkSensitiveCreate = lwkSensitiveCreate, + _walletsStorageRepository = walletsStorageRepository, super(const BackupSettingsState()); + final WalletsStorageRepository _walletsStorageRepository; final WalletSensitiveStorageRepository _walletSensRepository; final AppWalletsRepository _appWalletsRepository; - final Wallet _wallet; + final WalletSensitiveCreate _walletSensitiveCreate; + final BDKSensitiveCreate _bdkSensitiveCreate; + final WalletCreate _walletCreate; + final LWKSensitiveCreate _lwkSensitiveCreate; + Wallet? _currentWallet; final List _wallets; final FileSystemBackupManager _manager; final GoogleDriveBackupManager _driveManager; + final FilePick? _filePicker; static const _kDelayDuration = Duration(milliseconds: 800); static const _kShuffleDelay = Duration(milliseconds: 500); static const _kMinBackupInterval = Duration(seconds: 5); From a63020eec427d4a0b4ea08c3cf36cdb8fec91578 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Wed, 5 Feb 2025 19:51:59 -0500 Subject: [PATCH 124/401] feat(backup): implement cleanup logic in BackupSettingsCubit on close --- lib/wallet_settings/bloc/backup_settings_cubit.dart | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/wallet_settings/bloc/backup_settings_cubit.dart b/lib/wallet_settings/bloc/backup_settings_cubit.dart index 77dfc33e5..9daf2da2b 100644 --- a/lib/wallet_settings/bloc/backup_settings_cubit.dart +++ b/lib/wallet_settings/bloc/backup_settings_cubit.dart @@ -88,6 +88,12 @@ class BackupSettingsCubit extends Cubit { static const _kMinBackupInterval = Duration(seconds: 5); static const _kDerivationPath = "m/1608'/0'"; + @override + Future close() async { + await _driveManager.dispose(); + await super.close(); + } + // Seed loading helper Future<(Seed?, String?)> _loadWalletSeed(Wallet wallet) async { final (seed, err) = await _walletSensRepository.readSeed( From 71ea12154222a04cdee08e5aa51f187dc89b37cf Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Wed, 5 Feb 2025 19:53:33 -0500 Subject: [PATCH 125/401] refactor(backup): enhance physical backup verification logic --- .../bloc/backup_settings_cubit.dart | 98 ++++++++++++------- 1 file changed, 65 insertions(+), 33 deletions(-) diff --git a/lib/wallet_settings/bloc/backup_settings_cubit.dart b/lib/wallet_settings/bloc/backup_settings_cubit.dart index 9daf2da2b..8d8882fb9 100644 --- a/lib/wallet_settings/bloc/backup_settings_cubit.dart +++ b/lib/wallet_settings/bloc/backup_settings_cubit.dart @@ -102,30 +102,64 @@ class BackupSettingsCubit extends Cubit { return (seed, err?.toString()); } - // Backup verification methods - Future loadBackupForVerification() async { - final (seed, error) = await _loadWalletSeed(_wallet); - if (error != null || seed == null) { - emit(state.copyWith(errTestingBackup: error ?? 'Seed data not found')); + // physical backup & verification methods + + void _emitBackupState(Seed seed) { + if (_currentWallet == null) { + emit( + state.copyWith( + errorLoadingBackups: 'No active wallet selected', + loadingBackups: false, + ), + ); return; } - _emitBackupState(seed); - } - - void _emitBackupState(Seed seed) { final words = seed.mnemonic.split(' '); final shuffled = words.toList()..shuffle(); + + emit(state.copyWith( + testMnemonicOrder: [], + mnemonic: words, + errTestingBackup: '', + password: seed + .getPassphraseFromIndex(_currentWallet!.sourceFingerprint) + .passphrase, + shuffledMnemonic: shuffled, + loadingBackups: false, + )); + } + + void _emitBackupTestSuccessState() { emit( state.copyWith( - testMnemonicOrder: [], - mnemonic: words, - errTestingBackup: '', - password: - seed.getPassphraseFromIndex(_wallet.sourceFingerprint).passphrase, - shuffledMnemonic: shuffled, + backupTested: true, + testingBackup: false, ), ); + clearSensitive(); + } + + Future loadBackupForVerification() async { + if (_currentWallet == null) { + emit(state.copyWith( + errorLoadingBackups: 'No wallet selected for verification', + loadingBackups: false, + )); + return; + } + + emit(state.copyWith(loadingBackups: true)); + final (seed, error) = await _loadWalletSeed(_currentWallet!); + if (error != null || seed == null) { + emit(state.copyWith( + errTestingBackup: error ?? 'Seed data not found', + loadingBackups: false, + )); + return; + } + + _emitBackupState(seed); } Future testBackupClicked() async { @@ -133,7 +167,7 @@ class BackupSettingsCubit extends Cubit { final words = state.testMneString(); final password = state.testBackupPassword; - final seed = await _loadSeedData(_wallet); + final seed = await _loadSeedData(_currentWallet!); if (seed == null) { emit( @@ -172,9 +206,12 @@ class BackupSettingsCubit extends Cubit { bool _verifyWords(String seedMnemonic, String testWords) => seedMnemonic == testWords; - bool _verifyPassphrase(Seed seed, String password) => - seed.getPassphraseFromIndex(_wallet.sourceFingerprint).passphrase == - password; + bool _verifyPassphrase(Seed seed, String password) { + final storedPassphrase = seed + .getPassphraseFromIndex(_currentWallet!.sourceFingerprint) + .passphrase; + return storedPassphrase == password; + } Future _loadSeedData(Wallet wallet) async { final (seed, err) = await _walletSensRepository.readSeed( @@ -188,24 +225,19 @@ class BackupSettingsCubit extends Cubit { } Future _updateWalletBackupStatus() async { - final wallet = _wallet.copyWith( + final wallet = _currentWallet!.copyWith( physicalBackupTested: true, lastPhysicalBackupTested: DateTime.now(), ); - await _appWalletsRepository - .getWalletServiceById(wallet.id) - ?.updateWallet(wallet, updateTypes: [UpdateWalletTypes.settings]); - } - - void _emitBackupTestSuccessState() { - emit( - state.copyWith( - backupTested: true, - testingBackup: false, - ), - ); - clearSensitive(); + final service = _appWalletsRepository.getWalletServiceById(wallet.id); + if (service != null) { + await service.updateWallet( + wallet, + updateTypes: [UpdateWalletTypes.settings], + ); + _currentWallet = wallet; + } } void word24Clicked(int shuffledIdx) { From a6ee06cd70fd2dcdb49b05f6c132b44ca40f9cb3 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Wed, 5 Feb 2025 19:54:31 -0500 Subject: [PATCH 126/401] feat(backup): add error handling and backup interval checks --- .../bloc/backup_settings_cubit.dart | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/lib/wallet_settings/bloc/backup_settings_cubit.dart b/lib/wallet_settings/bloc/backup_settings_cubit.dart index 8d8882fb9..59223e6b4 100644 --- a/lib/wallet_settings/bloc/backup_settings_cubit.dart +++ b/lib/wallet_settings/bloc/backup_settings_cubit.dart @@ -338,6 +338,27 @@ class BackupSettingsCubit extends Cubit { ); } +// encrypted vault backup methods + void _emitBackupError(String message) { + emit( + state.copyWith( + savingBackups: false, + errorSavingBackups: message, + ), + ); + } + + bool _canStartBackup() { + final lastAttempt = state.lastBackupAttempt; + if (lastAttempt != null) { + final timeSinceLastBackup = DateTime.now().difference(lastAttempt); + if (timeSinceLastBackup < _kMinBackupInterval) { + return false; + } + } + return true; + } + Future saveEncryptedBackup() async { if (!_canStartBackup()) { emit( @@ -386,6 +407,22 @@ class BackupSettingsCubit extends Cubit { return; } + // Connect if needed + if (state.backupFolderId.isEmpty) { + final (folderId, err) = await _driveManager.connect(); + if (err != null) { + _emitBackupError('Failed to connect to Google Drive: ${err.message}'); + return; + } + emit(state.copyWith(backupFolderId: folderId ?? '')); + } + + // Ensure we have a folder ID + if (state.backupFolderId.isEmpty) { + _emitBackupError('Failed to initialize Google Drive backup folder'); + return; + } + final (encryptedData, err) = await _encryptBackups(backups); if (err != null || encryptedData == null) return; @@ -527,6 +564,9 @@ class BackupSettingsCubit extends Cubit { name: wallet.name ?? '', network: wallet.network.name, mnemonicFingerPrint: wallet.mnemonicFingerprint, + layer: wallet.baseWalletType.name, + script: wallet.scriptType.name, + type: wallet.type.name, ); if (!wallet.hasPassphrase()) { From 133792cb64139cc508eafab3c858044f258e59af Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Wed, 5 Feb 2025 19:55:32 -0500 Subject: [PATCH 127/401] feat(backup): implement backup recovery and loading logic --- .../bloc/backup_settings_cubit.dart | 403 +++++++++++++++++- 1 file changed, 381 insertions(+), 22 deletions(-) diff --git a/lib/wallet_settings/bloc/backup_settings_cubit.dart b/lib/wallet_settings/bloc/backup_settings_cubit.dart index 59223e6b4..f6741d6a9 100644 --- a/lib/wallet_settings/bloc/backup_settings_cubit.dart +++ b/lib/wallet_settings/bloc/backup_settings_cubit.dart @@ -383,7 +383,6 @@ class BackupSettingsCubit extends Cubit { if (err != null || encryptedData == null) { return; } - await _saveToFileSystem(encryptedData); } @@ -396,11 +395,6 @@ class BackupSettingsCubit extends Cubit { emit(state.copyWith(savingBackups: true, errorSavingBackups: '')); try { - if (state.backupFolderId.isEmpty) { - await connectToGoogleDrive(); - if (state.backupFolderId.isEmpty) return; - } - final backups = await _createBackupsForAllWallets(); if (backups.isEmpty) { _emitBackupError('No wallets available for backup'); @@ -594,20 +588,137 @@ class BackupSettingsCubit extends Cubit { } } - void _emitBackupError(String message) { - emit( - state.copyWith( - savingBackups: false, - errorSavingBackups: message, - ), - ); - } - void disconnectGoogleDrive() { _driveManager.disconnect(); emit(state.copyWith(backupFolderId: '')); } +// encrypted vault backup methods + + Future fetchLatestBacup({bool forceRefresh = false}) async { + try { + if (!forceRefresh && state.loadedBackups.isNotEmpty) { + emit(state.copyWith(loadingBackups: false)); + return; + } + + emit(state.copyWith(loadingBackups: true)); + + // Connect if needed + if (state.backupFolderId.isEmpty) { + final (folderId, err) = await _driveManager.connect(); + if (err != null) { + emit(state.copyWith( + loadingBackups: false, + errorLoadingBackups: err.message, + )); + return; + } + emit(state.copyWith(backupFolderId: folderId ?? '')); + } + + // Ensure we have a folder ID + if (state.backupFolderId.isEmpty) { + emit(state.copyWith( + loadingBackups: false, + errorLoadingBackups: "Failed to initialize Google Drive folder", + )); + return; + } + + // Rest of the existing code... + final (availableBackups, err) = + await _driveManager.loadAllEncryptedBackupFiles( + backupFolder: state.backupFolderId, + ); + + if (err != null) { + debugPrint('Error loading backups: ${err.message}'); + emit( + state.copyWith( + loadingBackups: false, + errorLoadingBackups: "Failed to get backup files", + ), + ); + return; + } + + if (availableBackups != null && availableBackups.isNotEmpty) { + final latestBackup = availableBackups.reduce((a, b) { + final aTime = a.createdTime; + final bTime = b.createdTime; + if (aTime == null) return b; + if (bTime == null) return a; + return aTime.compareTo(bTime) > 0 ? a : b; + }); + final backupId = latestBackup.name?.split('_').last.split('.').first; + if (backupId == null) { + emit( + state.copyWith( + loadingBackups: false, + errorLoadingBackups: "Corrupted backup file", + ), + ); + return; + } + final (loadedBackupMetaData, mediaErr) = + await _driveManager.fetchMediaStream( + file: latestBackup, + ); + + if (mediaErr != null || loadedBackupMetaData == null) { + emit( + state.copyWith( + loadingBackups: false, + errorLoadingBackups: "Failed to load backup data", + ), + ); + return; + } + + final (loadedBackup, err) = await _driveManager.loadEncryptedBackup( + encrypted: utf8.decode(loadedBackupMetaData), + ); + if (loadedBackup != null) { + emit( + state.copyWith( + loadingBackups: false, + latestRecoveredBackup: loadedBackup, + lastBackupAttempt: DateTime.now(), + ), + ); + return; + } else if ((err != null) || loadedBackup?["id"] == null) { + debugPrint('Error loading backups: ${err?.message}'); + emit( + state.copyWith( + loadingBackups: false, + errorLoadingBackups: "Corrupted backup file", + ), + ); + return; + } + } else { + emit( + state.copyWith( + loadingBackups: false, + errorLoadingBackups: "Failed to get backup files", + ), + ); + } + } catch (e) { + debugPrint('Error loading backups: $e'); + emit( + state.copyWith( + loadingBackups: false, + errorLoadingBackups: "Failed to get backup files", + ), + ); + } + } + + Future refreshBackups() => fetchLatestBacup(forceRefresh: true); + void clearError() => emit( state.copyWith( errTestingBackup: '', @@ -616,14 +727,262 @@ class BackupSettingsCubit extends Cubit { ), ); - bool _canStartBackup() { - final lastAttempt = state.lastBackupAttempt; - if (lastAttempt != null) { - final timeSinceLastBackup = DateTime.now().difference(lastAttempt); - if (timeSinceLastBackup < _kMinBackupInterval) { - return false; + Future recoverFromFs() async { + if (_filePicker == null) { + return; + } + final (file, error) = await _filePicker.pickFile(); + + if (error != null) { + emit(state.copyWith(errorLoadingBackups: "Error picking file")); + return; + } + + if (file == null || file.isEmpty) { + emit(state.copyWith(errorLoadingBackups: 'Corrupted backup file')); + return; + } + final (loadedBackup, err) = await _manager.loadEncryptedBackup( + encrypted: file, + ); + if (loadedBackup != null) { + final id = loadedBackup['id'] as String; + + debugPrint('Loaded backup: $id'); + emit( + state.copyWith( + loadingBackups: false, + latestRecoveredBackup: loadedBackup, + lastBackupAttempt: DateTime.now(), + ), + ); + return; + } else if ((err != null) || loadedBackup?["id"] == null) { + debugPrint('Error loading backups: ${err?.message}'); + emit( + state.copyWith( + loadingBackups: false, + errorLoadingBackups: "Corrupted backup file", + ), + ); + return; + } + } + + Future recoverBackup(String encrypted, String backupKey) async { + emit( + state.copyWith( + loadingBackups: true, + backupKey: backupKey, + errorLoadingBackups: '', + ), + ); + + if (state.backupKey.isEmpty) { + emit( + state.copyWith( + loadingBackups: false, + errorLoadingBackups: 'Backup key is missing', + ), + ); + return; + } + + try { + final decodeEncryptedFile = jsonDecode(encrypted) as Map; + final id = decodeEncryptedFile['id']?.toString() ?? ''; + + if (id.isEmpty) { + emit( + state.copyWith( + loadingBackups: false, + errorLoadingBackups: 'Invalid backup format', + ), + ); + return; } + + // Then decrypt the backup data + final (backups, err) = await _manager.decryptBackups( + encrypted: encrypted, + backupKey: state.backupKey, + ); + + if (err != null || backups == null || backups.isEmpty) { + debugPrint('Error loading backups: ${err?.message}'); + emit( + state.copyWith( + loadingBackups: false, + errorLoadingBackups: err?.message ?? 'No wallets found in backup', + ), + ); + return; + } + + // Process each backup + for (final backup in backups) { + await _processBackupRecovery(backup); + if (state.errorLoadingBackups.isNotEmpty) { + return; + } + } + + emit( + state.copyWith( + loadingBackups: false, + loadedBackups: backups, + errorLoadingBackups: '', + ), + ); + } catch (e) { + debugPrint('Recovery error: $e'); + emit( + state.copyWith( + loadingBackups: false, + errorLoadingBackups: 'Recovery failed: $e', + ), + ); + } + } + + Future _processBackupRecovery(Backup backup) async { + final network = _getNetwork(backup.network); + final layer = _getLayer(backup.layer); + final script = _getScript(backup.script); + final type = _getWalletType(backup.type); + + if (network == null || layer == null || script == null || type == null) { + emit( + state.copyWith( + errorLoadingBackups: + 'Invalid backup configuration for ${backup.network}', + loadingBackups: false, + ), + ); + return; + } + + await _addOrUpdateWallet( + network, + layer, + script, + type, + backup.mnemonic.join(' '), + backup.passphrase, + ); + } + + BBNetwork? _getNetwork(String network) => switch (network.toLowerCase()) { + 'mainnet' => BBNetwork.Mainnet, + 'testnet' => BBNetwork.Testnet, + _ => null + }; + + BaseWalletType? _getLayer(String layer) => switch (layer.toLowerCase()) { + 'bitcoin' => BaseWalletType.Bitcoin, + 'liquid' => BaseWalletType.Liquid, + _ => null + }; + + ScriptType? _getScript(String script) => switch (script.toLowerCase()) { + 'bip44' => ScriptType.bip44, + 'bip49' => ScriptType.bip49, + 'bip84' => ScriptType.bip84, + _ => null + }; + + BBWalletType? _getWalletType(String type) => switch (type.toLowerCase()) { + 'main' => BBWalletType.main, + 'xpub' => BBWalletType.xpub, + 'words' => BBWalletType.words, + 'descriptors' => BBWalletType.descriptors, + 'coldcard' => BBWalletType.coldcard, + _ => null + }; + + Future _addOrUpdateWallet( + BBNetwork network, + BaseWalletType layer, + ScriptType script, + BBWalletType type, + String mnemonic, + String passphrase, + ) async { + final (seed, error) = + await _walletSensitiveCreate.mnemonicSeed(mnemonic, network); + if (seed == null) { + emit( + state.copyWith( + errorLoadingBackups: 'Failed to create seed', + loadingBackups: false, + ), + ); + return; + } + + try { + await _walletSensRepository.newSeed(seed: seed); + + final wallet = await _createWalletFromSeed( + layer, + seed, + passphrase, + script, + network, + type, + ); + + if (wallet == null) { + emit( + state.copyWith( + errorLoadingBackups: 'Failed to create wallet', + loadingBackups: false, + ), + ); + return; + } + + await _walletsStorageRepository.newWallet(wallet); + } catch (e) { + debugPrint('Wallet creation error: $e'); + emit( + state.copyWith( + errorLoadingBackups: 'Failed to save wallet: $e', + loadingBackups: false, + ), + ); + } + } + + Future _createWalletFromSeed( + BaseWalletType layer, + Seed seed, + String passphrase, + ScriptType script, + BBNetwork network, + BBWalletType type, + ) async { + switch (layer) { + case BaseWalletType.Bitcoin: + final (wallet, error) = await _bdkSensitiveCreate.oneFromBIP39( + seed: seed, + passphrase: passphrase, + scriptType: script, + network: network, + walletType: type, + walletCreate: _walletCreate, + ); + return wallet; + case BaseWalletType.Liquid: + final (wallet, error) = await _lwkSensitiveCreate.oneLiquidFromBIP39( + seed: seed, + passphrase: passphrase, + scriptType: script, + network: network, + walletType: type, + walletCreate: _walletCreate, + ); + return wallet; } - return true; } } From 0a8a9fa6930891eea4d3369e68cfdded59ea4795 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Wed, 5 Feb 2025 19:55:50 -0500 Subject: [PATCH 128/401] feat(backup): update BackupSettingsCubit to accept wallet ID --- lib/wallet_settings/backup_settings.dart | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/lib/wallet_settings/backup_settings.dart b/lib/wallet_settings/backup_settings.dart index 3a610d4f1..3ccab4736 100644 --- a/lib/wallet_settings/backup_settings.dart +++ b/lib/wallet_settings/backup_settings.dart @@ -31,7 +31,7 @@ class _BackupSettingsState extends State { ), BlocProvider( create: (BuildContext context) => - createBackupSettingsCubit(widget.wallet), + createBackupSettingsCubit(walletId: widget.wallet), ), ], child: Scaffold( @@ -126,7 +126,7 @@ class _Screen extends StatelessWidget { label: "Recover or test backup", onPressed: () { context.push( - '/wallet-settings/backup-settings/recover-options', + '/wallet-settings/backup-settings/recover-encrypted', extra: context.read().state.wallet.id, ); }, @@ -139,15 +139,6 @@ class _Screen extends StatelessWidget { } } -class RecoverOptionsScreen extends StatelessWidget { - const RecoverOptionsScreen({super.key, required this.wallet}); - final String wallet; - @override - Widget build(BuildContext context) { - return const Placeholder(); - } -} - class BackupOptionsScreen extends StatelessWidget { const BackupOptionsScreen({super.key, required this.wallet}); final String wallet; From be983077e884026a1310b3234084b5522f98b443 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Wed, 5 Feb 2025 19:56:13 -0500 Subject: [PATCH 129/401] feat(backup): add recovery functionality and UI for encrypted vault backups --- .../encrypted_vault_backup.dart | 318 +++++++++++++++++- 1 file changed, 316 insertions(+), 2 deletions(-) diff --git a/lib/wallet_settings/encrypted_vault_backup.dart b/lib/wallet_settings/encrypted_vault_backup.dart index 27d2893af..edc345db2 100644 --- a/lib/wallet_settings/encrypted_vault_backup.dart +++ b/lib/wallet_settings/encrypted_vault_backup.dart @@ -12,6 +12,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:gap/gap.dart'; import 'package:go_router/go_router.dart'; +import 'package:intl/intl.dart'; const double _kSpacing = 15.0; @@ -42,7 +43,7 @@ class _EncryptedVaultBackupPageState extends State { @override void initState() { super.initState(); - _cubit = createBackupSettingsCubit(widget.wallet); + _cubit = createBackupSettingsCubit(walletId: widget.wallet); } @override @@ -95,7 +96,10 @@ class _EncryptedVaultBackupPageState extends State { state.errorSavingBackups.isEmpty) { context.push( '/wallet-settings/backup-settings/keychain', - extra: (state.backupId, (state.backupKey, state.backupSalt)), + extra: ( + state.backupKey, + {'id': state.backupId, 'salt': state.backupSalt} + ), ); _cubit.clearError(); } @@ -261,3 +265,313 @@ class _StorageOptionCard extends StatelessWidget { ); } } + +class EncryptedVaultRecoverPage extends StatefulWidget { + const EncryptedVaultRecoverPage({super.key, required this.wallet}); + final String wallet; + + @override + State createState() => + _EncryptedVaultRecoverPageState(); +} + +class _EncryptedVaultRecoverPageState extends State { + late final BackupSettingsCubit _cubit; + + @override + void initState() { + super.initState(); + _cubit = createBackupSettingsCubit(walletId: widget.wallet); + } + + @override + void dispose() { + _cubit.close(); + super.dispose(); + } + + Future _handleRecover( + BuildContext context, + BackupProvider provider, + ) async { + switch (provider) { + case BackupProvider.googleDrive: + await _cubit.fetchLatestBacup(); + break; + case BackupProvider.iCloud: + debugPrint('iCloud backup'); + break; + case BackupProvider.custom: + _cubit.recoverFromFs(); + break; + } + } + + Widget _buildContent(BuildContext context, BackupSettingsState state) { + return SingleChildScrollView( + padding: const EdgeInsets.all(20), + child: Column( + children: [ + const BBText.titleLarge( + 'Where is your backup?', + isBold: true, + ), + const Gap(20), + ...BackupProvider.values.map( + (provider) => Padding( + padding: const EdgeInsets.only(bottom: 10), + child: _StorageOptionCard( + title: provider.title, + description: provider.description, + icon: Icon(provider.icon, size: 40), + onTap: () => _handleRecover(context, provider), + ), + ), + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + return BlocProvider.value( + value: _cubit, + child: BlocConsumer( + listenWhen: (previous, current) => + previous.errorLoadingBackups != current.errorLoadingBackups || + previous.latestRecoveredBackup != current.latestRecoveredBackup, + listener: (context, state) { + if (state.errorLoadingBackups.isNotEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + context.showToast(state.errorLoadingBackups), + ); + _cubit.clearError(); + return; + } + if (state.latestRecoveredBackup.isNotEmpty) { + context.push( + '/wallet-settings/backup-settings/recover-encrypted/info', + extra: state.latestRecoveredBackup, + ); + _cubit.clearError(); + } + }, + builder: (context, state) { + return Scaffold( + appBar: AppBar( + elevation: 0, + automaticallyImplyLeading: false, + centerTitle: true, + flexibleSpace: BBAppBar( + text: '', + onBack: () => context.pop(), + ), + ), + body: state.loadingBackups + ? const Center( + child: CircularProgressIndicator(), + ) + : _buildContent(context, state), + ); + }, + ), + ); + } +} + +class RecoveredBackupInfoPage extends StatelessWidget { + const RecoveredBackupInfoPage({ + super.key, + required this.recoveredBackup, + }); + + final Map recoveredBackup; + + Widget _buildErrorView(BuildContext context) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'ERROR', + style: context.font.titleLarge!.copyWith( + fontWeight: FontWeight.w900, + ), + ), + const Gap(16), + const BBText.title( + 'This is not a backup file', + isBold: true, + ), + const Gap(24), + FilledButton( + onPressed: () => context.pop(), + style: FilledButton.styleFrom( + backgroundColor: context.colour.shadow, + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 16, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Try again', + style: context.font.bodyMedium!.copyWith( + color: Colors.white, + fontWeight: FontWeight.w900, + ), + ), + const Gap(8), + const Icon( + Icons.arrow_forward, + color: Colors.white, + size: 20, + ), + ], + ), + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + if (recoveredBackup.isEmpty) { + return Scaffold( + appBar: AppBar( + elevation: 0, + automaticallyImplyLeading: false, + centerTitle: true, + flexibleSpace: BBAppBar(text: '', onBack: () => context.pop()), + ), + body: _buildErrorView(context), + ); + } else if (recoveredBackup['id'] == null || + recoveredBackup['createdAt'] == null) { + return Scaffold( + appBar: AppBar( + elevation: 0, + automaticallyImplyLeading: false, + centerTitle: true, + flexibleSpace: BBAppBar(text: '', onBack: () => context.pop()), + ), + body: _buildErrorView(context), + ); + } + + return Scaffold( + appBar: AppBar( + elevation: 0, + automaticallyImplyLeading: false, + centerTitle: true, + flexibleSpace: BBAppBar(text: '', onBack: () => context.pop()), + ), + body: Padding( + padding: const EdgeInsets.symmetric(horizontal: 30), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'We have your file', + style: context.font.titleLarge!.copyWith( + fontWeight: FontWeight.w900, + ), + ), + const Gap(20), + RichText( + textAlign: TextAlign.center, + text: TextSpan( + children: [ + TextSpan( + text: 'Backup ID:', + style: context.font.bodyMedium!.copyWith( + fontWeight: FontWeight.bold, + ), + ), + TextSpan( + text: '${recoveredBackup['id']}', + style: context.font.bodyMedium!.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + const Gap(8), + RichText( + textAlign: TextAlign.center, + text: TextSpan( + children: [ + TextSpan( + text: 'Created at:', + style: context.font.bodyMedium!.copyWith( + fontWeight: FontWeight.bold, + ), + ), + TextSpan( + text: + ' ${DateFormat('MMM dd, yyyy HH:mm:ss').format(DateTime.fromMillisecondsSinceEpoch(recoveredBackup['createdAt'] as int).toLocal())}', + style: context.font.bodyMedium!.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + const Gap(16), + Text( + "Now let's decrypt", + textAlign: TextAlign.center, + style: context.font.bodyMedium!.copyWith( + fontWeight: FontWeight.bold, + color: NewColours.lightGray, + ), + ), + const Gap(20), + FilledButton( + onPressed: () => context.push( + '/wallet-settings/backup-settings/keychain', + extra: ('', recoveredBackup), + ), + style: FilledButton.styleFrom( + backgroundColor: context.colour.shadow, + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 16, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Decrypt Backup', + style: context.font.bodyMedium!.copyWith( + color: Colors.white, + fontWeight: FontWeight.w900, + ), + ), + const Gap(8), + const Icon( + Icons.arrow_forward, + color: Colors.white, + size: 20, + ), + ], + ), + ), + ], + ), + ), + ); + } +} From ebdb5b3bb37158431a6e9399c32d1c4c9baa4bd9 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Wed, 5 Feb 2025 19:56:45 -0500 Subject: [PATCH 130/401] code cleanup --- lib/wallet_settings/physical_backup.dart | 2 +- lib/wallet_settings/test_backup.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/wallet_settings/physical_backup.dart b/lib/wallet_settings/physical_backup.dart index 0b4fc0b5b..506fbd74c 100644 --- a/lib/wallet_settings/physical_backup.dart +++ b/lib/wallet_settings/physical_backup.dart @@ -35,7 +35,7 @@ class _PhysicalBackupPageState extends State { @override void initState() { walletBloc = createOrRetreiveWalletBloc(widget.wallet); - backupSettings = createBackupSettingsCubit(widget.wallet); + backupSettings = createBackupSettingsCubit(walletId: widget.wallet); backupSettings.loadBackupForVerification(); diff --git a/lib/wallet_settings/test_backup.dart b/lib/wallet_settings/test_backup.dart index bfe0dd75b..9598fdd57 100644 --- a/lib/wallet_settings/test_backup.dart +++ b/lib/wallet_settings/test_backup.dart @@ -32,7 +32,7 @@ class _TestBackupPageState extends State { @override void initState() { walletBloc = createOrRetreiveWalletBloc(widget.wallet); - backupSettings = createBackupSettingsCubit(widget.wallet); + backupSettings = createBackupSettingsCubit(walletId: widget.wallet); backupSettings.loadBackupForVerification(); From 3f8a5cde0a7a263738c0e022d0f838dcf3979d35 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Wed, 5 Feb 2025 19:57:24 -0500 Subject: [PATCH 131/401] feat(ui): add success and error dialogs for backup and recovery processes --- lib/wallet_settings/keychain_page.dart | 101 +++++++++++++++++++++++++ 1 file changed, 101 insertions(+) diff --git a/lib/wallet_settings/keychain_page.dart b/lib/wallet_settings/keychain_page.dart index 5fa9a8c31..300ca6871 100644 --- a/lib/wallet_settings/keychain_page.dart +++ b/lib/wallet_settings/keychain_page.dart @@ -534,3 +534,104 @@ class KeyPad extends StatelessWidget { ); } } + +class _SuccessDialog extends StatelessWidget { + const _SuccessDialog({required this.isRecovery}); + final bool isRecovery; + + @override + Widget build(BuildContext context) { + Widget dialogContent = Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.check_circle_outline, + color: context.colour.shadow, + size: 48, + ), + const Gap(16), + BBText.title( + isRecovery ? 'Recovery Successful' : 'Backup Successful', + textAlign: TextAlign.center, + isBold: true, + ), + const Gap(8), + BBText.bodySmall( + isRecovery + ? 'Your wallet has been recovered successfully' + : 'Your wallet has been backed up successfully', + textAlign: TextAlign.center, + ), + const Gap(24), + FilledButton( + onPressed: () { + Navigator.of(context).pop(); + context.go('/home'); + }, + style: FilledButton.styleFrom( + backgroundColor: context.colour.shadow, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: const Text('Continue'), + ), + ], + ), + ); + + return Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + child: dialogContent, + ); + } +} + +class _ErrorDialog extends StatelessWidget { + const _ErrorDialog({required this.error}); + final String error; + + @override + Widget build(BuildContext context) { + return Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.error_outline, + color: context.colour.error, + size: 48, + ), + const Gap(16), + BBText.title( + 'Recovery Failed', + textAlign: TextAlign.center, + isBold: true, + ), + const Gap(8), + BBText.bodySmall( + error, + textAlign: TextAlign.center, + ), + const Gap(24), + FilledButton( + onPressed: () => Navigator.of(context).pop(), + style: FilledButton.styleFrom( + backgroundColor: context.colour.shadow, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: const Text('Close'), + ), + ], + ), + ), + ); + } +} From 025e17cc59863785228ce530c17b7bcfee4787f5 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Wed, 5 Feb 2025 19:58:32 -0500 Subject: [PATCH 132/401] feat(backup): add recovery page and button for keychain recovery --- lib/wallet_settings/keychain_page.dart | 108 +++++++++++++++++++++++++ 1 file changed, 108 insertions(+) diff --git a/lib/wallet_settings/keychain_page.dart b/lib/wallet_settings/keychain_page.dart index 300ca6871..c74c0947c 100644 --- a/lib/wallet_settings/keychain_page.dart +++ b/lib/wallet_settings/keychain_page.dart @@ -199,6 +199,45 @@ class _ConfirmPage extends StatelessWidget { } } +class _RecoveryPage extends StatelessWidget { + const _RecoveryPage({super.key, required this.inputType}); + final KeyChainInputType inputType; + + @override + Widget build(BuildContext context) { + return StackedPage( + bottomChildHeight: MediaQuery.of(context).size.height * 0.1, + bottomChild: _RecoverButton(inputType: inputType), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Gap(50), + BBText.titleLarge( + 'Enter Recovery ${inputType == KeyChainInputType.pin ? 'PIN' : 'Password'}', + textAlign: TextAlign.center, + isBold: true, + ), + const Gap(8), + BBText.bodySmall( + 'Enter the ${inputType == KeyChainInputType.pin ? 'PIN' : 'password'} you used to backup your keychain', + textAlign: TextAlign.center, + ), + const Gap(50), + if (inputType == KeyChainInputType.pin) ...[ + _PinField(), + const KeyPad(), + ] else + _PasswordField(), + const Gap(30), + ], + ), + ), + ); + } +} + class _PinField extends StatelessWidget { const _PinField(); @@ -435,6 +474,75 @@ class _ConfirmButton extends StatelessWidget { } } +class _RecoverButton extends StatelessWidget { + const _RecoverButton({required this.inputType}); + final KeyChainInputType inputType; + + @override + Widget build(BuildContext context) { + final canRecover = context.select((KeychainCubit x) => x.state.canRecover); + return AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: Column( + children: [ + InkWell( + onTap: () { + final cubit = context.read(); + if (inputType == KeyChainInputType.pin) { + cubit.updatePageState( + KeyChainInputType.password, + KeyChainPageState.recovery, + ); + } else { + cubit.updatePageState( + KeyChainInputType.pin, + KeyChainPageState.recovery, + ); + } + }, + child: BBText.bodySmall( + inputType == KeyChainInputType.pin + ? 'Use a password instead of a pin' + : 'Use a PIN instead of a password', + ), + ), + const Gap(5), + FilledButton( + onPressed: () { + if (canRecover) context.read().clickRecoverKey(); + }, + style: FilledButton.styleFrom( + backgroundColor: + canRecover ? context.colour.shadow : context.colour.surface, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Recover with ${inputType == KeyChainInputType.pin ? 'PIN' : 'password'}', + style: context.font.bodyMedium!.copyWith( + color: Colors.white, + fontWeight: FontWeight.w900, + ), + ), + const SizedBox(width: 8), + const Icon( + Icons.arrow_forward, + color: Colors.white, + size: 16, + ), + ], + ), + ), + ], + ), + ); + } +} + class NumberButton extends StatefulWidget { const NumberButton({super.key, required this.text}); From 0715d94154023ed74fc8f280507ecca514a2bb0a Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Wed, 5 Feb 2025 19:58:53 -0500 Subject: [PATCH 133/401] fix(keychain): correct state selection for showButton --- lib/wallet_settings/keychain_page.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/wallet_settings/keychain_page.dart b/lib/wallet_settings/keychain_page.dart index c74c0947c..c0abe13b5 100644 --- a/lib/wallet_settings/keychain_page.dart +++ b/lib/wallet_settings/keychain_page.dart @@ -430,8 +430,7 @@ class _ConfirmButton extends StatelessWidget { final KeyChainInputType inputType; @override Widget build(BuildContext context) { - final showButton = - context.select((KeychainCubit x) => x.state.showButton()); + final showButton = context.select((KeychainCubit x) => x.state.showButton); final err = context.select((KeychainCubit x) => x.state.error); if (err.isNotEmpty && inputType == KeyChainInputType.password) { From c18b7d6ec8c690b4b8662c5f01d2fca12f6c4297 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Wed, 5 Feb 2025 19:59:51 -0500 Subject: [PATCH 134/401] refactor(keychain): simplify input field handling --- lib/wallet_settings/keychain_page.dart | 37 ++++++++++++-------------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/lib/wallet_settings/keychain_page.dart b/lib/wallet_settings/keychain_page.dart index c0abe13b5..7d160a683 100644 --- a/lib/wallet_settings/keychain_page.dart +++ b/lib/wallet_settings/keychain_page.dart @@ -152,10 +152,10 @@ class _EnterPage extends StatelessWidget { const _SubtitleText(), const Gap(50), if (inputType == KeyChainInputType.pin) ...[ - const _PinField(), + _PinField(), const KeyPad(), ] else - const _PasswordField(), + _PasswordField(), const Gap(30), ], ), @@ -186,10 +186,10 @@ class _ConfirmPage extends StatelessWidget { const _ConfirmSubtitleText(), const Gap(48), if (inputType == KeyChainInputType.pin) ...[ - const _PinField(), + _PinField(), const KeyPad(), ] else - const _PasswordField(), + _PasswordField(), const Gap(24), ], ), @@ -239,8 +239,6 @@ class _RecoveryPage extends StatelessWidget { } class _PinField extends StatelessWidget { - const _PinField(); - @override Widget build(BuildContext context) { final pin = context.select((KeychainCubit x) => x.state.displayPin()); @@ -281,17 +279,12 @@ class _PinField extends StatelessWidget { } class _PasswordField extends StatelessWidget { - const _PasswordField(); - @override Widget build(BuildContext context) { - final password = context.select((KeychainCubit x) => x.state.password); - // final err = context.select((KeychainCubit x) => x.state.err); + final secret = context.select((KeychainCubit x) => x.state.secret); return BBTextInput.big( - value: password, - onChanged: (value) { - context.read().passwordChanged(value); - }, + value: secret, + onChanged: (value) => context.read().updateInput(value), hint: 'Enter your password', ); } @@ -362,13 +355,11 @@ class _SubtitleText extends StatelessWidget { } class _SetButton extends StatelessWidget { - const _SetButton({required this.inputType}); final KeyChainInputType inputType; + const _SetButton({required this.inputType}); @override Widget build(BuildContext context) { - final showButton = - context.select((KeychainCubit x) => x.state.showButton()); - + final showButton = context.select((KeychainCubit x) => x.state.showButton); return AnimatedSwitcher( duration: const Duration(milliseconds: 300), child: Column( @@ -377,9 +368,15 @@ class _SetButton extends StatelessWidget { onTap: () { final cubit = context.read(); if (inputType == KeyChainInputType.pin) { - cubit.changeInputType(KeyChainInputType.password); + cubit.updatePageState( + KeyChainInputType.password, + KeyChainPageState.enter, + ); } else { - cubit.changeInputType(KeyChainInputType.pin); + cubit.updatePageState( + KeyChainInputType.pin, + KeyChainPageState.enter, + ); } }, child: BBText.bodySmall( From 811e4b7b97d8a370e1ae2a2fe0fdc30083947d91 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Wed, 5 Feb 2025 20:00:43 -0500 Subject: [PATCH 135/401] refactor(keychain): update loading state --- lib/wallet_settings/keychain_page.dart | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/lib/wallet_settings/keychain_page.dart b/lib/wallet_settings/keychain_page.dart index 7d160a683..2f0c31677 100644 --- a/lib/wallet_settings/keychain_page.dart +++ b/lib/wallet_settings/keychain_page.dart @@ -83,23 +83,31 @@ class _Screen extends StatelessWidget { ], child: BlocBuilder( builder: (context, state) { - if (state.saving) return const _LoadingView(); + if (state.loading) { + return const _LoadingView(); + } return Scaffold( appBar: AppBar( automaticallyImplyLeading: false, elevation: 0, flexibleSpace: BBAppBar( - text: 'Keychain Backup', + text: + state.isRecovering ? 'Recover Keychain' : 'Keychain Backup', onBack: () { - //TODO; clear sensitive data + context.read().clearSensitive(); context.pop(); }, ), ), body: AnimatedSwitcher( duration: 300.ms, - child: state.pageState == KeyChainPageState.enter + child: state.pageState == KeyChainPageState.recovery + ? _RecoveryPage( + key: const ValueKey('recovery'), + inputType: state.inputType, + ) + : state.pageState == KeyChainPageState.enter ? _EnterPage( key: const ValueKey('enter'), inputType: state.inputType, @@ -123,9 +131,7 @@ class _LoadingView extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( body: Center( - child: CircularProgressIndicator( - color: context.colour.onSurface, - ), + child: CircularProgressIndicator(color: context.colour.primary), ), ); } From dba0fba230c152b64c2dc8c7efded3f616680e0f Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Wed, 5 Feb 2025 20:01:59 -0500 Subject: [PATCH 136/401] feat(keychain): enhance backup state handling --- lib/wallet_settings/keychain_page.dart | 51 +++++++++++++++++++++++--- 1 file changed, 46 insertions(+), 5 deletions(-) diff --git a/lib/wallet_settings/keychain_page.dart b/lib/wallet_settings/keychain_page.dart index 2f0c31677..21072052b 100644 --- a/lib/wallet_settings/keychain_page.dart +++ b/lib/wallet_settings/keychain_page.dart @@ -1,9 +1,13 @@ +import 'dart:convert'; + import 'package:bb_mobile/_ui/app_bar.dart'; import 'package:bb_mobile/_ui/components/text.dart'; import 'package:bb_mobile/_ui/components/text_input.dart'; import 'package:bb_mobile/_ui/page_template.dart'; import 'package:bb_mobile/_ui/toast.dart'; import 'package:bb_mobile/styles.dart'; +import 'package:bb_mobile/wallet_settings/bloc/backup_settings_cubit.dart'; +import 'package:bb_mobile/wallet_settings/bloc/backup_settings_state.dart'; import 'package:bb_mobile/wallet_settings/bloc/keychain_cubit.dart'; import 'package:bb_mobile/wallet_settings/bloc/keychain_state.dart'; import 'package:flutter/material.dart'; @@ -48,16 +52,53 @@ class _Screen extends StatelessWidget { final String backupKey; final String backupId; + final String? backupKey; + final Map encryptedBackup; @override Widget build(BuildContext context) { return MultiBlocListener( listeners: [ - BlocListener( - listenWhen: (previous, current) => previous.saved != current.saved, + BlocListener( + listenWhen: (previous, current) => + previous.errorLoadingBackups != current.errorLoadingBackups || + previous.loadingBackups != current.loadingBackups || + previous.loadedBackups != current.loadedBackups, listener: (context, state) { - if (state.saved) { - context.read().clearSensitive(); - context.go('/home'); + // Always close loading dialog first if it's open + if (state.loadingBackups == false) { + Navigator.of(context).pop(); // Close loading dialog + } + + // Handle errors + if (state.errorLoadingBackups.isNotEmpty) { + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => _ErrorDialog( + error: state.errorLoadingBackups, + ), + ); + return; + } + + if (state.loadingBackups) { + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => const _LoadingView(), + ); + return; + } + + // Only show success if we have loaded backups and no errors + if (!state.loadingBackups && + state.loadedBackups.isNotEmpty && + state.errorLoadingBackups.isEmpty) { + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => const _SuccessDialog(isRecovery: true), + ); } }, ), From cdde7f3836aa92b151aabb8d3b91e2e240cbd428 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Wed, 5 Feb 2025 20:03:03 -0500 Subject: [PATCH 137/401] refactor(keychain): update backup handling and state management i --- lib/wallet_settings/keychain_page.dart | 61 +++++++++++++++++--------- 1 file changed, 40 insertions(+), 21 deletions(-) diff --git a/lib/wallet_settings/keychain_page.dart b/lib/wallet_settings/keychain_page.dart index 21072052b..6316d3402 100644 --- a/lib/wallet_settings/keychain_page.dart +++ b/lib/wallet_settings/keychain_page.dart @@ -21,22 +21,35 @@ import 'package:go_router/go_router.dart'; class KeychainBackupPage extends StatelessWidget { const KeychainBackupPage({ super.key, - required this.backupKey, - required this.backupId, - required this.backupSalt, + this.backupKey, + required this.backup, }); - final String backupSalt; - final String backupKey; - final String backupId; + final String? backupKey; + final Map backup; @override Widget build(BuildContext context) { - return BlocProvider( - create: (context) => KeychainCubit(), + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => KeychainCubit() + ..setChainState( + (backupKey == null || backupKey!.isEmpty) + ? KeyChainPageState.recovery + : KeyChainPageState.enter, + backup["id"] as String, + backupKey, + backup["salt"] as String, + ), + ), + BlocProvider.value( + value: createBackupSettingsCubit(), + ), + ], child: _Screen( backupId: backupId, backupKey: backupKey, - backupSalt: backupSalt, + encryptedBackup: backup, ), ); } @@ -44,13 +57,9 @@ class KeychainBackupPage extends StatelessWidget { class _Screen extends StatelessWidget { const _Screen({ - required this.backupKey, - required this.backupId, - required this.backupSalt, + this.backupKey, + required this.encryptedBackup, }); - final String backupSalt; - final String backupKey; - final String backupId; final String? backupKey; final Map encryptedBackup; @@ -107,12 +116,22 @@ class _Screen extends StatelessWidget { previous.pinConfirmed != current.pinConfirmed || previous.passwordConfirmed != current.passwordConfirmed, listener: (context, state) { - if ((state.pinConfirmed || state.passwordConfirmed) && - !state.saving && - state.error.isEmpty) { - context - .read() - .secureKey(backupId, backupKey, backupSalt); + if (state.isSecretConfirmed && + !state.loading && + !state.hasError && + state.keySecretState != KeySecretState.saved) { + context.read().secureKey(); + } + + if (state.keySecretState == KeySecretState.saved && + !state.loading && + !state.hasError) { + context.read().clearSensitive(); + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => const _SuccessDialog(isRecovery: false), + ); } if (state.error.isNotEmpty) { ScaffoldMessenger.of(context).showSnackBar( From bdd98fad04daf3638c05a5a6460841ed65cb0e97 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Wed, 5 Feb 2025 20:04:03 -0500 Subject: [PATCH 138/401] refactor(keychain): improve state handling and error management for recovery --- lib/wallet_settings/keychain_page.dart | 31 ++++++++++++++++++++------ 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/lib/wallet_settings/keychain_page.dart b/lib/wallet_settings/keychain_page.dart index 6316d3402..025c3d264 100644 --- a/lib/wallet_settings/keychain_page.dart +++ b/lib/wallet_settings/keychain_page.dart @@ -4,7 +4,6 @@ import 'package:bb_mobile/_ui/app_bar.dart'; import 'package:bb_mobile/_ui/components/text.dart'; import 'package:bb_mobile/_ui/components/text_input.dart'; import 'package:bb_mobile/_ui/page_template.dart'; -import 'package:bb_mobile/_ui/toast.dart'; import 'package:bb_mobile/styles.dart'; import 'package:bb_mobile/wallet_settings/bloc/backup_settings_cubit.dart'; import 'package:bb_mobile/wallet_settings/bloc/backup_settings_state.dart'; @@ -47,7 +46,6 @@ class KeychainBackupPage extends StatelessWidget { ), ], child: _Screen( - backupId: backupId, backupKey: backupKey, encryptedBackup: backup, ), @@ -113,8 +111,9 @@ class _Screen extends StatelessWidget { ), BlocListener( listenWhen: (previous, current) => - previous.pinConfirmed != current.pinConfirmed || - previous.passwordConfirmed != current.passwordConfirmed, + previous.isSecretConfirmed != current.isSecretConfirmed || + previous.keySecretState != current.keySecretState || + previous.error != current.error, listener: (context, state) { if (state.isSecretConfirmed && !state.loading && @@ -133,9 +132,27 @@ class _Screen extends StatelessWidget { builder: (context) => const _SuccessDialog(isRecovery: false), ); } - if (state.error.isNotEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - context.showToast(state.error), + + if (state.keySecretState == KeySecretState.recovered && + !state.loading && + !state.hasError) { + if (encryptedBackup.isNotEmpty) { + context.read().recoverBackup( + jsonEncode(encryptedBackup), + state.backupKey, + ); + // Remove immediate success dialog - will show after successful recovery + // showDialog(...) + } + } + + if (state.hasError) { + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => _ErrorDialog( + error: state.error, + ), ); } }, From e6217c4e023ca9f2ae6cee446ca067421cad1408 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Wed, 5 Feb 2025 20:04:27 -0500 Subject: [PATCH 139/401] refactor(keychain): cleanup --- lib/wallet_settings/keychain_page.dart | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/wallet_settings/keychain_page.dart b/lib/wallet_settings/keychain_page.dart index 025c3d264..8832dd176 100644 --- a/lib/wallet_settings/keychain_page.dart +++ b/lib/wallet_settings/keychain_page.dart @@ -185,14 +185,14 @@ class _Screen extends StatelessWidget { inputType: state.inputType, ) : state.pageState == KeyChainPageState.enter - ? _EnterPage( - key: const ValueKey('enter'), - inputType: state.inputType, - ) - : _ConfirmPage( - key: const ValueKey('confirm'), - inputType: state.inputType, - ), + ? _EnterPage( + key: const ValueKey('enter'), + inputType: state.inputType, + ) + : _ConfirmPage( + key: const ValueKey('confirm'), + inputType: state.inputType, + ), ), ); }, From c9706b7039a354b325b3020ddd4e4894488cba4d Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Wed, 5 Feb 2025 20:04:45 -0500 Subject: [PATCH 140/401] refactor(keychain): update route parameters for KeychainBackupPage --- lib/routes.dart | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/routes.dart b/lib/routes.dart index e04b0fddd..6c9d4a2af 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -237,12 +237,11 @@ GoRouter setupRouter() => GoRouter( GoRoute( path: 'keychain', builder: (context, state) { - final (backupId, (backupKey, backupSalt)) = - state.extra! as (String, (String, String)); + final (backupKey, backup) = + state.extra! as (String?, Map); return KeychainBackupPage( - backupId: backupId, backupKey: backupKey, - backupSalt: backupSalt, + backup: backup, ); }, ), From 217dac6c8b4e43da738d03e8abb80cac7db10361 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Wed, 5 Feb 2025 20:04:59 -0500 Subject: [PATCH 141/401] feat(keychain): add recovery route for encrypted vault --- lib/routes.dart | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/lib/routes.dart b/lib/routes.dart index 6c9d4a2af..121c0145c 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -234,6 +234,24 @@ GoRouter setupRouter() => GoRouter( wallet: state.extra! as String, ), ), + GoRoute( + path: 'recover-encrypted', + builder: (context, state) => EncryptedVaultRecoverPage( + wallet: state.extra! as String, + ), + routes: [ + GoRoute( + path: 'info', + builder: (context, state) { + final recoveredBackup = + state.extra! as Map; + return RecoveredBackupInfoPage( + recoveredBackup: recoveredBackup, + ); + }, + ), + ], + ), GoRoute( path: 'keychain', builder: (context, state) { From 37de8752cd40b873085fbd5be723bb536836b46b Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Wed, 5 Feb 2025 20:05:19 -0500 Subject: [PATCH 142/401] refactor(keychain): remove recover-options route --- lib/routes.dart | 6 ------ 1 file changed, 6 deletions(-) diff --git a/lib/routes.dart b/lib/routes.dart index 121c0145c..57dd7bc2a 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -263,12 +263,6 @@ GoRouter setupRouter() => GoRouter( ); }, ), - GoRoute( - path: 'recover-options', - builder: (context, state) => RecoverOptionsScreen( - wallet: state.extra! as String, - ), - ), ], ), ], From b7592642e929871328b9c8d6945f182e2e49bddc Mon Sep 17 00:00:00 2001 From: ethicnology Date: Thu, 6 Feb 2025 10:38:37 -0500 Subject: [PATCH 143/401] =?UTF-8?q?refactor:=20com.bullbitcoin.bbMobile=20?= =?UTF-8?q?=E2=80=93>=20com.bullbitcoin.mobile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ios/Runner.xcodeproj/project.pbxproj | 6 +++--- macos/Runner.xcodeproj/project.pbxproj | 6 +++--- macos/Runner/Configs/AppInfo.xcconfig | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 1a6ef2a74..27d389125 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -515,7 +515,7 @@ CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.bullbitcoin.bbMobile.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = com.bullbitcoin.mobile.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -533,7 +533,7 @@ CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.bullbitcoin.bbMobile.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = com.bullbitcoin.mobile.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; @@ -549,7 +549,7 @@ CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.bullbitcoin.bbMobile.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = com.bullbitcoin.mobile.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index 3b0d74368..c918e5e90 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -385,7 +385,7 @@ CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.bullbitcoin.bbMobile.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = com.bullbitcoin.mobile.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/bb_mobile.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/bb_mobile"; @@ -399,7 +399,7 @@ CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.bullbitcoin.bbMobile.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = com.bullbitcoin.mobile.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/bb_mobile.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/bb_mobile"; @@ -413,7 +413,7 @@ CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.bullbitcoin.bbMobile.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = com.bullbitcoin.mobile.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/bb_mobile.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/bb_mobile"; diff --git a/macos/Runner/Configs/AppInfo.xcconfig b/macos/Runner/Configs/AppInfo.xcconfig index 14a160863..09f0fa524 100644 --- a/macos/Runner/Configs/AppInfo.xcconfig +++ b/macos/Runner/Configs/AppInfo.xcconfig @@ -8,7 +8,7 @@ PRODUCT_NAME = bb_mobile // The application's bundle identifier -PRODUCT_BUNDLE_IDENTIFIER = com.bullbitcoin.bbMobile +PRODUCT_BUNDLE_IDENTIFIER = com.bullbitcoin.mobile // The copyright displayed in application information PRODUCT_COPYRIGHT = Copyright © 2024 com.bullbitcoin. All rights reserved. From d4ae9e7702419f540ca4cc257cd306aec2129805 Mon Sep 17 00:00:00 2001 From: ethicnology Date: Thu, 6 Feb 2025 11:15:13 -0500 Subject: [PATCH 144/401] refactor: update Info.plist with Google Drive ids --- ios/Runner/Info.plist | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index e7c149e34..8a311ea50 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -49,5 +49,19 @@ UIFileSharingEnabled + + GIDClientID + 249304738825-k5vfkegjtqv85pffdmcdr1hv6chusth6.apps.googleusercontent.com + BUNDLE_ID + com.bullbitcoin.mobile + CFBundleURLTypes + + + CFBundleURLSchemes + + com.googleusercontent.apps.249304738825-k5vfkegjtqv85pffdmcdr1hv6chusth6 + + + - + \ No newline at end of file From 461c721511f7303580ac326551408b477848f150 Mon Sep 17 00:00:00 2001 From: ethicnology Date: Thu, 6 Feb 2025 11:19:02 -0500 Subject: [PATCH 145/401] chore: update lock files --- ios/Podfile.lock | 35 ++++++++++++++++++++--------------- pubspec.lock | 32 ++++++++++++++++---------------- 2 files changed, 36 insertions(+), 31 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 90744a7f7..0fb36805b 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -174,30 +174,35 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/url_launcher_ios/ios" SPEC CHECKSUMS: - bdk_flutter: fb57a7400a7f3f181c5977bcdc2a5ef347ae4e7f - bip85: f656a7e6b23afda4960efb11c87d51d68e8be3db + AppAuth: d4f13a8fe0baf391b2108511793e4b479691fb73 + bdk_flutter: 08274535b7bd58a09bfa9c3733474fb44133484c + bip85: 3059e217a3a606a100dd244f21d2fc61b0a1aa83 boltz: 6388ec2412f3753b63a9e65c97f87ea26f43bddc DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 - document_file_save_plus: 913d440d8b611ae19add4522ed578e3ed1483a2f - file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655 + document_file_save_plus: 47b52a647efb29f7d431af45a856ce1a841f32fc + file_picker: 9b3292d7c8bc68c8a7bf8eb78f730e49c8efc517 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - flutter_barcode_scanner: 7a1144744c28dc0c57a8de7218ffe5ec59a9e4bf - flutter_file_dialog: 4c014a45b105709a27391e266c277d7e588e9299 - flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069 - flutter_native_splash: f71420956eb811e6d310720fee915f1d42852e7a - flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12 - integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573 + flutter_barcode_scanner: c5aa9f51c150a6242fa392386bd52b64bb27fcb5 + flutter_file_dialog: ca8d7fbd1772d4f0c2777b4ab20a7787ef4e7dd8 + flutter_keyboard_visibility: 4625131e43015dbbe759d9b20daaf77e0e3f6619 + flutter_native_splash: 6cad9122ea0fad137d23137dd14b937f3e90b145 + flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13 + google_sign_in_ios: 0ab078e60da6dfe23cbc55c83502b52bba1aad63 + GoogleSignIn: d4281ab6cf21542b1cfaff85c191f230b399d2db + GTMAppAuth: f69bd07d68cd3b766125f7e072c45d7340dea0de + GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6 + integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e lwk: 22e06bc5664247d6b2dac91cfe209b63b70dd580 - no_screenshot: 67d110f12466f4913b488803d4e498d03ef2889e - path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 + no_screenshot: 6d183496405a3ab709a67a54e5cd0f639e94729e + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 payjoin_flutter: 6397d7b698cdad6453be4949ab6aca1863f6c5e5 - permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 + permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d ScreenProtectorKit: 83a6281b02c7a5902ee6eac4f5045f674e902ae4 SDWebImage: 8a6b7b160b4d710e2a22b6900e25301075c34cb3 - share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad + share_plus: 011d6fb4f9d2576b83179a3a5c5e323202cdabcf SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 - url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe + url_launcher_ios: 694010445543906933d732453a59da0a173ae33d PODFILE CHECKSUM: 819463e6a0290f5a72f145ba7cde16e8b6ef0796 diff --git a/pubspec.lock b/pubspec.lock index 891da3bb0..951430ce3 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -339,18 +339,18 @@ packages: dependency: "direct main" description: name: currency_text_input_formatter - sha256: e2dda73e5ddbe2b70b898ece09aeb661a10808e084f776bebf81e07e8d57b29c + sha256: "7ed954e38b0d89c068e29cbc7e9194cd5c36ec2940b8ef407c1425d8854ec45f" url: "https://pub.dev" source: hosted - version: "2.2.6" + version: "2.2.8" dart_style: dependency: transitive description: name: dart_style - sha256: "7856d364b589d1f08986e140938578ed36ed948581fbc3bc9aef1805039ac5ab" + sha256: "7306ab8a2359a48d22310ad823521d723acfed60ee1f7e37388e8986853b6820" url: "https://pub.dev" source: hosted - version: "2.3.7" + version: "2.3.8" diff_match_patch: dependency: transitive description: @@ -363,18 +363,18 @@ packages: dependency: "direct main" description: name: dio - sha256: "5598aa796bbf4699afd5c67c0f5f6e2ed542afc956884b9cd58c306966efc260" + sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9" url: "https://pub.dev" source: hosted - version: "5.7.0" + version: "5.8.0+1" dio_web_adapter: dependency: transitive description: name: dio_web_adapter - sha256: "33259a9276d6cea88774a0000cfae0d861003497755969c92faa223108620dc8" + sha256: e485c7a39ff2b384fa1d7e09b4e25f755804de8384358049124830b04fc4f93a url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.1.0" document_file_save_plus: dependency: "direct main" description: @@ -732,18 +732,18 @@ packages: dependency: transitive description: name: glob - sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" go_router: dependency: "direct main" description: name: go_router - sha256: "7c2d40b59890a929824f30d442e810116caf5088482629c894b9e4478c67472d" + sha256: "9b736a9fa879d8ad6df7932cbdcc58237c173ab004ef90d8377923d7ad731eaa" url: "https://pub.dev" source: hosted - version: "14.6.3" + version: "14.7.2" google_fonts: dependency: "direct main" description: @@ -1640,10 +1640,10 @@ packages: dependency: transitive description: name: vector_graphics - sha256: "27d5fefe86fb9aace4a9f8375b56b3c292b64d8c04510df230f849850d912cb7" + sha256: "44cc7104ff32563122a929e4620cf3efd584194eec6d1d913eb5ba593dbcf6de" url: "https://pub.dev" source: hosted - version: "1.1.15" + version: "1.1.18" vector_graphics_codec: dependency: transitive description: @@ -1728,10 +1728,10 @@ packages: dependency: transitive description: name: win32 - sha256: "154360849a56b7b67331c21f09a386562d88903f90a1099c5987afc1912e1f29" + sha256: daf97c9d80197ed7b619040e86c8ab9a9dad285e7671ee7390f9180cc828a51e url: "https://pub.dev" source: hosted - version: "5.10.0" + version: "5.10.1" xdg_directories: dependency: transitive description: From 63296cfe284a1cdf6f284d807d2ce828047bf1dd Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Thu, 6 Feb 2025 12:24:16 -0500 Subject: [PATCH 146/401] chore: update Podfile.lock with new dependency checksums --- ios/Podfile.lock | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 0fb36805b..1f68a0215 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -175,34 +175,34 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: AppAuth: d4f13a8fe0baf391b2108511793e4b479691fb73 - bdk_flutter: 08274535b7bd58a09bfa9c3733474fb44133484c - bip85: 3059e217a3a606a100dd244f21d2fc61b0a1aa83 + bdk_flutter: fb57a7400a7f3f181c5977bcdc2a5ef347ae4e7f + bip85: f656a7e6b23afda4960efb11c87d51d68e8be3db boltz: 6388ec2412f3753b63a9e65c97f87ea26f43bddc DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 - document_file_save_plus: 47b52a647efb29f7d431af45a856ce1a841f32fc - file_picker: 9b3292d7c8bc68c8a7bf8eb78f730e49c8efc517 + document_file_save_plus: 913d440d8b611ae19add4522ed578e3ed1483a2f + file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - flutter_barcode_scanner: c5aa9f51c150a6242fa392386bd52b64bb27fcb5 - flutter_file_dialog: ca8d7fbd1772d4f0c2777b4ab20a7787ef4e7dd8 - flutter_keyboard_visibility: 4625131e43015dbbe759d9b20daaf77e0e3f6619 - flutter_native_splash: 6cad9122ea0fad137d23137dd14b937f3e90b145 - flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13 - google_sign_in_ios: 0ab078e60da6dfe23cbc55c83502b52bba1aad63 + flutter_barcode_scanner: 7a1144744c28dc0c57a8de7218ffe5ec59a9e4bf + flutter_file_dialog: 4c014a45b105709a27391e266c277d7e588e9299 + flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069 + flutter_native_splash: f71420956eb811e6d310720fee915f1d42852e7a + flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12 + google_sign_in_ios: 07375bfbf2620bc93a602c0e27160d6afc6ead38 GoogleSignIn: d4281ab6cf21542b1cfaff85c191f230b399d2db GTMAppAuth: f69bd07d68cd3b766125f7e072c45d7340dea0de GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6 - integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e + integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573 lwk: 22e06bc5664247d6b2dac91cfe209b63b70dd580 - no_screenshot: 6d183496405a3ab709a67a54e5cd0f639e94729e - path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + no_screenshot: 67d110f12466f4913b488803d4e498d03ef2889e + path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 payjoin_flutter: 6397d7b698cdad6453be4949ab6aca1863f6c5e5 - permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d + permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 ScreenProtectorKit: 83a6281b02c7a5902ee6eac4f5045f674e902ae4 SDWebImage: 8a6b7b160b4d710e2a22b6900e25301075c34cb3 - share_plus: 011d6fb4f9d2576b83179a3a5c5e323202cdabcf + share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 - url_launcher_ios: 694010445543906933d732453a59da0a173ae33d + url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe PODFILE CHECKSUM: 819463e6a0290f5a72f145ba7cde16e8b6ef0796 From adb508f3bd4fa91b88aed9b97ec981c19d087249 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Thu, 6 Feb 2025 12:24:27 -0500 Subject: [PATCH 147/401] refactor: remove unnecessary drive manager disposal --- lib/wallet_settings/bloc/backup_settings_cubit.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/wallet_settings/bloc/backup_settings_cubit.dart b/lib/wallet_settings/bloc/backup_settings_cubit.dart index f6741d6a9..56c24d1f8 100644 --- a/lib/wallet_settings/bloc/backup_settings_cubit.dart +++ b/lib/wallet_settings/bloc/backup_settings_cubit.dart @@ -90,7 +90,6 @@ class BackupSettingsCubit extends Cubit { @override Future close() async { - await _driveManager.dispose(); await super.close(); } From b3823d5f5eddc6e4f65c17bc9ebcd6dcc96ad7d0 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Thu, 6 Feb 2025 12:25:24 -0500 Subject: [PATCH 148/401] refactor: improve error messages for key recovery and fixed password validation --- lib/wallet_settings/bloc/keychain_cubit.dart | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/wallet_settings/bloc/keychain_cubit.dart b/lib/wallet_settings/bloc/keychain_cubit.dart index 855600bca..ca5e7538e 100644 --- a/lib/wallet_settings/bloc/keychain_cubit.dart +++ b/lib/wallet_settings/bloc/keychain_cubit.dart @@ -141,7 +141,13 @@ class KeychainCubit extends Cubit { Future clickRecoverKey() async { if (state.secret.length != 6) { - emit(state.copyWith(error: 'pin should be 6 digits long')); + state.inputType == KeyChainInputType.pin + ? emit(state.copyWith(error: 'pin should be atleast 6 digits long')) + : emit( + state.copyWith( + error: 'password should be atleast 6 characters long', + ), + ); return; } From 729d18965674de7cb018ae900d6a5abe17669cce Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Thu, 6 Feb 2025 12:35:05 -0500 Subject: [PATCH 149/401] fix: update key recovery validation --- lib/wallet_settings/bloc/keychain_cubit.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/wallet_settings/bloc/keychain_cubit.dart b/lib/wallet_settings/bloc/keychain_cubit.dart index ca5e7538e..bc40310cc 100644 --- a/lib/wallet_settings/bloc/keychain_cubit.dart +++ b/lib/wallet_settings/bloc/keychain_cubit.dart @@ -140,7 +140,8 @@ class KeychainCubit extends Cubit { } Future clickRecoverKey() async { - if (state.secret.length != 6) { + if (state.secret.length < 6) { + print(state.secret); state.inputType == KeyChainInputType.pin ? emit(state.copyWith(error: 'pin should be atleast 6 digits long')) : emit( @@ -164,6 +165,7 @@ class KeychainCubit extends Cubit { password: state.secret, salt: state.backupSalt, ); + print(state.secret); emit( state.copyWith( backupKey: HEX.encode(backupKey), From e52165d534dd007019ed20081cdceaa966126d24 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Thu, 6 Feb 2025 12:52:05 -0500 Subject: [PATCH 150/401] refactor: remove debug print statements from key recovery process --- lib/wallet_settings/bloc/keychain_cubit.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/wallet_settings/bloc/keychain_cubit.dart b/lib/wallet_settings/bloc/keychain_cubit.dart index bc40310cc..15bbe144a 100644 --- a/lib/wallet_settings/bloc/keychain_cubit.dart +++ b/lib/wallet_settings/bloc/keychain_cubit.dart @@ -141,7 +141,6 @@ class KeychainCubit extends Cubit { Future clickRecoverKey() async { if (state.secret.length < 6) { - print(state.secret); state.inputType == KeyChainInputType.pin ? emit(state.copyWith(error: 'pin should be atleast 6 digits long')) : emit( @@ -165,7 +164,6 @@ class KeychainCubit extends Cubit { password: state.secret, salt: state.backupSalt, ); - print(state.secret); emit( state.copyWith( backupKey: HEX.encode(backupKey), From c3a8743b9b5d167889223d3d049a2544c48bc55a Mon Sep 17 00:00:00 2001 From: ethicnology Date: Thu, 6 Feb 2025 13:31:55 -0500 Subject: [PATCH 151/401] chore: remove prints of the secret --- lib/wallet_settings/bloc/keychain_cubit.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/wallet_settings/bloc/keychain_cubit.dart b/lib/wallet_settings/bloc/keychain_cubit.dart index bc40310cc..15bbe144a 100644 --- a/lib/wallet_settings/bloc/keychain_cubit.dart +++ b/lib/wallet_settings/bloc/keychain_cubit.dart @@ -141,7 +141,6 @@ class KeychainCubit extends Cubit { Future clickRecoverKey() async { if (state.secret.length < 6) { - print(state.secret); state.inputType == KeyChainInputType.pin ? emit(state.copyWith(error: 'pin should be atleast 6 digits long')) : emit( @@ -165,7 +164,6 @@ class KeychainCubit extends Cubit { password: state.secret, salt: state.backupSalt, ); - print(state.secret); emit( state.copyWith( backupKey: HEX.encode(backupKey), From 3df57828b0997847e94cbfcd14977238453e9868 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Thu, 6 Feb 2025 13:44:39 -0500 Subject: [PATCH 152/401] feat: add obscure text option to BBTextInput for password fields --- lib/_ui/components/text_input.dart | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/lib/_ui/components/text_input.dart b/lib/_ui/components/text_input.dart index 9a27d756d..e75cb46de 100644 --- a/lib/_ui/components/text_input.dart +++ b/lib/_ui/components/text_input.dart @@ -26,7 +26,8 @@ class BBTextInput extends StatefulWidget { onEnter = null, onDone = null, maxLength = null, - onlyNumbers = false; + onlyNumbers = false, + obscure = false; const BBTextInput.big({ required this.onChanged, @@ -39,6 +40,7 @@ class BBTextInput extends StatefulWidget { this.maxLength, this.uiKey, this.onEnter, + this.obscure = false, }) : type = _TextInputType.big, rightIcon = null, onRightTap = null, @@ -55,6 +57,7 @@ class BBTextInput extends StatefulWidget { this.hint, this.uiKey, this.controller, + this.obscure = false, }) : type = _TextInputType.bigWithIcon, onEnter = null, onDone = null, @@ -71,6 +74,7 @@ class BBTextInput extends StatefulWidget { this.hint, this.uiKey, this.controller, + this.obscure = false, }) : type = _TextInputType.bigWithIcon2, onEnter = null, onDone = null, @@ -88,6 +92,7 @@ class BBTextInput extends StatefulWidget { this.controller, this.onEnter, this.onDone, + this.obscure = false, }) : type = _TextInputType.small, rightIcon = null, onRightTap = null, @@ -110,6 +115,7 @@ class BBTextInput extends StatefulWidget { final Function(String)? onDone; final int? maxLength; final bool onlyNumbers; + final bool? obscure; @override State createState() => _BBTextInputState(); @@ -143,7 +149,7 @@ class _BBTextInputState extends State { controller: _editingController, enableIMEPersonalizedLearning: false, keyboardType: TextInputType.multiline, - maxLines: 5, + maxLines: widget.obscure ?? false ? 1 : 5, style: context.font.bodySmall! .copyWith(color: context.colour.onPrimaryContainer), onTap: () => widget.onEnter?.call(), @@ -176,8 +182,8 @@ class _BBTextInputState extends State { widgett = SizedBox( height: height, child: TextField( - expands: true, - maxLines: null, + expands: !(widget.obscure ?? false), + maxLines: widget.obscure ?? false ? 1 : null, key: widget.uiKey, enabled: !widget.disabled, focusNode: widget.focusNode, @@ -186,6 +192,7 @@ class _BBTextInputState extends State { enableIMEPersonalizedLearning: false, controller: _editingController, keyboardType: widget.onlyNumbers ? TextInputType.number : null, + obscureText: widget.obscure ?? false, onTap: () => widget.onEnter?.call(), decoration: InputDecoration( hintText: widget.hint, @@ -225,14 +232,15 @@ class _BBTextInputState extends State { widgett = SizedBox( height: height, child: TextField( - expands: true, - maxLines: null, + expands: !(widget.obscure ?? false), + maxLines: widget.obscure ?? false ? 1 : null, focusNode: widget.focusNode, enabled: !widget.disabled, onChanged: widget.onChanged, controller: _editingController, enableIMEPersonalizedLearning: false, keyboardType: widget.onlyNumbers ? TextInputType.number : null, + obscureText: widget.obscure ?? false, onTap: () => widget.onEnter?.call(), decoration: InputDecoration( hintText: widget.hint, @@ -271,8 +279,8 @@ class _BBTextInputState extends State { widgett = SizedBox( height: height, child: TextField( - expands: true, - maxLines: null, + expands: !(widget.obscure ?? false), + maxLines: widget.obscure ?? false ? 1 : null, focusNode: widget.focusNode, enabled: !widget.disabled, onChanged: widget.onChanged, @@ -280,6 +288,7 @@ class _BBTextInputState extends State { enableIMEPersonalizedLearning: false, onTap: () => widget.onEnter?.call(), keyboardType: widget.onlyNumbers ? TextInputType.number : null, + obscureText: widget.obscure ?? false, decoration: InputDecoration( hintText: widget.hint, hintStyle: TextStyle( @@ -324,7 +333,7 @@ class _BBTextInputState extends State { onChanged: widget.onChanged, controller: _editingController, keyboardType: widget.onlyNumbers ? TextInputType.number : null, - + obscureText: widget.obscure ?? false, onSubmitted: (value) => widget.onDone?.call(value), // widget.onDone != null ? widget.onDone!(value) : null, onTap: () => widget.onEnter?.call(), From c9300686e581b03bcfe281637369d2612e91e7fd Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Thu, 6 Feb 2025 13:45:15 -0500 Subject: [PATCH 153/401] refactor: remove unused BIP85PathsButton --- lib/wallet_settings/wallet_settings_page.dart | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/lib/wallet_settings/wallet_settings_page.dart b/lib/wallet_settings/wallet_settings_page.dart index a7bf4d97f..01bd9e367 100644 --- a/lib/wallet_settings/wallet_settings_page.dart +++ b/lib/wallet_settings/wallet_settings_page.dart @@ -148,8 +148,6 @@ class _ScreenState extends State<_Screen> { // const Gap(8), const WalletDetailsButton(), const Gap(8), - const Bip85PathsButton(), - const Gap(8), const AddressesButtons(), const Gap(8), @@ -421,24 +419,6 @@ class AccountingButton extends StatelessWidget { } } -class Bip85PathsButton extends StatelessWidget { - const Bip85PathsButton({super.key}); - - @override - Widget build(BuildContext context) { - return BBButton.textWithStatusAndRightArrow( - label: 'BIP85 Paths', - onPressed: () { - final wallet = context.read().state.wallet; - context.push( - '/wallet-settings/bip85-paths', - extra: wallet.id, - ); - }, - ); - } -} - class WalletDetailsButton extends StatelessWidget { const WalletDetailsButton({super.key}); From b733db554789e8e50a8d9d3fdc8fbac580f5acc7 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Thu, 6 Feb 2025 13:45:34 -0500 Subject: [PATCH 154/401] feat: add toggle functionality for obscuring sensitive information --- lib/wallet_settings/bloc/keychain_cubit.dart | 6 ++++++ lib/wallet_settings/bloc/keychain_state.dart | 1 + 2 files changed, 7 insertions(+) diff --git a/lib/wallet_settings/bloc/keychain_cubit.dart b/lib/wallet_settings/bloc/keychain_cubit.dart index 15bbe144a..365f3a45f 100644 --- a/lib/wallet_settings/bloc/keychain_cubit.dart +++ b/lib/wallet_settings/bloc/keychain_cubit.dart @@ -15,6 +15,12 @@ class KeychainCubit extends Cubit { emit(state.copyWith(shuffledNumbers: shuffledList)); } + void clickObscure() { + emit( + state.copyWith(obscure: !state.obscure), + ); + } + void setChainState( KeyChainPageState keyChainPageState, String backupId, diff --git a/lib/wallet_settings/bloc/keychain_state.dart b/lib/wallet_settings/bloc/keychain_state.dart index 755e8a7a0..00f46f0dd 100644 --- a/lib/wallet_settings/bloc/keychain_state.dart +++ b/lib/wallet_settings/bloc/keychain_state.dart @@ -17,6 +17,7 @@ class KeychainState with _$KeychainState { @Default(KeySecretState.none) KeySecretState keySecretState, @Default('') String secret, @Default('') String tempSecret, + @Default(false) bool obscure, @Default('') String backupId, @Default('') String backupKey, @Default([]) List backupSalt, From 22acfff8f9a4285af20a16548ab730523ac730ec Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Thu, 6 Feb 2025 13:45:52 -0500 Subject: [PATCH 155/401] feat: enhance password field with toggle visibility option --- lib/wallet_settings/keychain_page.dart | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/wallet_settings/keychain_page.dart b/lib/wallet_settings/keychain_page.dart index 8832dd176..4e2f00f02 100644 --- a/lib/wallet_settings/keychain_page.dart +++ b/lib/wallet_settings/keychain_page.dart @@ -365,10 +365,17 @@ class _PasswordField extends StatelessWidget { @override Widget build(BuildContext context) { final secret = context.select((KeychainCubit x) => x.state.secret); - return BBTextInput.big( + final isObscure = context.select((KeychainCubit x) => x.state.obscure); + return BBTextInput.bigWithIcon( value: secret, onChanged: (value) => context.read().updateInput(value), + obscure: isObscure, hint: 'Enter your password', + rightIcon: Icon( + isObscure ? Icons.visibility_off : Icons.visibility, + color: context.colour.onPrimaryContainer, + ), + onRightTap: () => context.read().clickObscure(), ); } } From ddb2342da718d94ae0e221c4f0291ad3b194271d Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Fri, 7 Feb 2025 15:34:09 -0500 Subject: [PATCH 156/401] feat: add method to get directory path in FilePicker --- lib/_pkg/file_picker.dart | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/_pkg/file_picker.dart b/lib/_pkg/file_picker.dart index 3a2df6255..5fffaede4 100644 --- a/lib/_pkg/file_picker.dart +++ b/lib/_pkg/file_picker.dart @@ -23,4 +23,14 @@ class FilePick { return (null, Err(e.toString())); } } + + Future<(String?, Err?)> getDirectoryPath() async { + try { + final path = await FilePicker.platform.getDirectoryPath(); + if (path == null) return (null, Err('No directory selected')); + return (path, null); + } catch (e) { + return (null, Err(e.toString())); + } + } } From f5c8b299b97099ca2b35ba6ff058d8edc0a4d239 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Fri, 7 Feb 2025 15:38:25 -0500 Subject: [PATCH 157/401] fix: optimize HomeBloc to load the backed up wallet data and manage wallet subscriptions --- lib/home/bloc/home_bloc.dart | 35 ++++++++++++++++--- .../bloc/backup_settings_cubit.dart | 5 +++ 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/lib/home/bloc/home_bloc.dart b/lib/home/bloc/home_bloc.dart index 4128ec56b..b5723fcff 100644 --- a/lib/home/bloc/home_bloc.dart +++ b/lib/home/bloc/home_bloc.dart @@ -66,12 +66,39 @@ class HomeBloc extends Bloc { final walletServicesData = event.walletServices .map((_) => WalletServiceData(wallet: _.wallet)) .toList(); - emit(state.copyWith(wallets: walletServicesData)); - // Listen to wallet data updates - for (final ws in event.walletServices) { + // Check for new wallets + final currentWalletIds = state.wallets.map((w) => w.wallet.id).toSet(); + final newWalletIds = walletServicesData.map((w) => w.wallet.id).toSet(); + final hasNewWallets = newWalletIds.difference(currentWalletIds).isNotEmpty; + + // Only emit if we have changes + if (hasNewWallets || state.wallets != walletServicesData) { + emit(state.copyWith( + wallets: walletServicesData, + updated: hasNewWallets, + )); + } + + // Update subscriptions for wallet data changes + _updateWalletSubscriptions(event.walletServices); + } + + void _updateWalletSubscriptions(List services) { + // Cancel old subscriptions that are no longer needed + final newIds = services.map((ws) => ws.wallet.id).toSet(); + _walletServiceDataUpdateSubscriptions.keys + .where((id) => !newIds.contains(id)) + .toList() + .forEach((id) { + _walletServiceDataUpdateSubscriptions[id]?.cancel(); + _walletServiceDataUpdateSubscriptions.remove(id); + }); + + // Add or update subscriptions for current services + for (final ws in services) { if (_walletServiceDataUpdateSubscriptions.containsKey(ws.wallet.id)) { - _walletServiceDataUpdateSubscriptions[ws.wallet.id]!.cancel(); + continue; // Skip if subscription already exists } _walletServiceDataUpdateSubscriptions[ws.wallet.id] = ws.dataStream.listen( diff --git a/lib/wallet_settings/bloc/backup_settings_cubit.dart b/lib/wallet_settings/bloc/backup_settings_cubit.dart index 56c24d1f8..cd37bf257 100644 --- a/lib/wallet_settings/bloc/backup_settings_cubit.dart +++ b/lib/wallet_settings/bloc/backup_settings_cubit.dart @@ -16,6 +16,8 @@ import 'package:bb_mobile/_repository/app_wallets_repository.dart'; import 'package:bb_mobile/_repository/wallet/sensitive_wallet_storage.dart'; import 'package:bb_mobile/_repository/wallet/wallet_storage.dart'; import 'package:bb_mobile/_repository/wallet_service.dart'; +import 'package:bb_mobile/home/bloc/home_bloc.dart'; +import 'package:bb_mobile/home/bloc/home_event.dart'; import 'package:bb_mobile/locator.dart'; import 'package:bb_mobile/wallet_settings/bloc/backup_settings_state.dart'; import 'package:flutter/material.dart'; @@ -826,6 +828,9 @@ class BackupSettingsCubit extends Cubit { } } + // Notify HomeBloc that wallets have been recovered + locator().add(LoadWalletsFromStorage()); + emit( state.copyWith( loadingBackups: false, From ae769887cf2015a324d668048e4bc9dfd0242fa0 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Fri, 7 Feb 2025 15:38:55 -0500 Subject: [PATCH 158/401] refactor: streamline backup directory creation and enhance backup saving logic --- lib/_pkg/backup/local.dart | 3 +- .../bloc/backup_settings_cubit.dart | 116 +++++++++++++----- 2 files changed, 89 insertions(+), 30 deletions(-) diff --git a/lib/_pkg/backup/local.dart b/lib/_pkg/backup/local.dart index 8692416ae..12c3f1304 100644 --- a/lib/_pkg/backup/local.dart +++ b/lib/_pkg/backup/local.dart @@ -67,8 +67,7 @@ class FileSystemBackupManager extends IBackupManager { return (null, Err('Failed to get application directory.')); } - final backupDir = - await Directory('${appDir!}/$backupFolder').create(recursive: true); + final backupDir = await Directory(backupFolder).create(recursive: true); final file = File('${backupDir.path}/$filename'); final (f, errSave) = await fileStorage.saveToFile( diff --git a/lib/wallet_settings/bloc/backup_settings_cubit.dart b/lib/wallet_settings/bloc/backup_settings_cubit.dart index cd37bf257..c6f98585e 100644 --- a/lib/wallet_settings/bloc/backup_settings_cubit.dart +++ b/lib/wallet_settings/bloc/backup_settings_cubit.dart @@ -119,16 +119,18 @@ class BackupSettingsCubit extends Cubit { final words = seed.mnemonic.split(' '); final shuffled = words.toList()..shuffle(); - emit(state.copyWith( - testMnemonicOrder: [], - mnemonic: words, - errTestingBackup: '', - password: seed - .getPassphraseFromIndex(_currentWallet!.sourceFingerprint) - .passphrase, - shuffledMnemonic: shuffled, - loadingBackups: false, - )); + emit( + state.copyWith( + testMnemonicOrder: [], + mnemonic: words, + errTestingBackup: '', + password: seed + .getPassphraseFromIndex(_currentWallet!.sourceFingerprint) + .passphrase, + shuffledMnemonic: shuffled, + loadingBackups: false, + ), + ); } void _emitBackupTestSuccessState() { @@ -143,20 +145,24 @@ class BackupSettingsCubit extends Cubit { Future loadBackupForVerification() async { if (_currentWallet == null) { - emit(state.copyWith( - errorLoadingBackups: 'No wallet selected for verification', - loadingBackups: false, - )); + emit( + state.copyWith( + errorLoadingBackups: 'No wallet selected for verification', + loadingBackups: false, + ), + ); return; } emit(state.copyWith(loadingBackups: true)); final (seed, error) = await _loadWalletSeed(_currentWallet!); if (error != null || seed == null) { - emit(state.copyWith( - errTestingBackup: error ?? 'Seed data not found', - loadingBackups: false, - )); + emit( + state.copyWith( + errTestingBackup: error ?? 'Seed data not found', + loadingBackups: false, + ), + ); return; } @@ -360,7 +366,7 @@ class BackupSettingsCubit extends Cubit { return true; } - Future saveEncryptedBackup() async { + Future saveFileSystemBackup() async { if (!_canStartBackup()) { emit( state.copyWith( @@ -372,6 +378,11 @@ class BackupSettingsCubit extends Cubit { return; } + if (_filePicker == null) { + _emitBackupError('Failed to pick the file'); + return; + } + emit(state.copyWith(savingBackups: true, errorSavingBackups: '')); final backups = await _createBackupsForAllWallets(); @@ -384,7 +395,52 @@ class BackupSettingsCubit extends Cubit { if (err != null || encryptedData == null) { return; } - await _saveToFileSystem(encryptedData); + + try { + final (savePath, pickErr) = await _filePicker.getDirectoryPath(); + if (pickErr != null) { + _emitBackupError('Failed to select backup location'); + return; + } + + if (savePath == null || savePath.isEmpty) { + _emitBackupError('No location selected for backup'); + return; + } + + // Use the selected path with the manager + final (filePath, errSave) = await _manager.saveEncryptedBackup( + encrypted: encryptedData.$2, + backupFolder: savePath, + ); + + if (errSave != null) { + _emitBackupError('Save failed: ${errSave.message}'); + return; + } + + final fileName = filePath?.split('/').last; + final backupId = fileName?.split('_').last.split('.').first; + if (backupId == null) { + _emitBackupError('Failed to extract backup ID'); + return; + } + + final backupSalt = jsonDecode(encryptedData.$2)['salt'] as String; + + emit( + state.copyWith( + backupId: backupId, + backupKey: encryptedData.$1, + backupFolderPath: filePath ?? '', + backupSalt: backupSalt, + savingBackups: false, + lastBackupAttempt: DateTime.now(), + ), + ); + } catch (e) { + _emitBackupError('Failed to save backup: $e'); + } } Future saveGoogleDriveBackup() async { @@ -609,10 +665,12 @@ class BackupSettingsCubit extends Cubit { if (state.backupFolderId.isEmpty) { final (folderId, err) = await _driveManager.connect(); if (err != null) { - emit(state.copyWith( - loadingBackups: false, - errorLoadingBackups: err.message, - )); + emit( + state.copyWith( + loadingBackups: false, + errorLoadingBackups: err.message, + ), + ); return; } emit(state.copyWith(backupFolderId: folderId ?? '')); @@ -620,10 +678,12 @@ class BackupSettingsCubit extends Cubit { // Ensure we have a folder ID if (state.backupFolderId.isEmpty) { - emit(state.copyWith( - loadingBackups: false, - errorLoadingBackups: "Failed to initialize Google Drive folder", - )); + emit( + state.copyWith( + loadingBackups: false, + errorLoadingBackups: "Failed to initialize Google Drive folder", + ), + ); return; } From 6a3bc88b835af4cca8c200af48b65da46258498f Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Fri, 7 Feb 2025 15:39:12 -0500 Subject: [PATCH 159/401] fix: update backup method to save using file system --- lib/wallet_settings/encrypted_vault_backup.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/wallet_settings/encrypted_vault_backup.dart b/lib/wallet_settings/encrypted_vault_backup.dart index edc345db2..c34a8cd79 100644 --- a/lib/wallet_settings/encrypted_vault_backup.dart +++ b/lib/wallet_settings/encrypted_vault_backup.dart @@ -62,7 +62,7 @@ class _EncryptedVaultBackupPageState extends State { case BackupProvider.iCloud: debugPrint('iCloud backup'); case BackupProvider.custom: - _cubit.saveEncryptedBackup(); + _cubit.saveFileSystemBackup(); } } From be2dd6186645bedc279e041d997787bbf4e6e38a Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Fri, 7 Feb 2025 15:39:31 -0500 Subject: [PATCH 160/401] fix: adjust bottom child height in wallet settings pages --- lib/wallet_settings/keychain_page.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/wallet_settings/keychain_page.dart b/lib/wallet_settings/keychain_page.dart index 4e2f00f02..551a791db 100644 --- a/lib/wallet_settings/keychain_page.dart +++ b/lib/wallet_settings/keychain_page.dart @@ -221,7 +221,7 @@ class _EnterPage extends StatelessWidget { @override Widget build(BuildContext context) { return StackedPage( - bottomChildHeight: MediaQuery.of(context).size.height * 0.1, + bottomChildHeight: MediaQuery.of(context).size.height * 0.12, bottomChild: _SetButton(inputType: inputType), child: Padding( key: ValueKey('enter$inputType'), @@ -255,6 +255,7 @@ class _ConfirmPage extends StatelessWidget { Widget build(BuildContext context) { return StackedPage( bottomChild: _ConfirmButton(inputType: inputType), + bottomChildHeight: MediaQuery.of(context).size.height * 0.12, child: SingleChildScrollView( key: ValueKey('confirm$inputType'), child: Padding( @@ -289,7 +290,7 @@ class _RecoveryPage extends StatelessWidget { @override Widget build(BuildContext context) { return StackedPage( - bottomChildHeight: MediaQuery.of(context).size.height * 0.1, + bottomChildHeight: MediaQuery.of(context).size.height * 0.12, bottomChild: _RecoverButton(inputType: inputType), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 32), From 0c9819da34ef5aeaa0ec6670fd0eb7f4e35e73df Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Fri, 7 Feb 2025 15:43:19 -0500 Subject: [PATCH 161/401] refactor: remove BIP85 paths page and clean up routing --- lib/routes.dart | 12 -- lib/wallet_settings/bip85_paths.dart | 259 --------------------------- 2 files changed, 271 deletions(-) delete mode 100644 lib/wallet_settings/bip85_paths.dart diff --git a/lib/routes.dart b/lib/routes.dart index 57dd7bc2a..f4ea7264b 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -33,7 +33,6 @@ import 'package:bb_mobile/wallet/information_page.dart'; import 'package:bb_mobile/wallet/wallet_page.dart'; import 'package:bb_mobile/wallet_settings/accounting.dart'; import 'package:bb_mobile/wallet_settings/backup_settings.dart'; -import 'package:bb_mobile/wallet_settings/bip85_paths.dart'; import 'package:bb_mobile/wallet_settings/encrypted_vault_backup.dart'; import 'package:bb_mobile/wallet_settings/keychain_page.dart'; import 'package:bb_mobile/wallet_settings/physical_backup.dart'; @@ -353,17 +352,6 @@ GoRouter setupRouter() => GoRouter( }, ), - GoRoute( - path: '/wallet-settings/bip85-paths', - builder: (context, state) { - final wallet = state.extra! as String; - return Bip85PathsPage(wallet: wallet); - }, - ), - // - // - // - GoRoute( path: '/swap-page', builder: (context, state) { diff --git a/lib/wallet_settings/bip85_paths.dart b/lib/wallet_settings/bip85_paths.dart deleted file mode 100644 index c7081e2ac..000000000 --- a/lib/wallet_settings/bip85_paths.dart +++ /dev/null @@ -1,259 +0,0 @@ -import 'package:bb_mobile/_model/wallet.dart'; -import 'package:bb_mobile/_ui/app_bar.dart'; -import 'package:bb_mobile/_ui/components/text.dart'; -import 'package:bb_mobile/_ui/toast.dart'; -import 'package:bb_mobile/wallet_settings/bloc/wallet_settings_state.dart'; -import 'package:bb_mobile/wallet_settings/bloc/wallet_settings_cubit.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:gap/gap.dart'; -import 'package:go_router/go_router.dart'; - -class Bip85PathsPage extends StatelessWidget { - const Bip85PathsPage({super.key, required this.wallet}); - - final String wallet; - - @override - Widget build(BuildContext context) { - return BlocProvider.value( - value: createWalletSettingsCubit(wallet)..loadBIP85Derivations(), - child: Scaffold( - appBar: AppBar( - automaticallyImplyLeading: false, - flexibleSpace: BBAppBar( - text: 'BIP85 Paths', - onBack: context.pop, - ), - ), - body: const _Screen(), - ), - ); - } -} - -class _Screen extends StatelessWidget { - const _Screen(); - - @override - Widget build(BuildContext context) { - return BlocConsumer( - listener: (context, state) { - if (state.errUpdatingBip85Derivations.isNotEmpty) { - context.showToast(state.errUpdatingBip85Derivations); - } - }, - buildWhen: (previous, current) => - previous.bip85Derivations.length != current.bip85Derivations.length || - current.bip85Derivations.entries.any((entry) { - final prev = previous.bip85Derivations[entry.key]; - return prev == null || - prev.label != entry.value.label || - prev.status != entry.value.status; - }), - builder: (context, state) { - final activeBip85Paths = state.bip85Derivations.entries - .where( - (entry) => entry.value.status == BIP85DerivationStatus.active, - ) - .toList(); - final deprecatedBip85Paths = state.bip85Derivations.entries - .where( - (entry) => entry.value.status == BIP85DerivationStatus.revoked, - ) - .toList(); - - return state.updatingBip85Derivations - ? const Center(child: CircularProgressIndicator()) - : Column( - children: [ - Expanded( - child: SingleChildScrollView( - padding: const EdgeInsets.all(24), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const BBText.body('Active bip85 paths'), - const Gap(8), - _buildBip85List(context, activeBip85Paths), - if (deprecatedBip85Paths.isNotEmpty) ...[ - const BBText.body('Revoked bip85 paths'), - const Gap(8), - _buildBip85List( - context, - deprecatedBip85Paths, - isDeprecated: true, - ), - ], - ], - ), - ), - ), - Padding( - padding: const EdgeInsets.all(24), - child: ElevatedButton( - onPressed: () => _showCreateDialog(context), - child: const Text('Create New Derivation Path'), - ), - ), - ], - ); - }, - ); - } - - Widget _buildBip85List( - BuildContext context, - List> paths, { - bool isDeprecated = false, - }) { - if (paths.isEmpty) { - return const Text('No paths available'); - } - - return ListView.separated( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: paths.length, - separatorBuilder: (_, __) => const Divider(height: 1), - itemBuilder: (context, index) { - final entry = paths[index]; - return InkWell( - onTap: isDeprecated ? null : () => _showEditDialog(context, entry), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 12), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - flex: 3, - child: Text( - entry.value.label, - style: TextStyle( - fontWeight: FontWeight.bold, - color: isDeprecated ? Colors.grey : null, - ), - ), - ), - Expanded( - child: Text( - entry.key, - style: TextStyle( - color: isDeprecated ? Colors.grey : null, - ), - ), - ), - ], - ), - ), - ); - }, - ); - } - - void _showEditDialog( - BuildContext context, - MapEntry entry, - ) { - final labelController = TextEditingController(text: entry.value.label); - final cubit = context.read(); - - showDialog( - context: context, - builder: (dialogContext) => - BlocConsumer( - bloc: cubit, - listener: (context, state) { - if (!state.updatingBip85Derivations) { - Navigator.pop(dialogContext); - } - }, - builder: (context, state) => AlertDialog( - title: const BBText.body('Edit Label'), - content: TextField( - controller: labelController, - decoration: const InputDecoration( - border: OutlineInputBorder(), - labelText: 'Label', - floatingLabelStyle: TextStyle(color: Colors.black), - floatingLabelBehavior: FloatingLabelBehavior.always, - focusedBorder: OutlineInputBorder(), - ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(dialogContext), - child: const Text('Cancel'), - ), - TextButton( - onPressed: state.updatingBip85Derivations - ? null - : () { - if (labelController.text.isNotEmpty) { - cubit.updateBIP85LabelClicked( - entry.key, - labelController.text, - ); - } - }, - child: state.updatingBip85Derivations - ? const CircularProgressIndicator() - : const Text('Update'), - ), - ], - ), - ), - ); - } - - void _showCreateDialog(BuildContext context) { - final labelController = TextEditingController(); - final cubit = context.read(); - - showDialog( - context: context, - builder: (dialogContext) => - BlocConsumer( - bloc: cubit, - listener: (context, state) { - if (!state.updatingBip85Derivations) { - Navigator.pop(dialogContext); - } - }, - builder: (context, state) => AlertDialog( - title: const BBText.body('Create Derivation Path'), - content: TextField( - controller: labelController, - decoration: const InputDecoration( - border: OutlineInputBorder(), - labelText: 'Label', - floatingLabelStyle: TextStyle(color: Colors.black), - floatingLabelBehavior: FloatingLabelBehavior.always, - focusedBorder: OutlineInputBorder(), - ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(dialogContext), - child: const Text('Cancel'), - ), - TextButton( - onPressed: state.updatingBip85Derivations - ? null - : () { - if (labelController.text.isNotEmpty) { - cubit.createNewBIP85BackupKeyClicked( - labelController.text, - ); - } - }, - child: state.updatingBip85Derivations - ? const CircularProgressIndicator() - : const Text('Create'), - ), - ], - ), - ), - ); - } -} From 3ee2555766bd0d6bc4fa7d705db38be108a66800 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Fri, 7 Feb 2025 15:43:32 -0500 Subject: [PATCH 162/401] fix: remove unused navigation code --- lib/wallet_settings/physical_backup.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/wallet_settings/physical_backup.dart b/lib/wallet_settings/physical_backup.dart index 506fbd74c..98746dbc8 100644 --- a/lib/wallet_settings/physical_backup.dart +++ b/lib/wallet_settings/physical_backup.dart @@ -70,7 +70,6 @@ class _Screen extends StatelessWidget { if (!context.mounted) return false; context.pop(); - //TODO: context.go('/home'); return true; } catch (e) { return false; From 0da83ddde752549a0cc3cf75664c61e1d3bebb73 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Fri, 7 Feb 2025 15:58:48 -0500 Subject: [PATCH 163/401] feat: add Google Drive integration with dynamic configuration --- ios/Flutter/Debug.xcconfig | 3 +++ ios/Flutter/Release.xcconfig | 3 +++ ios/Runner/Info.plist | 4 ++-- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig index ec97fc6f3..c66223dc6 100644 --- a/ios/Flutter/Debug.xcconfig +++ b/ios/Flutter/Debug.xcconfig @@ -1,2 +1,5 @@ #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" + +GOOGLE_DRIVE_CLIENT_ID=249304738825-k5vfkegjtqv85pffdmcdr1hv6chusth6.apps.googleusercontent.com +GOOGLE_DRIVE_URL_SCHEME=com.googleusercontent.apps.249304738825-k5vfkegjtqv85pffdmcdr1hv6chusth6 diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig index c4855bfe2..6188d6769 100644 --- a/ios/Flutter/Release.xcconfig +++ b/ios/Flutter/Release.xcconfig @@ -1,2 +1,5 @@ #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" + +GOOGLE_DRIVE_CLIENT_ID= +GOOGLE_DRIVE_URL_SCHEME= diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 8a311ea50..7fdc08e8d 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -51,7 +51,7 @@ GIDClientID - 249304738825-k5vfkegjtqv85pffdmcdr1hv6chusth6.apps.googleusercontent.com + $(GOOGLE_DRIVE_CLIENT_ID) BUNDLE_ID com.bullbitcoin.mobile CFBundleURLTypes @@ -59,7 +59,7 @@ CFBundleURLSchemes - com.googleusercontent.apps.249304738825-k5vfkegjtqv85pffdmcdr1hv6chusth6 + $(GOOGLE_DRIVE_URL_SCHEME) From c2faa1bf75c49a99f2ffc01a2773ff72ec16f733 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Fri, 7 Feb 2025 16:08:07 -0500 Subject: [PATCH 164/401] fix: remove unused mnemonicFingerPrint field from Backup model --- lib/_model/backup.dart | 1 - lib/wallet_settings/bloc/backup_settings_cubit.dart | 1 - 2 files changed, 2 deletions(-) diff --git a/lib/_model/backup.dart b/lib/_model/backup.dart index ca8ec7817..b20b215c7 100644 --- a/lib/_model/backup.dart +++ b/lib/_model/backup.dart @@ -10,7 +10,6 @@ class Backup with _$Backup { @Default('') String name, @Default([]) List mnemonic, @Default('') String passphrase, - @Default('') String mnemonicFingerPrint, @Default('') String network, @Default('') String layer, @Default('') String type, diff --git a/lib/wallet_settings/bloc/backup_settings_cubit.dart b/lib/wallet_settings/bloc/backup_settings_cubit.dart index c6f98585e..55ce9993c 100644 --- a/lib/wallet_settings/bloc/backup_settings_cubit.dart +++ b/lib/wallet_settings/bloc/backup_settings_cubit.dart @@ -614,7 +614,6 @@ class BackupSettingsCubit extends Cubit { final backup = Backup( name: wallet.name ?? '', network: wallet.network.name, - mnemonicFingerPrint: wallet.mnemonicFingerprint, layer: wallet.baseWalletType.name, script: wallet.scriptType.name, type: wallet.type.name, From 3e50ccabca718b93a3785196bfba051f29c752ea Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Fri, 7 Feb 2025 16:08:30 -0500 Subject: [PATCH 165/401] fix: refactor Google Drive backup saving logic --- .../bloc/backup_settings_cubit.dart | 81 +++++-------------- 1 file changed, 21 insertions(+), 60 deletions(-) diff --git a/lib/wallet_settings/bloc/backup_settings_cubit.dart b/lib/wallet_settings/bloc/backup_settings_cubit.dart index 55ce9993c..a3d08ad15 100644 --- a/lib/wallet_settings/bloc/backup_settings_cubit.dart +++ b/lib/wallet_settings/bloc/backup_settings_cubit.dart @@ -477,65 +477,6 @@ class BackupSettingsCubit extends Cubit { final (encryptedData, err) = await _encryptBackups(backups); if (err != null || encryptedData == null) return; - await _saveToGoogleDrive(encryptedData); - } catch (e) { - debugPrint('Error saving to Google Drive: $e'); - _emitBackupError('Failed to save Google Drive backup'); - } - } - - Future<((String, String)?, Err?)> _encryptBackups( - List backups, - ) async { - try { - final (encData, err) = await _manager.encryptBackups( - backups: backups, - derivationPath: _kDerivationPath, - ); - - if (err != null || encData == null) { - return (null, err); - } - - return (encData, null); - } catch (e) { - return (null, Err(e.toString())); - } - } - - Future _saveToFileSystem((String, String) encryptedData) async { - final (filePath, errSave) = await _manager.saveEncryptedBackup( - encrypted: encryptedData.$2, - ); - - if (errSave != null) { - _emitBackupError('Save failed: ${errSave.message}'); - return; - } - - final fileName = filePath?.split('/').last; - final backupId = fileName?.split('_').last.split('.').first; - if (backupId == null) { - _emitBackupError('Failed to extract backup ID'); - return; - } - - final backupSalt = jsonDecode(encryptedData.$2)['salt'] as String; - - emit( - state.copyWith( - backupId: backupId, - backupKey: encryptedData.$1, - backupFolderPath: filePath ?? '', - backupSalt: backupSalt, - savingBackups: false, - lastBackupAttempt: DateTime.now(), - ), - ); - } - - Future _saveToGoogleDrive((String, String) encryptedData) async { - try { final backupSalt = jsonDecode(encryptedData.$2)['salt'] as String; final (filePath, error) = await _driveManager.saveEncryptedBackup( @@ -566,7 +507,27 @@ class BackupSettingsCubit extends Cubit { ), ); } catch (e) { - _emitBackupError('Failed to save to Google Drive: $e'); + debugPrint('Error saving to Google Drive: $e'); + _emitBackupError('Failed to save Google Drive backup'); + } + } + + Future<((String, String)?, Err?)> _encryptBackups( + List backups, + ) async { + try { + final (encData, err) = await _manager.encryptBackups( + backups: backups, + derivationPath: _kDerivationPath, + ); + + if (err != null || encData == null) { + return (null, err); + } + + return (encData, null); + } catch (e) { + return (null, Err(e.toString())); } } From 23b7f5c1ce3e157ac5736b6a8ddbcbf4b78bb7c9 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 10 Feb 2025 14:16:22 -0500 Subject: [PATCH 166/401] refactor: remove BIP85 derivation logic and related state management --- lib/_model/wallet.dart | 27 ------ lib/_pkg/wallet/update.dart | 33 -------- lib/_repository/wallet_service.dart | 7 -- .../bloc/wallet_settings_cubit.dart | 82 ------------------- .../bloc/wallet_settings_state.dart | 3 - 5 files changed, 152 deletions(-) diff --git a/lib/_model/wallet.dart b/lib/_model/wallet.dart index d4d7b0b58..1eaa5a624 100644 --- a/lib/_model/wallet.dart +++ b/lib/_model/wallet.dart @@ -61,7 +61,6 @@ class Wallet with _$Wallet { @Default('') String externalPublicDescriptor, @Default('') String internalPublicDescriptor, // public - @Default({}) Map bip85Derivations, @Default('') String mnemonicFingerprint, @Default('') String sourceFingerprint, required BBNetwork network, @@ -205,19 +204,6 @@ class Wallet with _$Wallet { return exDescDerivedKey; } - //TODO; re-create this to derive any bip85 path - String generateNextBIP85BackupKey() { - const prefix = "m/1608'/"; - final highestIndex = bip85Derivations.keys - .where((path) => path.startsWith(prefix)) - .map( - (path) => - int.tryParse(path.split('/').last.replaceAll("'", "")) ?? -1, - ) - .fold(-1, (max, index) => index > max ? index : max); - return "$prefix${highestIndex + 1}'"; - } - // storage key String getRelatedSeedStorageString() { // TODO: Sai: Uncomment this (or) add :testnet while saving testnet seed (later) @@ -562,19 +548,6 @@ class Wallet with _$Wallet { String balanceStr() => ((balance ?? 0) / 100000000).toStringAsFixed(8); } -enum BIP85DerivationStatus { active, revoked } - -@freezed -class BIP85Derivation with _$BIP85Derivation { - const factory BIP85Derivation({ - required String label, - @Default(BIP85DerivationStatus.active) BIP85DerivationStatus status, - }) = _BIP85Derivation; - - factory BIP85Derivation.fromJson(Map json) => - _$BIP85DerivationFromJson(json); -} - @freezed class Balance with _$Balance { const factory Balance({ diff --git a/lib/_pkg/wallet/update.dart b/lib/_pkg/wallet/update.dart index 7ad20b633..bc3b4e507 100644 --- a/lib/_pkg/wallet/update.dart +++ b/lib/_pkg/wallet/update.dart @@ -24,39 +24,6 @@ class WalletUpdate { } } - Wallet updateBIP85Paths( - Wallet wallet, - String bip85Path, - String label, - ) { - final updatedDerivations = - Map.from(wallet.bip85Derivations); - final existingDerivation = updatedDerivations[bip85Path]; - if (existingDerivation != null) { - updatedDerivations[bip85Path] = existingDerivation.copyWith( - label: label, - ); - return wallet.copyWith(bip85Derivations: updatedDerivations); - } - - final activeBIP85Derivation = updatedDerivations.entries - .where((e) => e.value.status == BIP85DerivationStatus.active) - .firstOrNull; - - if (activeBIP85Derivation != null) { - updatedDerivations[activeBIP85Derivation.key] = BIP85Derivation( - label: activeBIP85Derivation.value.label, - status: BIP85DerivationStatus.revoked, - ); - } - updatedDerivations[bip85Path] = BIP85Derivation( - label: label, - ); - return wallet.copyWith( - bip85Derivations: updatedDerivations, - ); - } - Future<(Wallet?, Err?)> updateAddressLabels( Wallet wallet, List
addresses, diff --git a/lib/_repository/wallet_service.dart b/lib/_repository/wallet_service.dart index 66c552821..247d46c4d 100644 --- a/lib/_repository/wallet_service.dart +++ b/lib/_repository/wallet_service.dart @@ -281,12 +281,6 @@ class WalletService { lastPhysicalBackupTested: wallet.lastPhysicalBackupTested, ); } - case UpdateWalletTypes.bip85Paths: - if (wallet.bip85Derivations != storageWallet!.bip85Derivations) { - storageWallet = storageWallet.copyWith( - bip85Derivations: wallet.bip85Derivations, - ); - } } final err = await _walletsStorageRepository.updateWallet( @@ -396,5 +390,4 @@ enum UpdateWalletTypes { addresses, settings, utxos, - bip85Paths, } diff --git a/lib/wallet_settings/bloc/wallet_settings_cubit.dart b/lib/wallet_settings/bloc/wallet_settings_cubit.dart index 16cfaebf2..59023c150 100644 --- a/lib/wallet_settings/bloc/wallet_settings_cubit.dart +++ b/lib/wallet_settings/bloc/wallet_settings_cubit.dart @@ -318,88 +318,6 @@ class WalletSettingsCubit extends Cubit { } } - void loadBIP85Derivations() { - emit(state.copyWith(bip85Derivations: _wallet.bip85Derivations)); - } - - Future createNewBIP85BackupKeyClicked(String label) async { - emit(state.copyWith(updatingBip85Derivations: true)); - try { - final wallet = _wallet; - final path = wallet.generateNextBIP85BackupKey(); - - await _appWalletsRepository.getWalletServiceById(wallet.id)?.updateWallet( - WalletUpdate().updateBIP85Paths(wallet, path, label), - updateTypes: [UpdateWalletTypes.bip85Paths], - ); - final updatedWallet = _appWalletsRepository.getWalletById(wallet.id); - if (updatedWallet == null) { - emit( - state.copyWith( - errUpdatingBip85Derivations: 'Failed to update the wallet', - updatingBip85Derivations: false, - ), - ); - return; - } - - emit( - state.copyWith( - bip85Derivations: updatedWallet.bip85Derivations, - updatingBip85Derivations: false, - ), - ); - } catch (e) { - emit( - state.copyWith( - errUpdatingBip85Derivations: e.toString(), - updatingBip85Derivations: false, - ), - ); - } - } - - Future updateBIP85LabelClicked(String path, String newLabel) async { - emit( - state.copyWith( - updatingBip85Derivations: true, - errUpdatingBip85Derivations: '', - ), - ); - - try { - final wallet = _wallet; - await _appWalletsRepository.getWalletServiceById(wallet.id)?.updateWallet( - WalletUpdate().updateBIP85Paths(wallet, path, newLabel), - updateTypes: [UpdateWalletTypes.bip85Paths], - ); - - final updatedWallet = _appWalletsRepository.getWalletById(wallet.id); - if (updatedWallet == null) { - emit( - state.copyWith( - errUpdatingBip85Derivations: 'Failed to update the wallet', - updatingBip85Derivations: false, - ), - ); - return; - } - emit( - state.copyWith( - bip85Derivations: updatedWallet.bip85Derivations, - updatingBip85Derivations: false, - ), - ); - } catch (e) { - emit( - state.copyWith( - errUpdatingBip85Derivations: e.toString(), - updatingBip85Derivations: false, - ), - ); - } - } - Future clearSensitive() async { emit( state.copyWith( diff --git a/lib/wallet_settings/bloc/wallet_settings_state.dart b/lib/wallet_settings/bloc/wallet_settings_state.dart index 64cd453b0..f9892129c 100644 --- a/lib/wallet_settings/bloc/wallet_settings_state.dart +++ b/lib/wallet_settings/bloc/wallet_settings_state.dart @@ -25,9 +25,6 @@ class WalletSettingsState with _$WalletSettingsState { * SENSITIVE * */ - @Default({}) Map bip85Derivations, - @Default(false) bool updatingBip85Derivations, - @Default('') String errUpdatingBip85Derivations, @Default(false) bool backup, @Default(false) bool testingBackup, @Default('') String errTestingBackup, From ec78eb9b6665bd606f3cb3161fb38f8e311821c9 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 10 Feb 2025 14:40:52 -0500 Subject: [PATCH 167/401] refactor: remove BIP85 derivation logic and update backup key generation --- lib/_pkg/backup/_interface.dart | 37 +++++++------------ .../bloc/backup_settings_cubit.dart | 2 - pubspec.lock | 10 ++++- pubspec.yaml | 3 +- 4 files changed, 25 insertions(+), 27 deletions(-) diff --git a/lib/_pkg/backup/_interface.dart b/lib/_pkg/backup/_interface.dart index f97095ccf..d66cdac94 100644 --- a/lib/_pkg/backup/_interface.dart +++ b/lib/_pkg/backup/_interface.dart @@ -1,10 +1,8 @@ import 'dart:convert'; +import 'dart:math'; import 'package:bb_mobile/_model/backup.dart'; -import 'package:bb_mobile/_model/wallet.dart'; import 'package:bb_mobile/_pkg/consts/configs.dart'; import 'package:bb_mobile/_pkg/error.dart'; -import 'package:bdk_flutter/bdk_flutter.dart'; -import 'package:bip85/bip85.dart'; import 'package:hex/hex.dart'; import 'package:recoverbull/recoverbull.dart' as recoverbull; @@ -12,7 +10,6 @@ abstract class IBackupManager { /// Encrypts a list of backups using BIP85 derivation Future<((String, String)?, Err?)> encryptBackups({ required List backups, - required String derivationPath, }) async { if (backups.isEmpty) { return (null, Err('No backups provided')); @@ -20,11 +17,7 @@ abstract class IBackupManager { try { final plaintext = json.encode(backups.map((i) => i.toJson()).toList()); - final key = await _deriveBackupKey( - mnemonic: backups.first.mnemonic.join(' '), - network: backups.first.network, - path: derivationPath, - ); + final key = await _deriveBackupKey(); if (key == null) { return (null, Err('Failed to derive backup key')); @@ -57,22 +50,20 @@ abstract class IBackupManager { } } - Future?> _deriveBackupKey({ - required String mnemonic, - required String network, - required String path, - }) async { + Future?> _deriveBackupKey() async { try { - final mne = await Mnemonic.fromString(mnemonic); - final descriptorSecretKey = await DescriptorSecretKey.create( - network: BBNetwork.fromString(network).toBdkNetwork(), - mnemonic: mne, - ); - final res = derive( - xprv: descriptorSecretKey.toString().split('/*').first, - path: path, + final now = DateTime.now(); + final nowBytes = + utf8.encode(now.toUtc().millisecondsSinceEpoch.toString()); + + final secureRandom = Random.secure(); + final randomBytes = + List.generate(32, (_) => secureRandom.nextInt(256)); + final key = List.generate( + 32, + (i) => randomBytes[i] ^ nowBytes[i % nowBytes.length], ); - return res.sublist(0, 32); + return key; } catch (e) { return null; } diff --git a/lib/wallet_settings/bloc/backup_settings_cubit.dart b/lib/wallet_settings/bloc/backup_settings_cubit.dart index a3d08ad15..814d658ab 100644 --- a/lib/wallet_settings/bloc/backup_settings_cubit.dart +++ b/lib/wallet_settings/bloc/backup_settings_cubit.dart @@ -88,7 +88,6 @@ class BackupSettingsCubit extends Cubit { static const _kDelayDuration = Duration(milliseconds: 800); static const _kShuffleDelay = Duration(milliseconds: 500); static const _kMinBackupInterval = Duration(seconds: 5); - static const _kDerivationPath = "m/1608'/0'"; @override Future close() async { @@ -518,7 +517,6 @@ class BackupSettingsCubit extends Cubit { try { final (encData, err) = await _manager.encryptBackups( backups: backups, - derivationPath: _kDerivationPath, ); if (err != null || encData == null) { diff --git a/pubspec.lock b/pubspec.lock index 951430ce3..e44b47a2c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -95,7 +95,7 @@ packages: source: hosted version: "3.0.7" bip85: - dependency: "direct main" + dependency: transitive description: name: bip85 sha256: "1e556b32a6e2062a8e6f728bfdf1898058ecde9c9068f5272d4792af2477b10c" @@ -489,6 +489,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_env_native: + dependency: "direct dev" + description: + name: flutter_env_native + sha256: "3da347b4769452b9a67ea63239f6f63a46d7c183cac55c1a41972cbb8763eb8a" + url: "https://pub.dev" + source: hosted + version: "0.1.0" flutter_file_dialog: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index ec8fbbc12..a4dc592d9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -94,7 +94,6 @@ dependencies: permission_handler: ^11.3.1 rxdart: ^0.28.0 synchronized: ^3.3.0+3 - bip85: ^1.0.3 web_socket_channel: ^3.0.1 flutter_speed_dial: ^7.0.0 googleapis: ^13.2.0 @@ -117,6 +116,8 @@ dev_dependencies: bloc_test: ^9.1.7 mocktail: ^1.0.1 flutter_launcher_icons: ^0.13.1 + flutter_env_native: ^0.1.0 + flutter: uses-material-design: true From 1aaabeac2f594b62cb16f5464667b84a34fb16bd Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 10 Feb 2025 14:44:19 -0500 Subject: [PATCH 168/401] refactor: remove unused physicalBackupTested --- .../wallet/sensitive/wallet_sensitive_create_test.data.dart | 6 ------ 1 file changed, 6 deletions(-) diff --git a/test/_pkg/wallet/sensitive/wallet_sensitive_create_test.data.dart b/test/_pkg/wallet/sensitive/wallet_sensitive_create_test.data.dart index 8b41bcd66..b77e3c359 100644 --- a/test/_pkg/wallet/sensitive/wallet_sensitive_create_test.data.dart +++ b/test/_pkg/wallet/sensitive/wallet_sensitive_create_test.data.dart @@ -27,7 +27,6 @@ // myAddressBook: [], // transactions: [], // unsignedTxs: [], -// physicalBackupTested: hasImported, // baseWalletType: BaseWalletType.Bitcoin, // ); // case ScriptType.bip49: @@ -52,7 +51,6 @@ // myAddressBook: [], // transactions: [], // unsignedTxs: [], -// physicalBackupTested: hasImported, // ); // case ScriptType.bip84: // return Wallet( @@ -76,7 +74,6 @@ // myAddressBook: [], // transactions: [], // unsignedTxs: [], -// physicalBackupTested: hasImported, // ); // } // } else { @@ -103,7 +100,6 @@ // myAddressBook: [], // transactions: [], // unsignedTxs: [], -// physicalBackupTested: hasImported, // ); // case ScriptType.bip49: // return Wallet( @@ -127,7 +123,6 @@ // myAddressBook: [], // transactions: [], // unsignedTxs: [], -// physicalBackupTested: hasImported, // ); // case ScriptType.bip84: // return Wallet( @@ -151,7 +146,6 @@ // myAddressBook: [], // transactions: [], // unsignedTxs: [], -// physicalBackupTested: hasImported, // ); // } // } From 6c33612ce40a67e834d1823df9deb73bcb5eff59 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 10 Feb 2025 14:54:04 -0500 Subject: [PATCH 169/401] refactor: remove unused TestBackupButton and related code --- lib/wallet_settings/wallet_settings_page.dart | 53 ------------------- 1 file changed, 53 deletions(-) diff --git a/lib/wallet_settings/wallet_settings_page.dart b/lib/wallet_settings/wallet_settings_page.dart index 01bd9e367..8b0f8245d 100644 --- a/lib/wallet_settings/wallet_settings_page.dart +++ b/lib/wallet_settings/wallet_settings_page.dart @@ -25,11 +25,8 @@ class WalletSettingsPage extends StatelessWidget { const WalletSettingsPage({ super.key, required this.wallet, - // this.openTestBackup = false, this.openBackup = false, }); - - // final bool openTestBackup; final bool openBackup; final String wallet; @@ -59,7 +56,6 @@ class WalletSettingsPage extends StatelessWidget { ], child: WalletSettingsListeners( child: _Screen( - // openTestBackup: openTestBackup, openBackup: openBackup, ), ), @@ -78,14 +74,12 @@ class _Screen extends StatefulWidget { } class _ScreenState extends State<_Screen> { - // bool showPage = false; @override void initState() { _init(); super.initState(); } -//TODO; Move it to backup-settings page void _init() { scheduleMicrotask(() async { if (widget.openBackup) { @@ -139,8 +133,6 @@ class _ScreenState extends State<_Screen> { if (!watchOnly) ...[ const BackupSettingsButton(), const Gap(8), - // const TestBackupButton(), - // const Gap(8), ], // const PublicDescriptorButton(), // const Gap(8), @@ -434,51 +426,6 @@ class WalletDetailsButton extends StatelessWidget { } } -class TestBackupButton extends StatelessWidget { - const TestBackupButton({super.key}); - - @override - Widget build(BuildContext context) { - final isTested = - context.select((WalletBloc x) => x.state.wallet.physicalBackupTested); - - // if (isTested) return const SizedBox.shrink(); - return BBButton.textWithStatusAndRightArrow( - label: 'Test Backup', - statusText: isTested ? 'Tested' : 'Not Tested', - isRed: !isTested, - onPressed: () async { - ///TODO: Move it to backup-settings page - context.push( - '/wallet-settings/test-backup', - extra: context.read().state.wallet.id, - // ( - // context.read(), - // context.read(), - // ), - ); - // await TestBackupScreen.openPopup(context); - }, - ); - // return Row( - // children: [ - // BBButton.textWithLeftArrow( - // label: 'Test Backup', - // onPressed: () async { - // await TestBackupScreen.openPopup(context); - // }, - // ), - // const Spacer(), - // BBText.body( - // isTested ? 'Tested' : 'Not tested', - // isGreen: isTested, - // isRed: !isTested, - // ), - // ], - // ); - } -} - class BackupSettingsButton extends StatelessWidget { const BackupSettingsButton({super.key}); From 5f76c322d0b07035f27163aacd27241dfcfcb7d1 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 10 Feb 2025 14:58:25 -0500 Subject: [PATCH 170/401] refactor: remove flutter_speed_dial dependency and web_socket_channel --- pubspec.lock | 10 +--------- pubspec.yaml | 2 -- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index e44b47a2c..f9185f7a9 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -649,14 +649,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.1.3" - flutter_speed_dial: - dependency: "direct main" - description: - name: flutter_speed_dial - sha256: "698a037274a66dbae8697c265440e6acb6ab6cae9ac5f95c749e7944d8f28d41" - url: "https://pub.dev" - source: hosted - version: "7.0.0" flutter_svg: dependency: "direct main" description: @@ -1709,7 +1701,7 @@ packages: source: hosted version: "0.1.6" web_socket_channel: - dependency: "direct main" + dependency: transitive description: name: web_socket_channel sha256: "0b8e2457400d8a859b7b2030786835a28a8e80836ef64402abef392ff4f1d0e5" diff --git a/pubspec.yaml b/pubspec.yaml index a4dc592d9..08679353d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -94,8 +94,6 @@ dependencies: permission_handler: ^11.3.1 rxdart: ^0.28.0 synchronized: ^3.3.0+3 - web_socket_channel: ^3.0.1 - flutter_speed_dial: ^7.0.0 googleapis: ^13.2.0 google_sign_in: ^6.2.2 extension_google_sign_in_as_googleapis_auth: ^2.0.12 From 9f28080502a5c2646b9957b898bfb8ddbf7d6929 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 10 Feb 2025 15:20:17 -0500 Subject: [PATCH 171/401] refactor: remove Google Drive configuration from xcconfig files --- ios/Flutter/Debug.xcconfig | 4 ++-- ios/Flutter/Release.xcconfig | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig index c66223dc6..4cf795f17 100644 --- a/ios/Flutter/Debug.xcconfig +++ b/ios/Flutter/Debug.xcconfig @@ -1,5 +1,5 @@ #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" -GOOGLE_DRIVE_CLIENT_ID=249304738825-k5vfkegjtqv85pffdmcdr1hv6chusth6.apps.googleusercontent.com -GOOGLE_DRIVE_URL_SCHEME=com.googleusercontent.apps.249304738825-k5vfkegjtqv85pffdmcdr1hv6chusth6 + +#include "Environment.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig index 6188d6769..bb1d7a95f 100644 --- a/ios/Flutter/Release.xcconfig +++ b/ios/Flutter/Release.xcconfig @@ -1,5 +1,3 @@ #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" - -GOOGLE_DRIVE_CLIENT_ID= -GOOGLE_DRIVE_URL_SCHEME= +#include "Environment.xcconfig" From 16a2199de4be99c9d001592bab7c6b0385360de6 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 10 Feb 2025 15:20:34 -0500 Subject: [PATCH 172/401] feat: add environment template for Keychain API and Google Drive configuration --- .env.template | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .env.template diff --git a/.env.template b/.env.template new file mode 100644 index 000000000..7b17549f5 --- /dev/null +++ b/.env.template @@ -0,0 +1,3 @@ +KEYCHAIN_API=https://keychain.bullbitcoin.dev +GOOGLE_DRIVE_CLIENT_ID=249304738825-k5vfkegjtqv85pffdmcdr1hv6chusth6.apps.googleusercontent.com +GOOGLE_DRIVE_URL_SCHEME=com.googleusercontent.apps.249304738825-k5vfkegjtqv85pffdmcdr1hv6chusth6 \ No newline at end of file From c4a44e1d25e5603cc49e788ed8e972312c70c45f Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 10 Feb 2025 15:20:53 -0500 Subject: [PATCH 173/401] refactor: format CFBundleURLTypes in Info.plist for consistency --- ios/Runner/Info.plist | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 7fdc08e8d..2168fe202 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -56,12 +56,12 @@ com.bullbitcoin.mobile CFBundleURLTypes - - CFBundleURLSchemes - - $(GOOGLE_DRIVE_URL_SCHEME) - - + + CFBundleURLSchemes + + $(GOOGLE_DRIVE_URL_SCHEME) + + \ No newline at end of file From 84f7577f7cfb3bd495b124b9d4e3d98adfa263ce Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 10 Feb 2025 15:21:12 -0500 Subject: [PATCH 174/401] feat: add environment setup script and update Podfile for flutter_env_native integration --- ios/.gitignore | 1 + ios/Podfile | 3 + ios/Podfile.lock | 8 +- .../xcshareddata/xcschemes/Runner.xcscheme | 18 ++++ .../xcschemes/Runner.xcscheme.bak | 98 +++++++++++++++++++ 5 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme.bak diff --git a/ios/.gitignore b/ios/.gitignore index 7a7f9873a..f4687e3a1 100644 --- a/ios/.gitignore +++ b/ios/.gitignore @@ -32,3 +32,4 @@ Runner/GeneratedPluginRegistrant.* !default.mode2v3 !default.pbxuser !default.perspectivev3 +Flutter/Environment.xcconfig diff --git a/ios/Podfile b/ios/Podfile index d97f17e22..75f0f04d4 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -41,4 +41,7 @@ post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_ios_build_settings(target) end + flutter_env_plugin_path = File.join(Dir.pwd, '.symlinks', 'plugins', 'flutter_env_native', 'ios', 'setup_env.sh') + root_dir = File.expand_path('..', Dir.pwd) + system("sh \"#{flutter_env_plugin_path}\" \"#{root_dir}\"") end diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 1f68a0215..ea32cee0e 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -49,6 +49,8 @@ PODS: - Flutter (1.0.0) - flutter_barcode_scanner (2.0.0): - Flutter + - flutter_env_native (0.0.1): + - Flutter - flutter_file_dialog (0.0.1): - Flutter - flutter_keyboard_visibility (0.0.1): @@ -105,6 +107,7 @@ DEPENDENCIES: - file_picker (from `.symlinks/plugins/file_picker/ios`) - Flutter (from `Flutter`) - flutter_barcode_scanner (from `.symlinks/plugins/flutter_barcode_scanner/ios`) + - flutter_env_native (from `.symlinks/plugins/flutter_env_native/ios`) - flutter_file_dialog (from `.symlinks/plugins/flutter_file_dialog/ios`) - flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`) - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) @@ -146,6 +149,8 @@ EXTERNAL SOURCES: :path: Flutter flutter_barcode_scanner: :path: ".symlinks/plugins/flutter_barcode_scanner/ios" + flutter_env_native: + :path: ".symlinks/plugins/flutter_env_native/ios" flutter_file_dialog: :path: ".symlinks/plugins/flutter_file_dialog/ios" flutter_keyboard_visibility: @@ -184,6 +189,7 @@ SPEC CHECKSUMS: file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 flutter_barcode_scanner: 7a1144744c28dc0c57a8de7218ffe5ec59a9e4bf + flutter_env_native: 52530b3bced65bd04f849b447c298e75d12565e6 flutter_file_dialog: 4c014a45b105709a27391e266c277d7e588e9299 flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069 flutter_native_splash: f71420956eb811e6d310720fee915f1d42852e7a @@ -204,6 +210,6 @@ SPEC CHECKSUMS: SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe -PODFILE CHECKSUM: 819463e6a0290f5a72f145ba7cde16e8b6ef0796 +PODFILE CHECKSUM: abb44964f6e7af954d7e0c7aafcfb6e7607dee25 COCOAPODS: 1.16.2 diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 8e3ca5dfe..0533c716f 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -21,6 +21,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From e8fdb760052ef949d31e4c8ed51e160d3c9c093b Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 10 Feb 2025 15:21:26 -0500 Subject: [PATCH 175/401] feat: update build workflow to include Google Drive client ID and URL scheme as dart defines --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1790d237b..f61536f5a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -26,7 +26,7 @@ jobs: - run: dart run build_runner build --delete-conflicting-outputs - name: Build no-codesign release - run: flutter build ios --release --no-codesign + run: flutter build ios --release --no-codesign --dart-define=GOOGLE_DRIVE_CLIENT_ID=${{ secrets.GOOGLE_DRIVE_CLIENT_ID }} --dart-define=GOOGLE_DRIVE_URL_SCHEME=${{ secrets.GOOGLE_DRIVE_URL_SCHEME }} - name: Upload artifact uses: actions/upload-artifact@v4 From d04b60d68588c898f84e34cfef1d9c24e76ad517 Mon Sep 17 00:00:00 2001 From: ethicnology Date: Tue, 11 Feb 2025 15:01:20 -0500 Subject: [PATCH 176/401] fix: remove properly bip85 --- lib/main.dart | 2 -- linux/flutter/generated_plugins.cmake | 1 - pubspec.lock | 22 +++++++--------------- 3 files changed, 7 insertions(+), 18 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 1765942e8..c731c0a06 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -20,7 +20,6 @@ import 'package:bb_mobile/settings/bloc/settings_cubit.dart'; import 'package:bb_mobile/styles.dart'; import 'package:bb_mobile/swap/listeners.dart'; import 'package:bb_mobile/swap/watcher_bloc/watchtxs_bloc.dart'; -import 'package:bip85/bip85.dart'; import 'package:boltz/boltz.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -41,7 +40,6 @@ Future main({bool fromTest = false}) async { await core.init(); await LibLwk.init(); await LibBoltz.init(); - await LibBip85.init(); await dotenv.load(isOptional: true); Bloc.observer = BBlocObserver(); // await FlutterWindowManager.addFlags(FlutterWindowManager.FLAG_SECURE); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 407c9d317..32f64c052 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -8,7 +8,6 @@ list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST - bip85 lwk ) diff --git a/pubspec.lock b/pubspec.lock index f9185f7a9..2c701b38a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -94,14 +94,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.7" - bip85: - dependency: transitive - description: - name: bip85 - sha256: "1e556b32a6e2062a8e6f728bfdf1898058ecde9c9068f5272d4792af2477b10c" - url: "https://pub.dev" - source: hosted - version: "1.0.3" bitcoin_utils: dependency: "direct main" description: @@ -187,18 +179,18 @@ packages: dependency: transitive description: name: build_daemon - sha256: "294a2edaf4814a378725bfe6358210196f5ea37af89ecd81bfa32960113d4948" + sha256: "8e928697a82be082206edb0b9c99c5a4ad6bc31c9e9b8b2f291ae65cd4a25daa" url: "https://pub.dev" source: hosted - version: "4.0.3" + version: "4.0.4" build_resolvers: dependency: transitive description: name: build_resolvers - sha256: "99d3980049739a985cf9b21f30881f46db3ebc62c5b8d5e60e27440876b1ba1e" + sha256: b9e4fda21d846e192628e7a4f6deda6888c36b5b69ba02ff291a01fd529140f0 url: "https://pub.dev" source: hosted - version: "2.4.3" + version: "2.4.4" build_runner: dependency: "direct dev" description: @@ -740,10 +732,10 @@ packages: dependency: "direct main" description: name: go_router - sha256: "9b736a9fa879d8ad6df7932cbdcc58237c173ab004ef90d8377923d7ad731eaa" + sha256: "04539267a740931c6d4479a10d466717ca5901c6fdfd3fcda09391bbb8ebd651" url: "https://pub.dev" source: hosted - version: "14.7.2" + version: "14.8.0" google_fonts: dependency: "direct main" description: @@ -1331,7 +1323,7 @@ packages: description: path: "." ref: HEAD - resolved-ref: "1f3a181993fa66c8ca471012a19c4537f2d66403" + resolved-ref: c8c7dc5200d9b1ffacbdd0b2c3f9ca34abffab03 url: "https://github.com/SatoshiPortal/recoverbull-client-dart.git" source: git version: "1.0.0" From 1aa53138e18b16a4a42439ebb774018a5e911302 Mon Sep 17 00:00:00 2001 From: ethicnology Date: Tue, 11 Feb 2025 15:02:11 -0500 Subject: [PATCH 177/401] refactor: remove avoid_redundant_argument_values it's risky if the dependency change the default values --- analysis_options.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/analysis_options.yaml b/analysis_options.yaml index d58f37fb6..189bde9ad 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -8,6 +8,8 @@ linter: analyzer: + errors: + avoid_redundant_argument_values: ignore exclude: - lib/**/*.g.dart - lib/**/*.freezed.dart From 1a57393ed88961619eb92f9bc5000e3ad9848973 Mon Sep 17 00:00:00 2001 From: ethicnology Date: Tue, 11 Feb 2025 15:02:49 -0500 Subject: [PATCH 178/401] fix: various infos and warnings --- lib/home/bloc/home_bloc.dart | 10 ++++++---- lib/settings/settings_page.dart | 2 -- lib/wallet/bloc/wallet_bloc.dart | 2 +- lib/wallet_settings/bloc/wallet_settings_state.dart | 1 - lib/wallet_settings/encrypted_vault_backup.dart | 3 --- lib/wallet_settings/keychain_page.dart | 4 ++-- 6 files changed, 9 insertions(+), 13 deletions(-) diff --git a/lib/home/bloc/home_bloc.dart b/lib/home/bloc/home_bloc.dart index b5723fcff..5ecc08831 100644 --- a/lib/home/bloc/home_bloc.dart +++ b/lib/home/bloc/home_bloc.dart @@ -74,10 +74,12 @@ class HomeBloc extends Bloc { // Only emit if we have changes if (hasNewWallets || state.wallets != walletServicesData) { - emit(state.copyWith( - wallets: walletServicesData, - updated: hasNewWallets, - )); + emit( + state.copyWith( + wallets: walletServicesData, + updated: hasNewWallets, + ), + ); } // Update subscriptions for wallet data changes diff --git a/lib/settings/settings_page.dart b/lib/settings/settings_page.dart index a66242068..1dae6f00b 100644 --- a/lib/settings/settings_page.dart +++ b/lib/settings/settings_page.dart @@ -1,7 +1,5 @@ import 'package:bb_mobile/_pkg/consts/keys.dart'; import 'package:bb_mobile/_pkg/launcher.dart'; -import 'package:bb_mobile/_repository/app_wallets_repository.dart'; -import 'package:bb_mobile/_repository/network_repository.dart'; import 'package:bb_mobile/_ui/app_bar.dart'; import 'package:bb_mobile/_ui/components/button.dart'; import 'package:bb_mobile/_ui/components/text.dart'; diff --git a/lib/wallet/bloc/wallet_bloc.dart b/lib/wallet/bloc/wallet_bloc.dart index a1a6daaeb..0449036b2 100644 --- a/lib/wallet/bloc/wallet_bloc.dart +++ b/lib/wallet/bloc/wallet_bloc.dart @@ -67,7 +67,7 @@ class WalletBloc extends Bloc { final InternalWalletsRepository _walletsRepository; final WalletSync _walletSync; final AppWalletsRepository _appWalletsRepository; - WalletService? _walletServiceFromTempWallets; + late WalletService? _walletServiceFromTempWallets; FutureOr _removeInternalWallet( RemoveInternalWallet event, diff --git a/lib/wallet_settings/bloc/wallet_settings_state.dart b/lib/wallet_settings/bloc/wallet_settings_state.dart index f9892129c..db251fa94 100644 --- a/lib/wallet_settings/bloc/wallet_settings_state.dart +++ b/lib/wallet_settings/bloc/wallet_settings_state.dart @@ -1,4 +1,3 @@ -import 'package:bb_mobile/_model/wallet.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; part 'wallet_settings_state.freezed.dart'; diff --git a/lib/wallet_settings/encrypted_vault_backup.dart b/lib/wallet_settings/encrypted_vault_backup.dart index c34a8cd79..cad9ccea7 100644 --- a/lib/wallet_settings/encrypted_vault_backup.dart +++ b/lib/wallet_settings/encrypted_vault_backup.dart @@ -297,13 +297,10 @@ class _EncryptedVaultRecoverPageState extends State { switch (provider) { case BackupProvider.googleDrive: await _cubit.fetchLatestBacup(); - break; case BackupProvider.iCloud: debugPrint('iCloud backup'); - break; case BackupProvider.custom: _cubit.recoverFromFs(); - break; } } diff --git a/lib/wallet_settings/keychain_page.dart b/lib/wallet_settings/keychain_page.dart index 551a791db..2fd3dc66e 100644 --- a/lib/wallet_settings/keychain_page.dart +++ b/lib/wallet_settings/keychain_page.dart @@ -736,7 +736,7 @@ class _SuccessDialog extends StatelessWidget { @override Widget build(BuildContext context) { - Widget dialogContent = Padding( + final dialogContent = Padding( padding: const EdgeInsets.all(24), child: Column( mainAxisSize: MainAxisSize.min, @@ -803,7 +803,7 @@ class _ErrorDialog extends StatelessWidget { size: 48, ), const Gap(16), - BBText.title( + const BBText.title( 'Recovery Failed', textAlign: TextAlign.center, isBold: true, From 88c8a47e2832e7c97fe979a6982b844389f7681b Mon Sep 17 00:00:00 2001 From: ethicnology Date: Tue, 11 Feb 2025 15:09:33 -0500 Subject: [PATCH 179/401] refactor: rename key server env --- .env.template | 2 +- lib/_pkg/consts/configs.dart | 3 ++- lib/wallet_settings/bloc/keychain_cubit.dart | 6 +++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.env.template b/.env.template index 7b17549f5..89ee91b5b 100644 --- a/.env.template +++ b/.env.template @@ -1,3 +1,3 @@ -KEYCHAIN_API=https://keychain.bullbitcoin.dev +KEY_SERVER=https://keychain.bullbitcoin.dev GOOGLE_DRIVE_CLIENT_ID=249304738825-k5vfkegjtqv85pffdmcdr1hv6chusth6.apps.googleusercontent.com GOOGLE_DRIVE_URL_SCHEME=com.googleusercontent.apps.249304738825-k5vfkegjtqv85pffdmcdr1hv6chusth6 \ No newline at end of file diff --git a/lib/_pkg/consts/configs.dart b/lib/_pkg/consts/configs.dart index f618cbdb5..fb36b4d66 100644 --- a/lib/_pkg/consts/configs.dart +++ b/lib/_pkg/consts/configs.dart @@ -2,7 +2,6 @@ import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:lwk/lwk.dart' as lwk; void setupConfigs() {} -final keychainapi = dotenv.env['KEYCHAIN_API'] ?? 'http://localhost:3000'; final bbmempoolapi = dotenv.env['BB_MEMPOOL_API'] ?? 'mempool.bullbitcoin.com'; final openmempoolapi = dotenv.env['MEMPOOL_API'] ?? 'mempool.space'; final bbexchangeapi = dotenv.env['BB_API'] ?? 'api.bullbitcoin.com/price'; @@ -32,4 +31,6 @@ const liquidMainnetAssetId = lwk.lBtcAssetId; const liquidTestnetAssetId = lwk.lTestAssetId; //Backups +final keyServerUrl = dotenv.env['KEY_SERVER'] ?? 'http://localhost:3000'; + const defaultBackupPath = 'backups'; //todo; create a better folder structure diff --git a/lib/wallet_settings/bloc/keychain_cubit.dart b/lib/wallet_settings/bloc/keychain_cubit.dart index 365f3a45f..e1ae9d9c9 100644 --- a/lib/wallet_settings/bloc/keychain_cubit.dart +++ b/lib/wallet_settings/bloc/keychain_cubit.dart @@ -110,7 +110,7 @@ class KeychainCubit extends Cubit { Future secureKey() async { try { emit(state.copyWith(loading: true, error: '')); - await KeyService(keyServer: Uri.parse(keychainapi)).storeBackupKey( + await KeyService(keyServer: Uri.parse(keyServerUrl)).storeBackupKey( backupId: state.backupId, password: state.tempSecret, backupKey: HEX.decode(state.backupKey), @@ -160,12 +160,12 @@ class KeychainCubit extends Cubit { try { emit(state.copyWith(loading: true, error: '')); - if (keychainapi.isEmpty) { + if (keyServerUrl.isEmpty) { emit(state.copyWith(loading: false, error: 'keychain api is not set')); return; } final backupKey = - await KeyService(keyServer: Uri.parse(keychainapi)).recoverBackupKey( + await KeyService(keyServer: Uri.parse(keyServerUrl)).recoverBackupKey( backupId: state.backupId, password: state.secret, salt: state.backupSalt, From f5f40c105a3ca8a69452cc58c8113c2f81d49e3f Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Tue, 11 Feb 2025 15:10:12 -0500 Subject: [PATCH 180/401] refactor: remove unused words.dart file & added common password constants --- lib/_pkg/consts/passwords.dart | 1002 ++++++++++++++++++++++++++++++++ lib/_pkg/consts/words.dart | 0 2 files changed, 1002 insertions(+) create mode 100644 lib/_pkg/consts/passwords.dart delete mode 100644 lib/_pkg/consts/words.dart diff --git a/lib/_pkg/consts/passwords.dart b/lib/_pkg/consts/passwords.dart new file mode 100644 index 000000000..1466ce159 --- /dev/null +++ b/lib/_pkg/consts/passwords.dart @@ -0,0 +1,1002 @@ +const List passwordBlacklist = [ + "123456", + "password", + "12345678", + "qwerty", + "123456789", + "12345", + "1234", + "111111", + "1234567", + "dragon", + "123123", + "baseball", + "abc123", + "football", + "monkey", + "letmein", + "696969", + "shadow", + "master", + "666666", + "qwertyuiop", + "123321", + "mustang", + "1234567890", + "michael", + "654321", + "pussy", + "superman", + "1qaz2wsx", + "7777777", + "fuckyou", + "121212", + "000000", + "qazwsx", + "123qwe", + "killer", + "trustno1", + "jordan", + "jennifer", + "zxcvbnm", + "asdfgh", + "hunter", + "buster", + "soccer", + "harley", + "batman", + "andrew", + "tigger", + "sunshine", + "iloveyou", + "fuckme", + "2000", + "charlie", + "robert", + "thomas", + "hockey", + "ranger", + "daniel", + "starwars", + "klaster", + "112233", + "george", + "asshole", + "computer", + "michelle", + "jessica", + "pepper", + "1111", + "zxcvbn", + "555555", + "11111111", + "131313", + "freedom", + "777777", + "pass", + "fuck", + "maggie", + "159753", + "aaaaaa", + "ginger", + "princess", + "joshua", + "cheese", + "amanda", + "summer", + "love", + "ashley", + "6969", + "nicole", + "chelsea", + "biteme", + "matthew", + "access", + "yankees", + "987654321", + "dallas", + "austin", + "thunder", + "taylor", + "matrix", + "william", + "corvette", + "hello", + "martin", + "heather", + "secret", + "fucker", + "merlin", + "diamond", + "1234qwer", + "gfhjkm", + "hammer", + "silver", + "222222", + "88888888", + "anthony", + "justin", + "test", + "bailey", + "q1w2e3r4t5", + "patrick", + "internet", + "scooter", + "orange", + "11111", + "golfer", + "cookie", + "richard", + "samantha", + "bigdog", + "guitar", + "jackson", + "whatever", + "mickey", + "chicken", + "sparky", + "snoopy", + "maverick", + "phoenix", + "camaro", + "sexy", + "peanut", + "morgan", + "welcome", + "falcon", + "cowboy", + "ferrari", + "samsung", + "andrea", + "smokey", + "steelers", + "joseph", + "mercedes", + "dakota", + "arsenal", + "eagles", + "melissa", + "boomer", + "booboo", + "spider", + "nascar", + "monster", + "tigers", + "yellow", + "xxxxxx", + "123123123", + "gateway", + "marina", + "diablo", + "bulldog", + "qwer1234", + "compaq", + "purple", + "hardcore", + "banana", + "junior", + "hannah", + "123654", + "porsche", + "lakers", + "iceman", + "money", + "cowboys", + "987654", + "london", + "tennis", + "999999", + "ncc1701", + "coffee", + "scooby", + "0000", + "miller", + "boston", + "q1w2e3r4", + "fuckoff", + "brandon", + "yamaha", + "chester", + "mother", + "forever", + "johnny", + "edward", + "333333", + "oliver", + "redsox", + "player", + "nikita", + "knight", + "fender", + "barney", + "midnight", + "please", + "brandy", + "chicago", + "badboy", + "iwantu", + "slayer", + "rangers", + "charles", + "angel", + "flower", + "bigdaddy", + "rabbit", + "wizard", + "bigdick", + "jasper", + "enter", + "rachel", + "chris", + "steven", + "winner", + "adidas", + "victoria", + "natasha", + "1q2w3e4r", + "jasmine", + "winter", + "prince", + "panties", + "marine", + "ghbdtn", + "fishing", + "cocacola", + "casper", + "james", + "232323", + "raiders", + "888888", + "marlboro", + "gandalf", + "asdfasdf", + "crystal", + "87654321", + "12344321", + "sexsex", + "golden", + "blowme", + "bigtits", + "8675309", + "panther", + "lauren", + "angela", + "bitch", + "spanky", + "thx1138", + "angels", + "madison", + "winston", + "shannon", + "mike", + "toyota", + "blowjob", + "jordan23", + "canada", + "sophie", + "Password", + "apples", + "dick", + "tiger", + "razz", + "123abc", + "pokemon", + "qazxsw", + "55555", + "qwaszx", + "muffin", + "johnson", + "murphy", + "cooper", + "jonathan", + "liverpoo", + "david", + "danielle", + "159357", + "jackie", + "1990", + "123456a", + "789456", + "turtle", + "horny", + "abcd1234", + "scorpion", + "qazwsxedc", + "101010", + "butter", + "carlos", + "password1", + "dennis", + "slipknot", + "qwerty123", + "booger", + "asdf", + "1991", + "black", + "startrek", + "12341234", + "cameron", + "newyork", + "rainbow", + "nathan", + "john", + "1992", + "rocket", + "viking", + "redskins", + "butthead", + "asdfghjkl", + "1212", + "sierra", + "peaches", + "gemini", + "doctor", + "wilson", + "sandra", + "helpme", + "qwertyui", + "victor", + "florida", + "dolphin", + "pookie", + "captain", + "tucker", + "blue", + "liverpool", + "theman", + "bandit", + "dolphins", + "maddog", + "packers", + "jaguar", + "lovers", + "nicholas", + "united", + "tiffany", + "maxwell", + "zzzzzz", + "nirvana", + "jeremy", + "suckit", + "stupid", + "porn", + "monica", + "elephant", + "giants", + "jackass", + "hotdog", + "rosebud", + "success", + "debbie", + "mountain", + "444444", + "xxxxxxxx", + "warrior", + "1q2w3e4r5t", + "q1w2e3", + "123456q", + "albert", + "metallic", + "lucky", + "azerty", + "7777", + "shithead", + "alex", + "bond007", + "alexis", + "1111111", + "samson", + "5150", + "willie", + "scorpio", + "bonnie", + "gators", + "benjamin", + "voodoo", + "driver", + "dexter", + "2112", + "jason", + "calvin", + "freddy", + "212121", + "creative", + "12345a", + "sydney", + "rush2112", + "1989", + "asdfghjk", + "red123", + "bubba", + "4815162342", + "passw0rd", + "trouble", + "gunner", + "happy", + "fucking", + "gordon", + "legend", + "jessie", + "stella", + "qwert", + "eminem", + "arthur", + "apple", + "nissan", + "bullshit", + "bear", + "america", + "1qazxsw2", + "nothing", + "parker", + "4444", + "rebecca", + "qweqwe", + "garfield", + "01012011", + "beavis", + "69696969", + "jack", + "asdasd", + "december", + "2222", + "102030", + "252525", + "11223344", + "magic", + "apollo", + "skippy", + "315475", + "girls", + "kitten", + "golf", + "copper", + "braves", + "shelby", + "godzilla", + "beaver", + "fred", + "tomcat", + "august", + "buddy", + "airborne", + "1993", + "1988", + "lifehack", + "qqqqqq", + "brooklyn", + "animal", + "platinum", + "phantom", + "online", + "xavier", + "darkness", + "blink182", + "power", + "fish", + "green", + "789456123", + "voyager", + "police", + "travis", + "12qwaszx", + "heaven", + "snowball", + "lover", + "abcdef", + "00000", + "pakistan", + "007007", + "walter", + "playboy", + "blazer", + "cricket", + "sniper", + "hooters", + "donkey", + "willow", + "loveme", + "saturn", + "therock", + "redwings", + "bigboy", + "pumpkin", + "trinity", + "williams", + "tits", + "nintendo", + "digital", + "destiny", + "topgun", + "runner", + "marvin", + "guinness", + "chance", + "bubbles", + "testing", + "fire", + "november", + "minecraft", + "asdf1234", + "lasvegas", + "sergey", + "broncos", + "cartman", + "private", + "celtic", + "birdie", + "little", + "cassie", + "babygirl", + "donald", + "beatles", + "1313", + "dickhead", + "family", + "12121212", + "school", + "louise", + "gabriel", + "eclipse", + "fluffy", + "147258369", + "lol123", + "explorer", + "beer", + "nelson", + "flyers", + "spencer", + "scott", + "lovely", + "gibson", + "doggie", + "cherry", + "andrey", + "snickers", + "buffalo", + "pantera", + "metallica", + "member", + "carter", + "qwertyu", + "peter", + "alexande", + "steve", + "bronco", + "paradise", + "goober", + "5555", + "samuel", + "montana", + "mexico", + "dreams", + "michigan", + "cock", + "carolina", + "yankee", + "friends", + "magnum", + "surfer", + "poopoo", + "maximus", + "genius", + "cool", + "vampire", + "lacrosse", + "asd123", + "aaaa", + "christin", + "kimberly", + "speedy", + "sharon", + "carmen", + "111222", + "kristina", + "sammy", + "racing", + "ou812", + "sabrina", + "horses", + "0987654321", + "qwerty1", + "pimpin", + "baby", + "stalker", + "enigma", + "147147", + "star", + "poohbear", + "boobies", + "147258", + "simple", + "bollocks", + "12345q", + "marcus", + "brian", + "1987", + "qweasdzxc", + "drowssap", + "hahaha", + "caroline", + "barbara", + "dave", + "viper", + "drummer", + "action", + "einstein", + "bitches", + "genesis", + "hello1", + "scotty", + "friend", + "forest", + "010203", + "hotrod", + "google", + "vanessa", + "spitfire", + "badger", + "maryjane", + "friday", + "alaska", + "1232323q", + "tester", + "jester", + "jake", + "champion", + "billy", + "147852", + "rock", + "hawaii", + "badass", + "chevy", + "420420", + "walker", + "stephen", + "eagle1", + "bill", + "1986", + "october", + "gregory", + "svetlana", + "pamela", + "1984", + "music", + "shorty", + "westside", + "stanley", + "diesel", + "courtney", + "242424", + "kevin", + "porno", + "hitman", + "boobs", + "mark", + "12345qwert", + "reddog", + "frank", + "qwe123", + "popcorn", + "patricia", + "aaaaaaaa", + "1969", + "teresa", + "mozart", + "buddha", + "anderson", + "paul", + "melanie", + "abcdefg", + "security", + "lucky1", + "lizard", + "denise", + "3333", + "a12345", + "123789", + "ruslan", + "stargate", + "simpsons", + "scarface", + "eagle", + "123456789a", + "thumper", + "olivia", + "naruto", + "1234554321", + "general", + "cherokee", + "a123456", + "vincent", + "Usuckballz1", + "spooky", + "qweasd", + "cumshot", + "free", + "frankie", + "douglas", + "death", + "1980", + "loveyou", + "kitty", + "kelly", + "veronica", + "suzuki", + "semperfi", + "penguin", + "mercury", + "liberty", + "spirit", + "scotland", + "natalie", + "marley", + "vikings", + "system", + "sucker", + "king", + "allison", + "marshall", + "1979", + "098765", + "qwerty12", + "hummer", + "adrian", + "1985", + "vfhbyf", + "sandman", + "rocky", + "leslie", + "antonio", + "98765432", + "4321", + "softball", + "passion", + "mnbvcxz", + "bastard", + "passport", + "horney", + "rascal", + "howard", + "franklin", + "bigred", + "assman", + "alexander", + "homer", + "redrum", + "jupiter", + "claudia", + "55555555", + "141414", + "zaq12wsx", + "shit", + "patches", + "nigger", + "cunt", + "raider", + "infinity", + "andre", + "54321", + "galore", + "college", + "russia", + "kawasaki", + "bishop", + "77777777", + "vladimir", + "money1", + "freeuser", + "wildcats", + "francis", + "disney", + "budlight", + "brittany", + "1994", + "00000000", + "sweet", + "oksana", + "honda", + "domino", + "bulldogs", + "brutus", + "swordfis", + "norman", + "monday", + "jimmy", + "ironman", + "ford", + "fantasy", + "9999", + "7654321", + "PASSWORD", + "hentai", + "duncan", + "cougar", + "1977", + "jeffrey", + "house", + "dancer", + "brooke", + "timothy", + "super", + "marines", + "justice", + "digger", + "connor", + "patriots", + "karina", + "202020", + "molly", + "everton", + "tinker", + "alicia", + "rasdzv3", + "poop", + "pearljam", + "stinky", + "naughty", + "colorado", + "123123a", + "water", + "test123", + "ncc1701d", + "motorola", + "ireland", + "asdfg", + "slut", + "matt", + "houston", + "boogie", + "zombie", + "accord", + "vision", + "bradley", + "reggie", + "kermit", + "froggy", + "ducati", + "avalon", + "6666", + "9379992", + "sarah", + "saints", + "logitech", + "chopper", + "852456", + "simpson", + "madonna", + "juventus", + "claire", + "159951", + "zachary", + "yfnfif", + "wolverin", + "warcraft", + "hello123", + "extreme", + "penis", + "peekaboo", + "fireman", + "eugene", + "brenda", + "123654789", + "russell", + "panthers", + "georgia", + "smith", + "skyline", + "jesus", + "elizabet", + "spiderma", + "smooth", + "pirate", + "empire", + "bullet", + "8888", + "virginia", + "valentin", + "psycho", + "predator", + "arizona", + "134679", + "mitchell", + "alyssa", + "vegeta", + "titanic", + "christ", + "goblue", + "fylhtq", + "wolf", + "mmmmmm", + "kirill", + "indian", + "hiphop", + "baxter", + "awesome", + "people", + "danger", + "roland", + "mookie", + "741852963", + "1111111111", + "dreamer", + "bambam", + "arnold", + "1981", + "skipper", + "serega", + "rolltide", + "elvis", + "changeme", + "simon", + "1q2w3e", + "lovelove", + "fktrcfylh", + "denver", + "tommy", + "mine", + "loverboy", + "hobbes", + "happy1", + "alison", + "nemesis", + "chevelle", + "cardinal", + "burton", + "wanker", + "picard", + "151515", + "tweety", + "michael1", + "147852369", + "12312", + "xxxx", + "windows", + "turkey", + "456789", + "1974", + "vfrcbv", + "sublime", + "1975", + "galina", + "bobby", + "newport", + "manutd", + "daddy", + "american", + "alexandr", + "1966", + "victory", + "rooster", + "qqq111", + "madmax", + "electric", + "bigcock", + "a1b2c3", + "wolfpack", + "spring", + "phpbb", + "lalala", + "suckme", + "spiderman", + "eric", + "darkside", + "classic", + "raptor", + "123456789q", + "hendrix", + "1982", + "wombat", + "avatar", + "alpha", + "zxc123", + "crazy", + "hard", + "england", + "brazil", + "1978", + "01011980", + "wildcat", + "polina", + "freepass", +]; diff --git a/lib/_pkg/consts/words.dart b/lib/_pkg/consts/words.dart deleted file mode 100644 index e69de29bb..000000000 From c2c50890598eb39649f698a142daf480d82ed59d Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Tue, 11 Feb 2025 15:10:30 -0500 Subject: [PATCH 181/401] feat: enhance KeychainState validation with regex checks for PIN and password complexity --- lib/wallet_settings/bloc/keychain_state.dart | 46 ++++++++++++++++++-- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/lib/wallet_settings/bloc/keychain_state.dart b/lib/wallet_settings/bloc/keychain_state.dart index 00f46f0dd..dc9604676 100644 --- a/lib/wallet_settings/bloc/keychain_state.dart +++ b/lib/wallet_settings/bloc/keychain_state.dart @@ -1,3 +1,4 @@ +import 'package:bb_mobile/_pkg/consts/passwords.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; part 'keychain_state.freezed.dart'; @@ -28,14 +29,51 @@ class KeychainState with _$KeychainState { const KeychainState._(); - String displayPin() => List.filled(secret.length, 'x').join(''); + String displayPin() => 'x' * secret.length; - bool get isValid => inputType == KeyChainInputType.pin - ? secret.length == 6 - : secret.length >= 6; + static final _pinRegex = RegExp(r'^[0-9]{6,7}$'); + static final _uppercaseRegex = RegExp(r'(?=(?:.*[A-Z]){2})'); + static final _numbersRegex = RegExp(r'(?=(?:.*\d){2})'); + String? getValidationError() { + if (secret.isEmpty) return null; + + if (inputType == KeyChainInputType.pin) { + if (!_pinRegex.hasMatch(secret)) { + return secret.length < 6 + ? 'PIN must be at least 6 digits long' + : 'PIN must be less than 8 digits'; + } + return validateSecret(secret) ? 'PIN contains a common pattern' : null; + } + + if (secret.length < 7) { + return 'Password must be at greater than 6 characters long'; + } + if (!_uppercaseRegex.hasMatch(secret)) { + return 'Password must contain at least 2 uppercase letters'; + } + if (!_numbersRegex.hasMatch(secret)) { + return 'Password must contain at least 2 numbers'; + } + return validateSecret(secret) + ? 'Password contains a common word or pattern' + : null; + } + + bool get isValid => getValidationError() == null; bool get showButton => isValid; bool get hasError => error.isNotEmpty; bool get isRecovering => pageState == KeyChainPageState.recovery; bool get canRecover => backupId.isNotEmpty && isValid && !loading; + + // Cache the compiled regex patterns + static final _blacklistPattern = RegExp( + r'\b(' + + passwordBlacklist.map((word) => RegExp.escape(word)).join('|') + + r')\b', + caseSensitive: false, + ); + + bool validateSecret(String secret) => _blacklistPattern.hasMatch(secret); } From 9bc1fc0f71aebb471d343f384c8c7471dfc7c089 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Tue, 11 Feb 2025 15:10:42 -0500 Subject: [PATCH 182/401] fix: update key length validation to require a minimum of 7 characters --- lib/wallet_settings/bloc/keychain_cubit.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/wallet_settings/bloc/keychain_cubit.dart b/lib/wallet_settings/bloc/keychain_cubit.dart index e1ae9d9c9..b48f24941 100644 --- a/lib/wallet_settings/bloc/keychain_cubit.dart +++ b/lib/wallet_settings/bloc/keychain_cubit.dart @@ -69,7 +69,7 @@ class KeychainCubit extends Cubit { } void keyPressed(String key) { - if (state.secret.length >= 6) return; + if (state.secret.length >= 7) return; emit( state.copyWith( secret: state.secret + key, From 71aa53e15cdfb362c7044c9272ac73625507df1a Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Tue, 11 Feb 2025 15:10:49 -0500 Subject: [PATCH 183/401] feat: enhance error handling in keychain recovery and input fields with validation messages --- lib/wallet_settings/keychain_page.dart | 126 +++++++++++++++---------- 1 file changed, 78 insertions(+), 48 deletions(-) diff --git a/lib/wallet_settings/keychain_page.dart b/lib/wallet_settings/keychain_page.dart index 2fd3dc66e..edda6c082 100644 --- a/lib/wallet_settings/keychain_page.dart +++ b/lib/wallet_settings/keychain_page.dart @@ -141,8 +141,6 @@ class _Screen extends StatelessWidget { jsonEncode(encryptedBackup), state.backupKey, ); - // Remove immediate success dialog - will show after successful recovery - // showDialog(...) } } @@ -152,6 +150,7 @@ class _Screen extends StatelessWidget { barrierDismissible: false, builder: (context) => _ErrorDialog( error: state.error, + isRecovery: state.pageState == KeyChainPageState.recovery, ), ); } @@ -325,39 +324,52 @@ class _RecoveryPage extends StatelessWidget { class _PinField extends StatelessWidget { @override Widget build(BuildContext context) { - final pin = context.select((KeychainCubit x) => x.state.displayPin()); - return Center( - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const SizedBox(width: 40), - Expanded( - child: Center( - child: BBText.titleLarge( - pin, - isBold: true, + final state = context.select((KeychainCubit x) => x.state); + final error = state.getValidationError(); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox(width: 40), + Expanded( + child: Center( + child: BBText.titleLarge( + state.displayPin(), + isBold: true, + ), + ), ), - ), + SizedBox( + width: 40, + child: IconButton( + iconSize: 32, + color: state.secret.isEmpty + ? context.colour.surface + : context.colour.onPrimaryContainer, + splashColor: Colors.transparent, + onPressed: () { + SystemSound.play(SystemSoundType.click); + HapticFeedback.mediumImpact(); + context.read().backspacePressed(); + }, + icon: const FaIcon(FontAwesomeIcons.deleteLeft), + ), + ), + ], ), - SizedBox( - width: 40, - child: IconButton( - iconSize: 32, - color: pin.isEmpty - ? context.colour.surface - : context.colour.onPrimaryContainer, - splashColor: Colors.transparent, - onPressed: () { - SystemSound.play(SystemSoundType.click); - HapticFeedback.mediumImpact(); - - context.read().backspacePressed(); - }, - icon: const FaIcon(FontAwesomeIcons.deleteLeft), + ), + if (error != null) + Padding( + padding: const EdgeInsets.only(top: 8), + child: Center( + child: BBText.errorSmall(error), ), ), - ], - ), + ], ); } } @@ -365,18 +377,30 @@ class _PinField extends StatelessWidget { class _PasswordField extends StatelessWidget { @override Widget build(BuildContext context) { - final secret = context.select((KeychainCubit x) => x.state.secret); - final isObscure = context.select((KeychainCubit x) => x.state.obscure); - return BBTextInput.bigWithIcon( - value: secret, - onChanged: (value) => context.read().updateInput(value), - obscure: isObscure, - hint: 'Enter your password', - rightIcon: Icon( - isObscure ? Icons.visibility_off : Icons.visibility, - color: context.colour.onPrimaryContainer, - ), - onRightTap: () => context.read().clickObscure(), + final state = context.select((KeychainCubit x) => x.state); + final error = state.getValidationError(); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + BBTextInput.bigWithIcon( + value: state.secret, + onChanged: (value) => + context.read().updateInput(value), + obscure: state.obscure, + hint: 'Enter your password', + rightIcon: Icon( + state.obscure ? Icons.visibility_off : Icons.visibility, + color: context.colour.onPrimaryContainer, + ), + onRightTap: () => context.read().clickObscure(), + ), + if (error != null) + Padding( + padding: const EdgeInsets.only(top: 8, left: 8), + child: BBText.errorSmall(error), + ), + ], ); } } @@ -785,9 +809,9 @@ class _SuccessDialog extends StatelessWidget { } class _ErrorDialog extends StatelessWidget { - const _ErrorDialog({required this.error}); + const _ErrorDialog({required this.error, this.isRecovery = false}); final String error; - + final bool isRecovery; @override Widget build(BuildContext context) { return Dialog( @@ -803,8 +827,8 @@ class _ErrorDialog extends StatelessWidget { size: 48, ), const Gap(16), - const BBText.title( - 'Recovery Failed', + BBText.title( + isRecovery ? 'Recovery Failed' : 'Backup Failed', textAlign: TextAlign.center, isBold: true, ), @@ -822,7 +846,13 @@ class _ErrorDialog extends StatelessWidget { borderRadius: BorderRadius.circular(8), ), ), - child: const Text('Close'), + child: Text( + 'Close', + style: context.font.bodyMedium!.copyWith( + color: Colors.white, + fontWeight: FontWeight.w900, + ), + ), ), ], ), From 2da0088c6307dd0d8c2a89431aebff1239e46192 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Wed, 12 Feb 2025 12:42:29 -0500 Subject: [PATCH 184/401] refactor: update app name from 'bb_mobile' to 'Bull Bitcoin' in Info.plist --- ios/Runner/Info.plist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 2168fe202..1b9450f6d 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -15,7 +15,7 @@ CFBundleInfoDictionaryVersion 6.0 CFBundleName - bb_mobile + Bull Bitcoin CFBundlePackageType APPL CFBundleShortVersionString From e33fa10c9920dcde1492d9f9235b69fbbb12f3da Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Wed, 12 Feb 2025 12:42:37 -0500 Subject: [PATCH 185/401] refactor: remove bip85 dependency from Podfile.lock --- ios/Podfile.lock | 6 ------ 1 file changed, 6 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index ea32cee0e..258bad0ca 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -7,8 +7,6 @@ PODS: - AppAuth/Core - bdk_flutter (0.31.2): - Flutter - - bip85 (0.0.1): - - Flutter - boltz (0.1.6) - DKImagePickerController/Core (4.3.9): - DKImagePickerController/ImageDataManager @@ -101,7 +99,6 @@ PODS: DEPENDENCIES: - bdk_flutter (from `.symlinks/plugins/bdk_flutter/ios`) - - bip85 (from `.symlinks/plugins/bip85/ios`) - boltz (from `.symlinks/plugins/boltz/ios`) - document_file_save_plus (from `.symlinks/plugins/document_file_save_plus/ios`) - file_picker (from `.symlinks/plugins/file_picker/ios`) @@ -137,8 +134,6 @@ SPEC REPOS: EXTERNAL SOURCES: bdk_flutter: :path: ".symlinks/plugins/bdk_flutter/ios" - bip85: - :path: ".symlinks/plugins/bip85/ios" boltz: :path: ".symlinks/plugins/boltz/ios" document_file_save_plus: @@ -181,7 +176,6 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: AppAuth: d4f13a8fe0baf391b2108511793e4b479691fb73 bdk_flutter: fb57a7400a7f3f181c5977bcdc2a5ef347ae4e7f - bip85: f656a7e6b23afda4960efb11c87d51d68e8be3db boltz: 6388ec2412f3753b63a9e65c97f87ea26f43bddc DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 From ea2b0d870bea7e741eeb41cdbcd691085dace8bf Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Wed, 12 Feb 2025 12:42:45 -0500 Subject: [PATCH 186/401] feat: initialize KeyService and improve error handling for keychain API --- lib/wallet_settings/bloc/keychain_cubit.dart | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/lib/wallet_settings/bloc/keychain_cubit.dart b/lib/wallet_settings/bloc/keychain_cubit.dart index b48f24941..2f8132cbb 100644 --- a/lib/wallet_settings/bloc/keychain_cubit.dart +++ b/lib/wallet_settings/bloc/keychain_cubit.dart @@ -8,6 +8,17 @@ import 'package:recoverbull/recoverbull.dart'; class KeychainCubit extends Cubit { KeychainCubit() : super(const KeychainState()) { shuffleAndEmit(); + _initKeyService(); + } + + late final KeyService _keyService; + + void _initKeyService() { + if (keyServerUrl.isEmpty) { + emit(state.copyWith(error: 'keychain api is not set')); + return; + } + _keyService = KeyService(keyServer: Uri.parse(keyServerUrl)); } void shuffleAndEmit() { @@ -110,7 +121,7 @@ class KeychainCubit extends Cubit { Future secureKey() async { try { emit(state.copyWith(loading: true, error: '')); - await KeyService(keyServer: Uri.parse(keyServerUrl)).storeBackupKey( + await _keyService.storeBackupKey( backupId: state.backupId, password: state.tempSecret, backupKey: HEX.decode(state.backupKey), @@ -160,12 +171,7 @@ class KeychainCubit extends Cubit { try { emit(state.copyWith(loading: true, error: '')); - if (keyServerUrl.isEmpty) { - emit(state.copyWith(loading: false, error: 'keychain api is not set')); - return; - } - final backupKey = - await KeyService(keyServer: Uri.parse(keyServerUrl)).recoverBackupKey( + final backupKey = await _keyService.recoverBackupKey( backupId: state.backupId, password: state.secret, salt: state.backupSalt, From bfcf046e757d67e21f8b65d9291f154d3f831236 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Sun, 16 Feb 2025 07:56:08 -0500 Subject: [PATCH 187/401] refactor: remove unused Crypto class and related encryption methods --- lib/_pkg/crypto.dart | 57 -------------------------------------------- 1 file changed, 57 deletions(-) delete mode 100644 lib/_pkg/crypto.dart diff --git a/lib/_pkg/crypto.dart b/lib/_pkg/crypto.dart deleted file mode 100644 index d2c78fca2..000000000 --- a/lib/_pkg/crypto.dart +++ /dev/null @@ -1,57 +0,0 @@ -import 'dart:convert'; -import 'dart:math'; -import 'dart:typed_data'; - -import 'package:hex/hex.dart'; -import 'package:pointycastle/api.dart'; -import 'package:pointycastle/digests/sha256.dart'; -import 'package:pointycastle/export.dart' as pc; - -class Crypto { - static String aesEncrypt(String plainText, String key) { - final keyBytes = Uint8List.fromList(HEX.decode(key)); - final iv = generateRandomBytes(16); - final params = PaddedBlockCipherParameters( - ParametersWithIV(KeyParameter(keyBytes), iv), - null, - ); - final paddedBlockCipher = pc.PaddedBlockCipher('AES/CBC/PKCS7') - ..init(true, params); - - final input = Uint8List.fromList(utf8.encode(plainText)); - final encrypted = paddedBlockCipher.process(input); - - return '${base64Encode(iv)},${base64Encode(encrypted)}'; - } - - static String aesDecrypt(String encryptedBase64Text, String key) { - final keyBytes = Uint8List.fromList(HEX.decode(key)); - - final parts = encryptedBase64Text.split(','); - final iv = base64Decode(parts[0]); - final encrypted = base64Decode(parts[1]); - final params = PaddedBlockCipherParameters( - ParametersWithIV(KeyParameter(keyBytes), iv), - null, - ); - final paddedBlockCipher = pc.PaddedBlockCipher('AES/CBC/PKCS7') - ..init(false, params); - - final decrypted = paddedBlockCipher.process(encrypted); - - return utf8.decode(decrypted); - } - - static Uint8List generateRandomBytes(int length) { - final secureRandom = Random.secure(); - final randomIV = Uint8List(length); - for (int i = 0; i < length; i++) { - randomIV[i] = secureRandom.nextInt(256); - } - return randomIV; - } - - static List sha256(List input) { - return SHA256Digest().process(Uint8List.fromList(input)); - } -} From 380e26d4e06d90a4e1b14289f3223e2ed725453b Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Sun, 16 Feb 2025 07:56:40 -0500 Subject: [PATCH 188/401] feat: add support for lwk.Network in BBNetwork enum and simplify network parsing --- lib/_model/wallet.dart | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/lib/_model/wallet.dart b/lib/_model/wallet.dart index 1eaa5a624..5a510ce34 100644 --- a/lib/_model/wallet.dart +++ b/lib/_model/wallet.dart @@ -6,6 +6,7 @@ import 'package:bb_mobile/_model/swap.dart'; import 'package:bb_mobile/_model/transaction.dart'; import 'package:bdk_flutter/bdk_flutter.dart' as bdk; import 'package:crypto/crypto.dart'; +import 'package:lwk/lwk.dart' as lwk; import 'package:freezed_annotation/freezed_annotation.dart'; part 'wallet.freezed.dart'; @@ -17,13 +18,17 @@ enum BBNetwork { Mainnet; static BBNetwork fromString(String network) { - switch (network) { - case 'Testnet': - return BBNetwork.Testnet; - case 'Mainnet': - return BBNetwork.Mainnet; - default: - return BBNetwork.Mainnet; + return network.toLowerCase() == 'testnet' + ? BBNetwork.Testnet + : BBNetwork.Mainnet; + } + + lwk.Network toLwkNetwork() { + switch (this) { + case BBNetwork.Testnet: + return lwk.Network.testnet; + case BBNetwork.Mainnet: + return lwk.Network.mainnet; } } From 61eddaaa4cb26890ecc73dcf453c57202f9851b1 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Sun, 16 Feb 2025 07:56:54 -0500 Subject: [PATCH 189/401] feat: add publicDescriptors field to Backup model --- lib/_model/backup.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/_model/backup.dart b/lib/_model/backup.dart index b20b215c7..0e9c71583 100644 --- a/lib/_model/backup.dart +++ b/lib/_model/backup.dart @@ -14,6 +14,7 @@ class Backup with _$Backup { @Default('') String layer, @Default('') String type, @Default('') String script, + @Default('') String publicDescriptors, }) = _Backup; factory Backup.fromJson(Map json) => _$BackupFromJson(json); From fbb3225f7f1e6aece27cbe68f56e1f1139cafbe3 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Sun, 16 Feb 2025 07:57:10 -0500 Subject: [PATCH 190/401] feat: add support for publicDescriptors in BDKSensitiveCreate class --- lib/_pkg/wallet/bdk/sensitive_create.dart | 160 ++++++++++++---------- 1 file changed, 84 insertions(+), 76 deletions(-) diff --git a/lib/_pkg/wallet/bdk/sensitive_create.dart b/lib/_pkg/wallet/bdk/sensitive_create.dart index 634f73a9f..91b5208d3 100644 --- a/lib/_pkg/wallet/bdk/sensitive_create.dart +++ b/lib/_pkg/wallet/bdk/sensitive_create.dart @@ -259,97 +259,105 @@ class BDKSensitiveCreate { required BBWalletType walletType, required BBNetwork network, required WalletCreate walletCreate, + String? publicDescriptors, }) async { - final bdkMnemonic = await bdk.Mnemonic.fromString(seed.mnemonic); - final bdkNetwork = network == BBNetwork.Testnet - ? bdk.Network.testnet - : bdk.Network.bitcoin; - final rootXprv = await bdk.DescriptorSecretKey.create( - network: bdkNetwork, - mnemonic: bdkMnemonic, - password: passphrase, - ); - final networkPath = network == BBNetwork.Mainnet ? '0h' : '1h'; - const accountPath = '0h'; - // final sourceFingerprint = fing/erPrintFromXKeyDesc(bdkXpub84.asString()); final (sourceFingerprint, sfErr) = await getFingerprint( mnemonic: seed.mnemonic, passphrase: passphrase, ); - if (sfErr != null) { + if (sfErr != null || sourceFingerprint == null) { return (null, Err('Error Getting Fingerprint')); } + final bdkNetwork = network.toBdkNetwork(); bdk.Descriptor? internal; bdk.Descriptor? external; + if (publicDescriptors == null) { + final bdkMnemonic = await bdk.Mnemonic.fromString(seed.mnemonic); + + final rootXprv = await bdk.DescriptorSecretKey.create( + network: bdkNetwork, + mnemonic: bdkMnemonic, + password: passphrase, + ); + final networkPath = network == BBNetwork.Mainnet ? '0h' : '1h'; + const accountPath = '0h'; + + switch (scriptType) { + case ScriptType.bip84: + final mOnlybdkXpriv84 = await rootXprv.derive( + await bdk.DerivationPath.create( + path: 'm/84h/$networkPath/$accountPath', + ), + ); + + final bdkXpub84 = mOnlybdkXpriv84.toPublic(); - switch (scriptType) { - case ScriptType.bip84: - final mOnlybdkXpriv84 = await rootXprv.derive( - await bdk.DerivationPath.create( - path: 'm/84h/$networkPath/$accountPath', - ), - ); - - final bdkXpub84 = mOnlybdkXpriv84.toPublic(); - - internal = await bdk.Descriptor.newBip84Public( - publicKey: bdkXpub84, - fingerPrint: sourceFingerprint!, - network: bdkNetwork, - keychain: bdk.KeychainKind.internalChain, - ); - external = await bdk.Descriptor.newBip84Public( - publicKey: bdkXpub84, - fingerPrint: sourceFingerprint, - network: bdkNetwork, - keychain: bdk.KeychainKind.externalChain, - ); - case ScriptType.bip49: - final bdkXpriv49 = await rootXprv.derive( - await bdk.DerivationPath.create( - path: 'm/49h/$networkPath/$accountPath', - ), - ); - - final bdkXpub49 = bdkXpriv49.toPublic(); - internal = await bdk.Descriptor.newBip49Public( - publicKey: bdkXpub49, - fingerPrint: sourceFingerprint!, - network: bdkNetwork, - keychain: bdk.KeychainKind.internalChain, - ); - external = await bdk.Descriptor.newBip49Public( - publicKey: bdkXpub49, - fingerPrint: sourceFingerprint, - network: bdkNetwork, - keychain: bdk.KeychainKind.externalChain, - ); - case ScriptType.bip44: - final bdkXpriv44 = await rootXprv.derive( - await bdk.DerivationPath.create( - path: 'm/44h/$networkPath/$accountPath', - ), - ); - final bdkXpub44 = bdkXpriv44.toPublic(); - internal = await bdk.Descriptor.newBip44Public( - publicKey: bdkXpub44, - fingerPrint: sourceFingerprint!, - network: bdkNetwork, - keychain: bdk.KeychainKind.internalChain, - ); - external = await bdk.Descriptor.newBip44Public( - publicKey: bdkXpub44, - fingerPrint: sourceFingerprint, - network: bdkNetwork, - keychain: bdk.KeychainKind.externalChain, - ); + internal = await bdk.Descriptor.newBip84Public( + publicKey: bdkXpub84, + fingerPrint: sourceFingerprint, + network: bdkNetwork, + keychain: bdk.KeychainKind.internalChain, + ); + external = await bdk.Descriptor.newBip84Public( + publicKey: bdkXpub84, + fingerPrint: sourceFingerprint, + network: bdkNetwork, + keychain: bdk.KeychainKind.externalChain, + ); + case ScriptType.bip49: + final bdkXpriv49 = await rootXprv.derive( + await bdk.DerivationPath.create( + path: 'm/49h/$networkPath/$accountPath', + ), + ); + + final bdkXpub49 = bdkXpriv49.toPublic(); + internal = await bdk.Descriptor.newBip49Public( + publicKey: bdkXpub49, + fingerPrint: sourceFingerprint, + network: bdkNetwork, + keychain: bdk.KeychainKind.internalChain, + ); + external = await bdk.Descriptor.newBip49Public( + publicKey: bdkXpub49, + fingerPrint: sourceFingerprint, + network: bdkNetwork, + keychain: bdk.KeychainKind.externalChain, + ); + case ScriptType.bip44: + final bdkXpriv44 = await rootXprv.derive( + await bdk.DerivationPath.create( + path: 'm/44h/$networkPath/$accountPath', + ), + ); + final bdkXpub44 = bdkXpriv44.toPublic(); + internal = await bdk.Descriptor.newBip44Public( + publicKey: bdkXpub44, + fingerPrint: sourceFingerprint, + network: bdkNetwork, + keychain: bdk.KeychainKind.internalChain, + ); + external = await bdk.Descriptor.newBip44Public( + publicKey: bdkXpub44, + fingerPrint: sourceFingerprint, + network: bdkNetwork, + keychain: bdk.KeychainKind.externalChain, + ); + } + } else { + external = await bdk.Descriptor.create( + descriptor: publicDescriptors.split(',')[0], + network: bdkNetwork, + ); + internal = await bdk.Descriptor.create( + descriptor: publicDescriptors.split(',')[0], + network: bdkNetwork, + ); } final descHashId = createDescriptorHashId(external.asString()).substring(0, 12); - // final type = isImported ? BBWalletType.words : BBWalletType.newSeed; - var wallet = Wallet( id: descHashId, externalPublicDescriptor: external.asString(), From 90c9819c0dbf815975ee64ec42936a3eef6758bb Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Sun, 16 Feb 2025 07:57:34 -0500 Subject: [PATCH 191/401] refactor: simplify network conversion in LWKSensitiveCreate class --- lib/_pkg/wallet/lwk/sensitive_create.dart | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/_pkg/wallet/lwk/sensitive_create.dart b/lib/_pkg/wallet/lwk/sensitive_create.dart index 651566c91..2deb03f8b 100644 --- a/lib/_pkg/wallet/lwk/sensitive_create.dart +++ b/lib/_pkg/wallet/lwk/sensitive_create.dart @@ -26,11 +26,8 @@ class LWKSensitiveCreate { required BBWalletType walletType, required BBNetwork network, required WalletCreate walletCreate, - // bool isImported, }) async { - final lwkNetwork = network == BBNetwork.Mainnet - ? lwk.Network.mainnet - : lwk.Network.testnet; + final lwkNetwork = network.toLwkNetwork(); final lwk.Descriptor descriptor = await lwk.Descriptor.newConfidential( network: lwkNetwork, mnemonic: seed.mnemonic, From e8f34f90f933ef10e0e238244ff5abec8106afab Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Sun, 16 Feb 2025 10:32:09 -0500 Subject: [PATCH 192/401] feat: pubspec.lock updated --- pubspec.lock | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/pubspec.lock b/pubspec.lock index 2c701b38a..6c9d62ebb 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -78,6 +78,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.31.2" + bech32: + dependency: transitive + description: + name: bech32 + sha256: "156cbace936f7720c79a79d16a03efad343b1ef17106716e04b8b8e39f99f7f7" + url: "https://pub.dev" + source: hosted + version: "0.2.2" bip32: dependency: transitive description: @@ -86,6 +94,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + bip340: + dependency: transitive + description: + name: bip340 + sha256: b7bcd70a860e605046006adaa72bc4f7453f4d31d7ba74a4ad9d5de387a0fc0b + url: "https://pub.dev" + source: hosted + version: "0.3.0" bip39_mnemonic: dependency: transitive description: @@ -375,6 +391,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + elliptic: + dependency: transitive + description: + name: elliptic + sha256: "0c303d810603953a65dc39c4c542fb7538defd9e212403c54c266140819523b6" + url: "https://pub.dev" + source: hosted + version: "0.3.11" extension_google_sign_in_as_googleapis_auth: dependency: "direct main" description: @@ -1077,6 +1101,15 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.2" + nostr: + dependency: transitive + description: + path: "." + ref: develop + resolved-ref: "6197278729b2806f116e25e290e92019cf408a23" + url: "https://github.com/ethicnology/dart-nostr" + source: git + version: "1.5.0" oktoast: dependency: "direct main" description: @@ -1323,7 +1356,7 @@ packages: description: path: "." ref: HEAD - resolved-ref: c8c7dc5200d9b1ffacbdd0b2c3f9ca34abffab03 + resolved-ref: "6941511cce648d3478792f696874e057cac96ee7" url: "https://github.com/SatoshiPortal/recoverbull-client-dart.git" source: git version: "1.0.0" From 72aafd27b46fee0751e7cff40369f978e5f55e11 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Sun, 16 Feb 2025 12:59:37 -0500 Subject: [PATCH 193/401] fix: update wallet parameter type to nullable --- lib/routes.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/routes.dart b/lib/routes.dart index f4ea7264b..d9d68946b 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -236,7 +236,7 @@ GoRouter setupRouter() => GoRouter( GoRoute( path: 'recover-encrypted', builder: (context, state) => EncryptedVaultRecoverPage( - wallet: state.extra! as String, + wallet: state.extra as String?, ), routes: [ GoRoute( From accd2331c7c78de3ea18027890501d4163d8a5ed Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Sun, 16 Feb 2025 12:59:46 -0500 Subject: [PATCH 194/401] feat: add key server public key configuration --- lib/_pkg/consts/configs.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/_pkg/consts/configs.dart b/lib/_pkg/consts/configs.dart index fb36b4d66..b060c4014 100644 --- a/lib/_pkg/consts/configs.dart +++ b/lib/_pkg/consts/configs.dart @@ -32,5 +32,6 @@ const liquidTestnetAssetId = lwk.lTestAssetId; //Backups final keyServerUrl = dotenv.env['KEY_SERVER'] ?? 'http://localhost:3000'; - -const defaultBackupPath = 'backups'; //todo; create a better folder structure +final keyServerPublicKey = dotenv.env['KEY_SERVER_PUBLIC_KEY'] ?? + '6a04ab98d9e4774ad806e302dddeb63bea16b5cb5f223ee77478e861bb583eb3'; +const defaultBackupPath = 'backups'; From f9955003ca099fbf7baf4a828be7ed79f5e22ba9 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Sun, 16 Feb 2025 13:00:31 -0500 Subject: [PATCH 195/401] refactor: remove debug print statement --- lib/home/bloc/home_bloc.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/home/bloc/home_bloc.dart b/lib/home/bloc/home_bloc.dart index 5ecc08831..123639baa 100644 --- a/lib/home/bloc/home_bloc.dart +++ b/lib/home/bloc/home_bloc.dart @@ -62,7 +62,6 @@ class HomeBloc extends Bloc { WalletServicesUpdated event, Emitter emit, ) async { - debugPrint('wallet services updated: ${event.walletServices.length}'); final walletServicesData = event.walletServices .map((_) => WalletServiceData(wallet: _.wallet)) .toList(); From a662a77f2b376e8374be73a173ddd700ff995c4f Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Sun, 16 Feb 2025 13:01:11 -0500 Subject: [PATCH 196/401] feat: update backup settings to handle nullable wallet and include public descriptors --- .../bloc/backup_settings_cubit.dart | 28 +++++++++++-------- .../encrypted_vault_backup.dart | 4 +-- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/lib/wallet_settings/bloc/backup_settings_cubit.dart b/lib/wallet_settings/bloc/backup_settings_cubit.dart index 814d658ab..672cd573b 100644 --- a/lib/wallet_settings/bloc/backup_settings_cubit.dart +++ b/lib/wallet_settings/bloc/backup_settings_cubit.dart @@ -29,7 +29,7 @@ BackupSettingsCubit createBackupSettingsCubit({String? walletId}) { final currentWallet = walletId != null ? wallets.firstWhere((w) => w.id == walletId, orElse: () => wallets.first) - : wallets.first; + : null; return BackupSettingsCubit( wallets: wallets, @@ -576,6 +576,10 @@ class BackupSettingsCubit extends Cubit { layer: wallet.baseWalletType.name, script: wallet.scriptType.name, type: wallet.type.name, + publicDescriptors: [ + wallet.externalPublicDescriptor, + wallet.internalPublicDescriptor, + ].join(','), ); if (!wallet.hasPassphrase()) { @@ -845,10 +849,12 @@ class BackupSettingsCubit extends Cubit { return; } } - + print('Backups recovered: ${backups.length}'); // Notify HomeBloc that wallets have been recovered locator().add(LoadWalletsFromStorage()); - + print('called LoadWalletsFromStorage'); + await locator().sortWallets(); + print('called sortWallets'); emit( state.copyWith( loadingBackups: false, @@ -868,12 +874,12 @@ class BackupSettingsCubit extends Cubit { } Future _processBackupRecovery(Backup backup) async { - final network = _getNetwork(backup.network); + final network = BBNetwork.fromString(backup.network); final layer = _getLayer(backup.layer); final script = _getScript(backup.script); final type = _getWalletType(backup.type); - if (network == null || layer == null || script == null || type == null) { + if (layer == null || script == null || type == null) { emit( state.copyWith( errorLoadingBackups: @@ -891,15 +897,10 @@ class BackupSettingsCubit extends Cubit { type, backup.mnemonic.join(' '), backup.passphrase, + backup.publicDescriptors, ); } - BBNetwork? _getNetwork(String network) => switch (network.toLowerCase()) { - 'mainnet' => BBNetwork.Mainnet, - 'testnet' => BBNetwork.Testnet, - _ => null - }; - BaseWalletType? _getLayer(String layer) => switch (layer.toLowerCase()) { 'bitcoin' => BaseWalletType.Bitcoin, 'liquid' => BaseWalletType.Liquid, @@ -929,6 +930,7 @@ class BackupSettingsCubit extends Cubit { BBWalletType type, String mnemonic, String passphrase, + String publicDescriptors, ) async { final (seed, error) = await _walletSensitiveCreate.mnemonicSeed(mnemonic, network); @@ -944,7 +946,6 @@ class BackupSettingsCubit extends Cubit { try { await _walletSensRepository.newSeed(seed: seed); - final wallet = await _createWalletFromSeed( layer, seed, @@ -952,6 +953,7 @@ class BackupSettingsCubit extends Cubit { script, network, type, + publicDescriptors, ); if (wallet == null) { @@ -983,6 +985,7 @@ class BackupSettingsCubit extends Cubit { ScriptType script, BBNetwork network, BBWalletType type, + String publicDescriptors, ) async { switch (layer) { case BaseWalletType.Bitcoin: @@ -993,6 +996,7 @@ class BackupSettingsCubit extends Cubit { network: network, walletType: type, walletCreate: _walletCreate, + publicDescriptors: publicDescriptors, ); return wallet; case BaseWalletType.Liquid: diff --git a/lib/wallet_settings/encrypted_vault_backup.dart b/lib/wallet_settings/encrypted_vault_backup.dart index cad9ccea7..af979017c 100644 --- a/lib/wallet_settings/encrypted_vault_backup.dart +++ b/lib/wallet_settings/encrypted_vault_backup.dart @@ -267,8 +267,8 @@ class _StorageOptionCard extends StatelessWidget { } class EncryptedVaultRecoverPage extends StatefulWidget { - const EncryptedVaultRecoverPage({super.key, required this.wallet}); - final String wallet; + const EncryptedVaultRecoverPage({super.key, this.wallet}); + final String? wallet; @override State createState() => From 9dc285d1dc91846d250d0780eb057bced66ae660 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Sun, 16 Feb 2025 13:01:30 -0500 Subject: [PATCH 197/401] feat: add key server public key to KeyService initialization --- lib/wallet_settings/bloc/keychain_cubit.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/wallet_settings/bloc/keychain_cubit.dart b/lib/wallet_settings/bloc/keychain_cubit.dart index 2f8132cbb..d078bfe2d 100644 --- a/lib/wallet_settings/bloc/keychain_cubit.dart +++ b/lib/wallet_settings/bloc/keychain_cubit.dart @@ -18,7 +18,10 @@ class KeychainCubit extends Cubit { emit(state.copyWith(error: 'keychain api is not set')); return; } - _keyService = KeyService(keyServer: Uri.parse(keyServerUrl)); + _keyService = KeyService( + keyServer: Uri.parse(keyServerUrl), + keyServerPublicKey: keyServerPublicKey, + ); } void shuffleAndEmit() { @@ -176,6 +179,7 @@ class KeychainCubit extends Cubit { password: state.secret, salt: state.backupSalt, ); + emit( state.copyWith( backupKey: HEX.encode(backupKey), From 7dfe154bf457bcd9a64675fb7b45216eafad7e46 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Sun, 16 Feb 2025 13:45:46 -0500 Subject: [PATCH 198/401] refactor: remove unnecessary print statements from BackupSettingsCubit --- lib/wallet_settings/bloc/backup_settings_cubit.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/wallet_settings/bloc/backup_settings_cubit.dart b/lib/wallet_settings/bloc/backup_settings_cubit.dart index 672cd573b..8dda69fdf 100644 --- a/lib/wallet_settings/bloc/backup_settings_cubit.dart +++ b/lib/wallet_settings/bloc/backup_settings_cubit.dart @@ -849,12 +849,10 @@ class BackupSettingsCubit extends Cubit { return; } } - print('Backups recovered: ${backups.length}'); + // Notify HomeBloc that wallets have been recovered locator().add(LoadWalletsFromStorage()); - print('called LoadWalletsFromStorage'); await locator().sortWallets(); - print('called sortWallets'); emit( state.copyWith( loadingBackups: false, From 247c204416cb8596bd59c860f939b458b35a1971 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Sun, 16 Feb 2025 13:46:00 -0500 Subject: [PATCH 199/401] feat: check for testnet wallets and toggle network if necessary --- lib/home/bloc/home_bloc.dart | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/lib/home/bloc/home_bloc.dart b/lib/home/bloc/home_bloc.dart index 123639baa..dd7077d00 100644 --- a/lib/home/bloc/home_bloc.dart +++ b/lib/home/bloc/home_bloc.dart @@ -1,10 +1,14 @@ import 'dart:async'; +import 'package:bb_mobile/_model/wallet.dart'; import 'package:bb_mobile/_repository/app_wallets_repository.dart'; import 'package:bb_mobile/_repository/network_repository.dart'; import 'package:bb_mobile/_repository/wallet_service.dart'; import 'package:bb_mobile/home/bloc/home_event.dart'; import 'package:bb_mobile/home/bloc/home_state.dart'; +import 'package:bb_mobile/locator.dart'; +import 'package:bb_mobile/network/bloc/event.dart'; +import 'package:bb_mobile/network/bloc/network_bloc.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -116,8 +120,19 @@ class HomeBloc extends Bloc { Emitter emit, ) async { emit(state.copyWith(loadingWallets: true)); + await _appWalletsRepository.getWalletsFromStorage(); final wallets = _appWalletsRepository.allWallets; + + // Check if any wallet is testnet and switch network if needed + if (wallets.isNotEmpty) { + final hasTestnetWallet = wallets.any((w) => w.isTestnet()); + final currentNetwork = locator().state.getBBNetwork(); + + if (hasTestnetWallet && currentNetwork != BBNetwork.Testnet) { + locator().add(ToggleTestnet()); + } + } emit( state.copyWith( wallets: wallets.map((_) => WalletServiceData(wallet: _)).toList(), From fe8c382f52fdd64b30c44c754106d8c352beb4f1 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Sun, 16 Feb 2025 13:48:44 -0500 Subject: [PATCH 200/401] refactor: simplify SuccessDialog build method --- lib/wallet_settings/keychain_page.dart | 81 +++++++++++++------------- 1 file changed, 40 insertions(+), 41 deletions(-) diff --git a/lib/wallet_settings/keychain_page.dart b/lib/wallet_settings/keychain_page.dart index edda6c082..db8e483ee 100644 --- a/lib/wallet_settings/keychain_page.dart +++ b/lib/wallet_settings/keychain_page.dart @@ -498,6 +498,7 @@ class _SetButton extends StatelessWidget { inputType == KeyChainInputType.pin ? 'Use a password instead of a pin' : 'Use a PIN instead of a password', + isBold: true, ), ), const Gap(5), @@ -760,51 +761,49 @@ class _SuccessDialog extends StatelessWidget { @override Widget build(BuildContext context) { - final dialogContent = Padding( - padding: const EdgeInsets.all(24), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.check_circle_outline, - color: context.colour.shadow, - size: 48, - ), - const Gap(16), - BBText.title( - isRecovery ? 'Recovery Successful' : 'Backup Successful', - textAlign: TextAlign.center, - isBold: true, - ), - const Gap(8), - BBText.bodySmall( - isRecovery - ? 'Your wallet has been recovered successfully' - : 'Your wallet has been backed up successfully', - textAlign: TextAlign.center, - ), - const Gap(24), - FilledButton( - onPressed: () { - Navigator.of(context).pop(); - context.go('/home'); - }, - style: FilledButton.styleFrom( - backgroundColor: context.colour.shadow, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), + return Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.check_circle_outline, + color: context.colour.shadow, + size: 48, + ), + const Gap(16), + BBText.title( + isRecovery ? 'Recovery Successful' : 'Backup Successful', + textAlign: TextAlign.center, + isBold: true, + ), + const Gap(8), + BBText.bodySmall( + isRecovery + ? 'Your wallet has been recovered successfully' + : 'Your wallet has been backed up successfully', + textAlign: TextAlign.center, + ), + const Gap(24), + FilledButton( + onPressed: () { + Navigator.of(context).pop(); + context.go('/home'); + }, + style: FilledButton.styleFrom( + backgroundColor: context.colour.shadow, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), ), + child: const Text('Continue'), ), - child: const Text('Continue'), - ), - ], + ], + ), ), ); - - return Dialog( - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), - child: dialogContent, - ); } } From da542c678fefde40f369dbb8d371753adc894558 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Sun, 16 Feb 2025 13:48:54 -0500 Subject: [PATCH 201/401] refactor: update navigation paths for wallet recovery --- lib/home/home_page.dart | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/lib/home/home_page.dart b/lib/home/home_page.dart index cf6852be5..fe49d5361 100644 --- a/lib/home/home_page.dart +++ b/lib/home/home_page.dart @@ -102,7 +102,6 @@ class _ScreenState extends State<_Screen> { (HomeBloc x) => x.state.loadingWallets, ); final network = context.select((NetworkBloc x) => x.state.getBBNetwork()); - final wallets = context.select( (HomeBloc x) => x.state.walletsFromNetwork(network), ); @@ -115,7 +114,8 @@ class _ScreenState extends State<_Screen> { // final walletBlocsLen = // context.select((HomeBloc x) => x.state.lenWalletsFromNetwork(network)); - if (!loading && (wallets.isEmpty || !hasMainWallets)) { + // Question: Do we need to check for mainWallets? since the recoverd wallet are not main wallets by default + if (!loading && (wallets.isEmpty)) { final isTestnet = network == BBNetwork.Testnet; Widget widget = Scaffold( @@ -1029,7 +1029,9 @@ class HomeNoWalletsWithCreation extends StatelessWidget { label: 'Recover wallet backup', centered: true, onPressed: () { - context1.push('/import-main'); + context1.push( + '/wallet-settings/backup-settings/recover-encrypted', + ); }, ), ], @@ -1064,10 +1066,8 @@ class HomeNoWalletsView extends StatelessWidget { listener: (context3, state) async { if (state.saved) { if (state.savedWallets == null) return; - //if (state.mainWallet) await locator().sortWallets(); - locator() - .add(LoadWalletsFromStorage()); //getWalletsFromStorage(); + locator().add(LoadWalletsFromStorage()); if (!context.mounted) return; context3.go('/home'); } @@ -1126,7 +1126,9 @@ class HomeNoWalletsView extends StatelessWidget { isBlue: false, fontSize: 11, onPressed: () { - context.push('/import-main'); + context.push( + '/wallet-settings/backup-settings/recover-encrypted', + ); }, ), ], From c9f61466163c827e7d0c1909431fc597914708f8 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Sun, 16 Feb 2025 18:00:39 -0500 Subject: [PATCH 202/401] refactor: enhance wallet backup status update logic --- .../bloc/backup_settings_cubit.dart | 46 +++++++++++-------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/lib/wallet_settings/bloc/backup_settings_cubit.dart b/lib/wallet_settings/bloc/backup_settings_cubit.dart index 8dda69fdf..6e34dea5d 100644 --- a/lib/wallet_settings/bloc/backup_settings_cubit.dart +++ b/lib/wallet_settings/bloc/backup_settings_cubit.dart @@ -205,7 +205,12 @@ class BackupSettingsCubit extends Cubit { return; } - await _updateWalletBackupStatus(); + await _updateWalletBackupStatus( + _currentWallet!.copyWith( + physicalBackupTested: true, + lastPhysicalBackupTested: DateTime.now(), + ), + ); _emitBackupTestSuccessState(); } @@ -230,19 +235,15 @@ class BackupSettingsCubit extends Cubit { return seed; } - Future _updateWalletBackupStatus() async { - final wallet = _currentWallet!.copyWith( - physicalBackupTested: true, - lastPhysicalBackupTested: DateTime.now(), - ); - - final service = _appWalletsRepository.getWalletServiceById(wallet.id); + Future _updateWalletBackupStatus(Wallet updatedWallet) async { + final service = + _appWalletsRepository.getWalletServiceById(updatedWallet.id); if (service != null) { await service.updateWallet( - wallet, + updatedWallet, updateTypes: [UpdateWalletTypes.settings], ); - _currentWallet = wallet; + _currentWallet = updatedWallet; } } @@ -769,9 +770,6 @@ class BackupSettingsCubit extends Cubit { encrypted: file, ); if (loadedBackup != null) { - final id = loadedBackup['id'] as String; - - debugPrint('Loaded backup: $id'); emit( state.copyWith( loadingBackups: false, @@ -853,6 +851,7 @@ class BackupSettingsCubit extends Cubit { // Notify HomeBloc that wallets have been recovered locator().add(LoadWalletsFromStorage()); await locator().sortWallets(); + emit( state.copyWith( loadingBackups: false, @@ -888,7 +887,7 @@ class BackupSettingsCubit extends Cubit { return; } - await _addOrUpdateWallet( + final savedWallet = await _addOrUpdateWallet( network, layer, script, @@ -897,6 +896,14 @@ class BackupSettingsCubit extends Cubit { backup.passphrase, backup.publicDescriptors, ); + if (savedWallet != null) { + await _updateWalletBackupStatus( + savedWallet.copyWith( + vaultBackupTested: true, + lastVaultBackupTested: DateTime.now(), + ), + ); + } } BaseWalletType? _getLayer(String layer) => switch (layer.toLowerCase()) { @@ -921,7 +928,7 @@ class BackupSettingsCubit extends Cubit { _ => null }; - Future _addOrUpdateWallet( + Future _addOrUpdateWallet( BBNetwork network, BaseWalletType layer, ScriptType script, @@ -939,7 +946,7 @@ class BackupSettingsCubit extends Cubit { loadingBackups: false, ), ); - return; + return null; } try { @@ -961,10 +968,12 @@ class BackupSettingsCubit extends Cubit { loadingBackups: false, ), ); - return; + return null; } - await _walletsStorageRepository.newWallet(wallet); + await _walletsStorageRepository + .newWallet(wallet.copyWith(vaultBackupTested: true)); + return wallet; } catch (e) { debugPrint('Wallet creation error: $e'); emit( @@ -973,6 +982,7 @@ class BackupSettingsCubit extends Cubit { loadingBackups: false, ), ); + return null; } } From 4c36917af281f18dda520b24ff47805aae158244 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Sun, 16 Feb 2025 18:00:59 -0500 Subject: [PATCH 203/401] refactor: change lastVaultBackupTested type to DateTime --- lib/_model/wallet.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/_model/wallet.dart b/lib/_model/wallet.dart index 5a510ce34..e07f6283c 100644 --- a/lib/_model/wallet.dart +++ b/lib/_model/wallet.dart @@ -78,7 +78,7 @@ class Wallet with _$Wallet { @Default(false) bool physicalBackupTested, DateTime? lastPhysicalBackupTested, @Default(false) bool vaultBackupTested, - @Default(false) bool lastVaultBackupTested, + DateTime? lastVaultBackupTested, @Default(false) bool hide, @Default(false) bool mainWallet, required BaseWalletType baseWalletType, From 08ef3e47354163fb1b617a251a365a990f73d798 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Sun, 16 Feb 2025 18:01:07 -0500 Subject: [PATCH 204/401] refactor: update navigation paths for wallet recovery options --- lib/home/home_page.dart | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/home/home_page.dart b/lib/home/home_page.dart index fe49d5361..a808ce0c7 100644 --- a/lib/home/home_page.dart +++ b/lib/home/home_page.dart @@ -107,9 +107,9 @@ class _ScreenState extends State<_Screen> { ); // final hasWallets = context.select((HomeBloc x) => x.state.hasWallets()); - final hasMainWallets = context.select( - (HomeBloc x) => x.state.hasMainWallets(), - ); + // final hasMainWallets = context.select( + // (HomeBloc x) => x.state.hasMainWallets(), + // ); // final walletBlocsLen = // context.select((HomeBloc x) => x.state.lenWalletsFromNetwork(network)); @@ -1030,7 +1030,7 @@ class HomeNoWalletsWithCreation extends StatelessWidget { centered: true, onPressed: () { context1.push( - '/wallet-settings/backup-settings/recover-encrypted', + '/wallet-settings/backup-settings/recover-options/encrypted', ); }, ), @@ -1127,7 +1127,7 @@ class HomeNoWalletsView extends StatelessWidget { fontSize: 11, onPressed: () { context.push( - '/wallet-settings/backup-settings/recover-encrypted', + '/wallet-settings/backup-settings/recover-options/encrypted', ); }, ), @@ -1156,7 +1156,7 @@ class HomeWarnings extends StatelessWidget { WarningBanner( onTap: () { context.push( - '/wallet-settings/open-backup', + '/wallet-settings/backup-settings/backup-options', extra: w.walletBloc.id, ); }, From 81e5fb26ee883f4df002fde6f009eb579ce5c568 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Sun, 16 Feb 2025 18:01:15 -0500 Subject: [PATCH 205/401] refactor: update navigation path for backup settings --- lib/wallet/wallet_txs.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/wallet/wallet_txs.dart b/lib/wallet/wallet_txs.dart index d75898cfe..7acdd189c 100644 --- a/lib/wallet/wallet_txs.dart +++ b/lib/wallet/wallet_txs.dart @@ -238,7 +238,7 @@ class BackupAlertBanner extends StatelessWidget { return WarningBanner( onTap: () { context.push( - '/wallet-settings/open-backup', + '/wallet-settings/backup-settings/backup-options', extra: wallet.id, ); }, From b8df81092d28fc3a96b22b0fa7a503eab18b1118 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Sun, 16 Feb 2025 18:01:27 -0500 Subject: [PATCH 206/401] refactor: reorganize backup and recovery routes for improved navigation --- lib/routes.dart | 86 ++++++++++++++++++++++++++----------------------- 1 file changed, 45 insertions(+), 41 deletions(-) diff --git a/lib/routes.dart b/lib/routes.dart index d9d68946b..0013c68be 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -207,47 +207,59 @@ GoRouter setupRouter() => GoRouter( builder: (context, state) => BackupOptionsScreen( wallet: state.extra! as String, ), - ), - GoRoute( - path: 'physical', - builder: (context, state) => PhysicalBackupPage( - wallet: state.extra! as String, - ), routes: [ GoRoute( - path: 'test-backup', - builder: (context, state) { - final wallet = state.extra! as String; - return TestBackupPage( - wallet: wallet, - // walletSettings: blocs.$2, - ); - // const WalletSettingsPage(openTestBackup: true); - }, + path: 'physical', + builder: (context, state) => PhysicalBackupPage( + wallet: state.extra! as String, + ), + routes: [ + GoRoute( + path: 'test-backup', + builder: (context, state) { + final wallet = state.extra! as String; + return TestBackupPage( + wallet: wallet, + // walletSettings: blocs.$2, + ); + }, + ), + ], + ), + GoRoute( + path: 'encrypted', + builder: (context, state) => EncryptedVaultBackupPage( + wallet: state.extra! as String, + ), ), ], ), GoRoute( - path: 'encrypted', - builder: (context, state) => EncryptedVaultBackupPage( - wallet: state.extra! as String, - ), - ), - GoRoute( - path: 'recover-encrypted', - builder: (context, state) => EncryptedVaultRecoverPage( - wallet: state.extra as String?, - ), + path: 'recover-options', + builder: (context, state) => const RecoverOptionsScreen(), routes: [ GoRoute( - path: 'info', - builder: (context, state) { - final recoveredBackup = - state.extra! as Map; - return RecoveredBackupInfoPage( - recoveredBackup: recoveredBackup, - ); - }, + path: 'physical', + builder: (context, state) => + const ImportWalletPage(isRecovery: true), + ), + GoRoute( + path: 'encrypted', + builder: (context, state) => EncryptedVaultRecoverPage( + wallet: state.extra as String?, + ), + routes: [ + GoRoute( + path: 'info', + builder: (context, state) { + final recoveredBackup = + state.extra! as Map; + return RecoveredBackupInfoPage( + recoveredBackup: recoveredBackup, + ); + }, + ), + ], ), ], ), @@ -266,14 +278,6 @@ GoRouter setupRouter() => GoRouter( ), ], ), - //TODO: refactor route - GoRoute( - path: '/wallet-settings/open-backup', - builder: (context, state) { - final wallet = state.extra! as String; - return WalletSettingsPage(openBackup: true, wallet: wallet); - }, - ), GoRoute( path: '/wallet-settings/accounting', From 505c870ee724856f15fe364b31fbe198189171bd Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Sun, 16 Feb 2025 18:01:35 -0500 Subject: [PATCH 207/401] refactor: update backup settings navigation and add recovery options screen --- lib/wallet_settings/backup_settings.dart | 138 ++++++++++++++++++++++- 1 file changed, 134 insertions(+), 4 deletions(-) diff --git a/lib/wallet_settings/backup_settings.dart b/lib/wallet_settings/backup_settings.dart index 3ccab4736..5e841b99c 100644 --- a/lib/wallet_settings/backup_settings.dart +++ b/lib/wallet_settings/backup_settings.dart @@ -126,7 +126,7 @@ class _Screen extends StatelessWidget { label: "Recover or test backup", onPressed: () { context.push( - '/wallet-settings/backup-settings/recover-encrypted', + '/wallet-settings/backup-settings/recover-options', extra: context.read().state.wallet.id, ); }, @@ -160,7 +160,7 @@ class BackupOptionsScreen extends StatelessWidget { child: Column( children: [ const BBText.titleLarge( - 'Backup you wallet', + 'Backup your wallet', isBold: true, fontSize: 25, ), @@ -198,7 +198,7 @@ class BackupOptionsScreen extends StatelessWidget { description: 'Your backup is encrypted with a secure key that cannot be cracked, and uploaded to your cloud account. The key to unlock your vault is stored in an anonymous password manager and accessible with your PIN.', onTap: () => context.push( - '/wallet-settings/backup-settings/encrypted', + '/wallet-settings/backup-settings/backup-options/encrypted', extra: wallet, ), ), @@ -208,7 +208,7 @@ class BackupOptionsScreen extends StatelessWidget { description: 'You have to write down 12 words on a piece of paper or engrave it in metal. Make sure not to lose it. If anybody ever finds those 12 words, they can steal your Bitcoin.', onTap: () async => context.push( - '/wallet-settings/backup-settings/physical', + '/wallet-settings/backup-settings/backup-options/physical', extra: wallet, ), ), @@ -271,3 +271,133 @@ class BackupOptionsScreen extends StatelessWidget { ); } } + +class RecoverOptionsScreen extends StatelessWidget { + const RecoverOptionsScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: Colors.transparent, + elevation: 0, + automaticallyImplyLeading: false, + flexibleSpace: BBAppBar( + onBack: () => context.pop(), + text: '', + ), + ), + body: Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + children: [ + const BBText.titleLarge( + 'Recover or test your backup', + isBold: true, + fontSize: 25, + ), + const Gap(10), + RichText( + textAlign: TextAlign.center, + text: TextSpan( + children: [ + TextSpan( + text: 'Testing your backup is ', + style: context.font.bodySmall!.copyWith( + fontSize: 12, + ), + ), + TextSpan( + text: 'critically important ', + style: context.font.bodySmall!.copyWith( + fontWeight: FontWeight.w900, + fontSize: 12, + ), + ), + TextSpan( + text: + 'to ensure you can recover your wallet if needed. Choose your recovery method below.', + style: context.font.bodySmall!.copyWith( + fontSize: 12, + ), + ), + ], + ), + ), + const Gap(20), + _renderBackupSetting( + title: 'Encrypted vault', + description: + "Restore your wallet using the encrypted backup stored in your cloud account. You'll need your PIN to access the decryption key from the password manager.", + onTap: () => context.push( + '/wallet-settings/backup-settings/recover-options/encrypted', + ), + ), + const Gap(20), + _renderBackupSetting( + title: 'Physical backup', + description: + "Restore your wallet by entering the 12 words from your physical backup.", + onTap: () async => context.push( + '/wallet-settings/backup-settings/recover-options/physical', + ), + ), + ], + ), + ), + ); + } + + Widget _renderBackupSetting({ + required String title, + required String description, + required VoidCallback onTap, + }) { + return InkWell( + onTap: onTap, + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + boxShadow: [ + BoxShadow( + color: NewColours.lightGray.withAlpha(50), + blurRadius: 30, + spreadRadius: 2, + offset: const Offset(0, 10), + ), + ], + border: Border.all(color: NewColours.lightGray.withAlpha(100)), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + flex: 6, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + BBText.title( + title, + isBold: true, + ), + const Gap(4), + BBText.bodySmall( + description, + removeColourOpacity: true, + ), + ], + ), + ), + const Expanded( + child: Icon( + Icons.arrow_forward_ios, + size: 16, + ), + ), + ], + ), + ), + ); + } +} From 1d66efb9f58d48927eff765cbf701b855d7d4aa6 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Sun, 16 Feb 2025 18:01:40 -0500 Subject: [PATCH 208/401] refactor: update recovery navigation path for encrypted backup options --- lib/wallet_settings/encrypted_vault_backup.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/wallet_settings/encrypted_vault_backup.dart b/lib/wallet_settings/encrypted_vault_backup.dart index af979017c..75a0f6f8d 100644 --- a/lib/wallet_settings/encrypted_vault_backup.dart +++ b/lib/wallet_settings/encrypted_vault_backup.dart @@ -348,7 +348,7 @@ class _EncryptedVaultRecoverPageState extends State { } if (state.latestRecoveredBackup.isNotEmpty) { context.push( - '/wallet-settings/backup-settings/recover-encrypted/info', + '/wallet-settings/backup-settings/recover-options/encrypted/info', extra: state.latestRecoveredBackup, ); _cubit.clearError(); From f9f72d0e462c57196784db43f94c7ccf77f531e1 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Sun, 16 Feb 2025 18:01:54 -0500 Subject: [PATCH 209/401] refactor: update navigation path for physical test backup and remove unused parameters --- lib/wallet_settings/physical_backup.dart | 2 +- lib/wallet_settings/wallet_settings_page.dart | 30 ++----------------- 2 files changed, 3 insertions(+), 29 deletions(-) diff --git a/lib/wallet_settings/physical_backup.dart b/lib/wallet_settings/physical_backup.dart index 98746dbc8..4e4681c0d 100644 --- a/lib/wallet_settings/physical_backup.dart +++ b/lib/wallet_settings/physical_backup.dart @@ -256,7 +256,7 @@ class BackupScreen extends StatelessWidget { context // ..pop() .push( - '/wallet-settings/backup-settings/physical/test-backup', + '/wallet-settings/backup-settings/backup-options/physical/test-backup', extra: context.read().state.wallet.id, // ( // context.read(), diff --git a/lib/wallet_settings/wallet_settings_page.dart b/lib/wallet_settings/wallet_settings_page.dart index 8b0f8245d..cd7b4bcf5 100644 --- a/lib/wallet_settings/wallet_settings_page.dart +++ b/lib/wallet_settings/wallet_settings_page.dart @@ -25,9 +25,7 @@ class WalletSettingsPage extends StatelessWidget { const WalletSettingsPage({ super.key, required this.wallet, - this.openBackup = false, }); - final bool openBackup; final String wallet; @override @@ -55,19 +53,14 @@ class WalletSettingsPage extends StatelessWidget { ), ], child: WalletSettingsListeners( - child: _Screen( - openBackup: openBackup, - ), + child: _Screen(), ), ); } } class _Screen extends StatefulWidget { - const _Screen({required this.openBackup}); - - // final bool openTestBackup; - final bool openBackup; + const _Screen(); @override State<_Screen> createState() => _ScreenState(); @@ -76,28 +69,9 @@ class _Screen extends StatefulWidget { class _ScreenState extends State<_Screen> { @override void initState() { - _init(); super.initState(); } - void _init() { - scheduleMicrotask(() async { - if (widget.openBackup) { - // await Future.delayed(const Duration(milliseconds: 300)); - await context.push( - '/wallet-settings/backup-settings/physical', - extra: context.read().state.wallet.id, - // ( - // context.read(), - // context.read(), - // ), - ); - } else { - // showPage = true; - } - }); - } - @override Widget build(BuildContext context) { final watchOnly = From 5ae41bfb63b3e1c0acb215ba97b648727f545f80 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 17 Feb 2025 22:18:48 -0500 Subject: [PATCH 210/401] refactor: add overflow property to BBText widget --- lib/_ui/components/text.dart | 12 ++++++++++-- lib/_ui/warning.dart | 5 ++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/lib/_ui/components/text.dart b/lib/_ui/components/text.dart index 41a0ec8d2..33314ed29 100644 --- a/lib/_ui/components/text.dart +++ b/lib/_ui/components/text.dart @@ -25,6 +25,7 @@ class BBText extends StatelessWidget { this.fontSize, this.uiKey, this.compact = false, + this.overflow = TextOverflow.clip, }) : type = _FontTypes.headline, textAlign = TextAlign.left; @@ -40,6 +41,7 @@ class BBText extends StatelessWidget { this.fontSize, this.uiKey, this.compact = false, + this.overflow = TextOverflow.clip, }) : type = _FontTypes.titleLarge; const BBText.title( @@ -54,6 +56,7 @@ class BBText extends StatelessWidget { this.fontSize, this.uiKey, this.compact = false, + this.overflow = TextOverflow.clip, }) : type = _FontTypes.title; const BBText.body( @@ -68,6 +71,7 @@ class BBText extends StatelessWidget { this.fontSize, this.uiKey, this.compact = false, + this.overflow = TextOverflow.clip, }) : type = _FontTypes.body; const BBText.bodySmall( @@ -82,6 +86,7 @@ class BBText extends StatelessWidget { this.fontSize, this.uiKey, this.compact = false, + this.overflow = TextOverflow.clip, }) : type = _FontTypes.bodySmall; const BBText.bodyBold( @@ -96,6 +101,7 @@ class BBText extends StatelessWidget { this.fontSize, this.uiKey, this.compact = false, + this.overflow = TextOverflow.clip, }) : type = _FontTypes.body; const BBText.error( @@ -110,6 +116,7 @@ class BBText extends StatelessWidget { this.fontSize, this.uiKey, this.compact = false, + this.overflow = TextOverflow.clip, }) : type = _FontTypes.error; const BBText.errorSmall( @@ -124,6 +131,7 @@ class BBText extends StatelessWidget { this.fontSize, this.uiKey, this.compact = false, + this.overflow = TextOverflow.clip, }) : type = _FontTypes.errorSmall; final String text; @@ -138,7 +146,7 @@ class BBText extends StatelessWidget { final Key? uiKey; final double? fontSize; final bool compact; - + final TextOverflow? overflow; @override Widget build(BuildContext context) { TextStyle style; @@ -163,7 +171,7 @@ class BBText extends StatelessWidget { case _FontTypes.errorSmall: style = context.font.bodySmall!.copyWith(color: context.colour.error); } - + style = style.copyWith(overflow: overflow); if (onSurface) style = style.copyWith(color: context.colour.onSurface); if (isBlue) style = style.copyWith(color: context.colour.secondary); if (isRed) style = style.copyWith(color: context.colour.primary); diff --git a/lib/_ui/warning.dart b/lib/_ui/warning.dart index f627027cf..0afe5edb1 100644 --- a/lib/_ui/warning.dart +++ b/lib/_ui/warning.dart @@ -72,7 +72,10 @@ class WarningBanner extends StatelessWidget { size: 16, ), const Gap(8), - BBText.errorSmall(info), + BBText.errorSmall( + info, + overflow: TextOverflow.fade, + ), ], ), ), From 9aaa227534328f49eff87e8b4667c0ee38c36712 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 17 Feb 2025 22:18:59 -0500 Subject: [PATCH 211/401] refactor: correct public descriptor selection in BDKSensitiveCreate --- lib/_pkg/wallet/bdk/sensitive_create.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/_pkg/wallet/bdk/sensitive_create.dart b/lib/_pkg/wallet/bdk/sensitive_create.dart index 91b5208d3..5a7a52e96 100644 --- a/lib/_pkg/wallet/bdk/sensitive_create.dart +++ b/lib/_pkg/wallet/bdk/sensitive_create.dart @@ -351,11 +351,10 @@ class BDKSensitiveCreate { network: bdkNetwork, ); internal = await bdk.Descriptor.create( - descriptor: publicDescriptors.split(',')[0], + descriptor: publicDescriptors.split(',')[1], network: bdkNetwork, ); } - final descHashId = createDescriptorHashId(external.asString()).substring(0, 12); var wallet = Wallet( From a1f69fb510339385b963ad74b8b67b93b6efd0a7 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 17 Feb 2025 22:19:13 -0500 Subject: [PATCH 212/401] refactor: synchronize vault backup test status in wallet storage --- lib/_repository/wallet_service.dart | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/lib/_repository/wallet_service.dart b/lib/_repository/wallet_service.dart index 247d46c4d..e85f50f37 100644 --- a/lib/_repository/wallet_service.dart +++ b/lib/_repository/wallet_service.dart @@ -267,7 +267,11 @@ class WalletService { physicalBackupTested: wallet.physicalBackupTested, ); } - + if (wallet.vaultBackupTested != storageWallet.vaultBackupTested) { + storageWallet = storageWallet.copyWith( + vaultBackupTested: wallet.vaultBackupTested, + ); + } if (wallet.name != storageWallet.name) { storageWallet = storageWallet.copyWith( name: wallet.name, @@ -281,6 +285,13 @@ class WalletService { lastPhysicalBackupTested: wallet.lastPhysicalBackupTested, ); } + if (wallet.lastVaultBackupTested != null && + wallet.lastVaultBackupTested != + storageWallet.lastVaultBackupTested) { + storageWallet = storageWallet.copyWith( + lastVaultBackupTested: wallet.lastVaultBackupTested, + ); + } } final err = await _walletsStorageRepository.updateWallet( From 50e9228f6af32668fce3565477e217943a83c588 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 17 Feb 2025 22:19:50 -0500 Subject: [PATCH 213/401] refactor: improve error handling --- .../bloc/backup_settings_cubit.dart | 439 ++++++++---------- 1 file changed, 188 insertions(+), 251 deletions(-) diff --git a/lib/wallet_settings/bloc/backup_settings_cubit.dart b/lib/wallet_settings/bloc/backup_settings_cubit.dart index 6e34dea5d..eb47afd8f 100644 --- a/lib/wallet_settings/bloc/backup_settings_cubit.dart +++ b/lib/wallet_settings/bloc/backup_settings_cubit.dart @@ -345,7 +345,7 @@ class BackupSettingsCubit extends Cubit { ); } -// encrypted vault backup methods + // encrypted vault backup methods void _emitBackupError(String message) { emit( state.copyWith( @@ -368,148 +368,147 @@ class BackupSettingsCubit extends Cubit { Future saveFileSystemBackup() async { if (!_canStartBackup()) { - emit( - state.copyWith( - errorSavingBackups: 'Please wait before attempting another backup', - savingBackups: false, - backupKey: '', - ), - ); + _handleSaveError('Please wait before attempting another backup'); return; } - if (_filePicker == null) { - _emitBackupError('Failed to pick the file'); - return; - } - - emit(state.copyWith(savingBackups: true, errorSavingBackups: '')); + _emitSafe(state.copyWith(savingBackups: true, errorSavingBackups: '')); final backups = await _createBackupsForAllWallets(); if (backups.isEmpty) { - _emitBackupError('No wallets available for backup'); + _handleSaveError('No wallets available for backup'); return; } final (encryptedData, err) = await _encryptBackups(backups); if (err != null || encryptedData == null) { + _handleSaveError(err?.message ?? 'Encryption failed'); return; } - try { - final (savePath, pickErr) = await _filePicker.getDirectoryPath(); - if (pickErr != null) { - _emitBackupError('Failed to select backup location'); - return; - } + final (savePath, pickErr) = await _filePicker?.getDirectoryPath() ?? + (null, Err('File picker not initialized')); + if (pickErr != null) { + _handleSaveError('Failed to select backup location: ${pickErr.message}'); + return; + } - if (savePath == null || savePath.isEmpty) { - _emitBackupError('No location selected for backup'); - return; - } + if (savePath == null || savePath.isEmpty) { + _handleSaveError('No location selected for backup'); + return; + } - // Use the selected path with the manager - final (filePath, errSave) = await _manager.saveEncryptedBackup( - encrypted: encryptedData.$2, - backupFolder: savePath, - ); + final (filePath, saveErr) = await _manager.saveEncryptedBackup( + encrypted: encryptedData.$2, + backupFolder: savePath, + ); - if (errSave != null) { - _emitBackupError('Save failed: ${errSave.message}'); - return; - } + if (saveErr != null) { + _handleSaveError('Save failed: ${saveErr.message}'); + return; + } - final fileName = filePath?.split('/').last; - final backupId = fileName?.split('_').last.split('.').first; - if (backupId == null) { - _emitBackupError('Failed to extract backup ID'); - return; - } + final fileName = filePath?.split('/').last; + final backupId = fileName?.split('_').last.split('.').first; + if (backupId == null) { + _handleSaveError('Failed to extract backup ID'); + return; + } - final backupSalt = jsonDecode(encryptedData.$2)['salt'] as String; + final backupSalt = _extractBackupSalt(encryptedData.$2); + if (backupSalt == null) { + _handleSaveError('Failed to extract backup salt'); + return; + } - emit( - state.copyWith( - backupId: backupId, - backupKey: encryptedData.$1, - backupFolderPath: filePath ?? '', - backupSalt: backupSalt, - savingBackups: false, - lastBackupAttempt: DateTime.now(), - ), - ); - } catch (e) { - _emitBackupError('Failed to save backup: $e'); + _emitSafe( + state.copyWith( + backupId: backupId, + backupKey: encryptedData.$1, + backupFolderPath: filePath ?? '', + backupSalt: backupSalt, + savingBackups: false, + lastBackupAttempt: DateTime.now(), + ), + ); + } + + String? _extractBackupSalt(String encrypted) { + try { + final decoded = jsonDecode(encrypted) as Map; + return decoded['salt'] as String?; + } catch (_) { + return null; } } Future saveGoogleDriveBackup() async { if (!_canStartBackup()) { - _emitBackupError('Please wait before attempting another backup'); + _handleSaveError('Please wait before attempting another backup'); return; } - emit(state.copyWith(savingBackups: true, errorSavingBackups: '')); + _emitSafe(state.copyWith(savingBackups: true, errorSavingBackups: '')); - try { - final backups = await _createBackupsForAllWallets(); - if (backups.isEmpty) { - _emitBackupError('No wallets available for backup'); + final backups = await _createBackupsForAllWallets(); + if (backups.isEmpty) { + _handleSaveError('No wallets available for backup'); + return; + } + + if (state.backupFolderId.isEmpty) { + final (folderId, err) = await _driveManager.connect(); + if (err != null) { + _handleSaveError('Failed to connect to Google Drive: ${err.message}'); return; } + _emitSafe(state.copyWith(backupFolderId: folderId ?? '')); + } - // Connect if needed - if (state.backupFolderId.isEmpty) { - final (folderId, err) = await _driveManager.connect(); - if (err != null) { - _emitBackupError('Failed to connect to Google Drive: ${err.message}'); - return; - } - emit(state.copyWith(backupFolderId: folderId ?? '')); - } + if (state.backupFolderId.isEmpty) { + _handleSaveError('Failed to initialize Google Drive backup folder'); + return; + } - // Ensure we have a folder ID - if (state.backupFolderId.isEmpty) { - _emitBackupError('Failed to initialize Google Drive backup folder'); - return; - } + final (encryptedData, encryptErr) = await _encryptBackups(backups); + if (encryptErr != null || encryptedData == null) { + _handleSaveError(encryptErr?.message ?? 'Encryption failed'); + return; + } - final (encryptedData, err) = await _encryptBackups(backups); - if (err != null || encryptedData == null) return; + final backupSalt = _extractBackupSalt(encryptedData.$2); + if (backupSalt == null) { + _handleSaveError('Failed to extract backup salt'); + return; + } - final backupSalt = jsonDecode(encryptedData.$2)['salt'] as String; + final (filePath, saveErr) = await _driveManager.saveEncryptedBackup( + encrypted: encryptedData.$2, + backupFolder: state.backupFolderId, + ); - final (filePath, error) = await _driveManager.saveEncryptedBackup( - encrypted: encryptedData.$2, - backupFolder: state.backupFolderId, - ); + if (saveErr != null) { + _handleSaveError('Failed to save to Google Drive: ${saveErr.message}'); + return; + } - if (error != null) { - debugPrint('Error saving to Google Drive: ${error.message}'); - _emitBackupError('Failed to save to Google Drive'); - return; - } - final fileName = filePath?.split('/').last; - final backupId = fileName?.split('_').last.split('.').first; - if (backupId == null || fileName == null) { - debugPrint('Failed to extract backup ID'); - _emitBackupError('Failed to save to Google Drive'); - return; - } - emit( - state.copyWith( - backupId: backupId, - backupKey: encryptedData.$1, - backupFolderPath: fileName, - backupSalt: backupSalt, - savingBackups: false, - lastBackupAttempt: DateTime.now(), - ), - ); - } catch (e) { - debugPrint('Error saving to Google Drive: $e'); - _emitBackupError('Failed to save Google Drive backup'); + final fileName = filePath?.split('/').last; + final backupId = fileName?.split('_').last.split('.').first; + if (backupId == null || fileName == null) { + _handleSaveError('Failed to extract backup information'); + return; } + + _emitSafe( + state.copyWith( + backupId: backupId, + backupKey: encryptedData.$1, + backupFolderPath: fileName, + backupSalt: backupSalt, + savingBackups: false, + lastBackupAttempt: DateTime.now(), + ), + ); } Future<((String, String)?, Err?)> _encryptBackups( @@ -628,12 +627,7 @@ class BackupSettingsCubit extends Cubit { if (state.backupFolderId.isEmpty) { final (folderId, err) = await _driveManager.connect(); if (err != null) { - emit( - state.copyWith( - loadingBackups: false, - errorLoadingBackups: err.message, - ), - ); + _handleLoadError(err.message); return; } emit(state.copyWith(backupFolderId: folderId ?? '')); @@ -641,12 +635,7 @@ class BackupSettingsCubit extends Cubit { // Ensure we have a folder ID if (state.backupFolderId.isEmpty) { - emit( - state.copyWith( - loadingBackups: false, - errorLoadingBackups: "Failed to initialize Google Drive folder", - ), - ); + _handleLoadError("Failed to initialize Google Drive folder"); return; } @@ -658,12 +647,7 @@ class BackupSettingsCubit extends Cubit { if (err != null) { debugPrint('Error loading backups: ${err.message}'); - emit( - state.copyWith( - loadingBackups: false, - errorLoadingBackups: "Failed to get backup files", - ), - ); + _handleLoadError("Failed to get backup files"); return; } @@ -677,12 +661,7 @@ class BackupSettingsCubit extends Cubit { }); final backupId = latestBackup.name?.split('_').last.split('.').first; if (backupId == null) { - emit( - state.copyWith( - loadingBackups: false, - errorLoadingBackups: "Corrupted backup file", - ), - ); + _handleLoadError("Corrupted backup file"); return; } final (loadedBackupMetaData, mediaErr) = @@ -691,12 +670,7 @@ class BackupSettingsCubit extends Cubit { ); if (mediaErr != null || loadedBackupMetaData == null) { - emit( - state.copyWith( - loadingBackups: false, - errorLoadingBackups: "Failed to load backup data", - ), - ); + _handleLoadError("Failed to load backup data"); return; } @@ -714,30 +688,14 @@ class BackupSettingsCubit extends Cubit { return; } else if ((err != null) || loadedBackup?["id"] == null) { debugPrint('Error loading backups: ${err?.message}'); - emit( - state.copyWith( - loadingBackups: false, - errorLoadingBackups: "Corrupted backup file", - ), - ); + _handleLoadError("Corrupted backup file"); return; } } else { - emit( - state.copyWith( - loadingBackups: false, - errorLoadingBackups: "Failed to get backup files", - ), - ); + _handleLoadError("Failed to get backup files"); } } catch (e) { - debugPrint('Error loading backups: $e'); - emit( - state.copyWith( - loadingBackups: false, - errorLoadingBackups: "Failed to get backup files", - ), - ); + _handleLoadError('Failed to fetch backup: $e'); } } @@ -791,7 +749,7 @@ class BackupSettingsCubit extends Cubit { } Future recoverBackup(String encrypted, String backupKey) async { - emit( + _emitSafe( state.copyWith( loadingBackups: true, backupKey: backupKey, @@ -799,95 +757,60 @@ class BackupSettingsCubit extends Cubit { ), ); - if (state.backupKey.isEmpty) { - emit( - state.copyWith( - loadingBackups: false, - errorLoadingBackups: 'Backup key is missing', - ), - ); + if (backupKey.isEmpty) { + _handleLoadError('Backup key is missing'); return; } + final decoded = jsonDecode(encrypted) as Map; + final backupId = decoded['id'] as String?; - try { - final decodeEncryptedFile = jsonDecode(encrypted) as Map; - final id = decodeEncryptedFile['id']?.toString() ?? ''; - - if (id.isEmpty) { - emit( - state.copyWith( - loadingBackups: false, - errorLoadingBackups: 'Invalid backup format', - ), - ); - return; - } + if (backupId == null) { + _handleLoadError('Invalid backup format'); + return; + } - // Then decrypt the backup data - final (backups, err) = await _manager.decryptBackups( - encrypted: encrypted, - backupKey: state.backupKey, - ); + final (backups, decryptErr) = await _manager.decryptBackups( + encrypted: encrypted, + backupKey: backupKey, + ); - if (err != null || backups == null || backups.isEmpty) { - debugPrint('Error loading backups: ${err?.message}'); - emit( - state.copyWith( - loadingBackups: false, - errorLoadingBackups: err?.message ?? 'No wallets found in backup', - ), - ); - return; - } + if (decryptErr != null || backups == null || backups.isEmpty) { + _handleLoadError(decryptErr?.message ?? 'No wallets found in backup'); + return; + } - // Process each backup - for (final backup in backups) { - await _processBackupRecovery(backup); - if (state.errorLoadingBackups.isNotEmpty) { - return; - } + for (final backup in backups) { + final err = await _processBackupRecovery(backup); + if (err != null) { + _handleLoadError(err.message); + return; } + } - // Notify HomeBloc that wallets have been recovered - locator().add(LoadWalletsFromStorage()); - await locator().sortWallets(); + // Update home state and sort wallets + locator().add(LoadWalletsFromStorage()); + await locator().sortWallets(); - emit( - state.copyWith( - loadingBackups: false, - loadedBackups: backups, - errorLoadingBackups: '', - ), - ); - } catch (e) { - debugPrint('Recovery error: $e'); - emit( - state.copyWith( - loadingBackups: false, - errorLoadingBackups: 'Recovery failed: $e', - ), - ); - } + _emitSafe( + state.copyWith( + loadingBackups: false, + loadedBackups: backups, + errorLoadingBackups: '', + ), + ); } - Future _processBackupRecovery(Backup backup) async { + Future _processBackupRecovery(Backup backup) async { final network = BBNetwork.fromString(backup.network); final layer = _getLayer(backup.layer); final script = _getScript(backup.script); final type = _getWalletType(backup.type); if (layer == null || script == null || type == null) { - emit( - state.copyWith( - errorLoadingBackups: - 'Invalid backup configuration for ${backup.network}', - loadingBackups: false, - ), - ); - return; + return Err('Invalid backup configuration for ${backup.network}'); } - final savedWallet = await _addOrUpdateWallet( + final (savedWallet, err) = await _addOrUpdateWallet( network, layer, script, @@ -904,6 +827,7 @@ class BackupSettingsCubit extends Cubit { ), ); } + return err; } BaseWalletType? _getLayer(String layer) => switch (layer.toLowerCase()) { @@ -928,7 +852,7 @@ class BackupSettingsCubit extends Cubit { _ => null }; - Future _addOrUpdateWallet( + Future<(Wallet?, Err?)> _addOrUpdateWallet( BBNetwork network, BaseWalletType layer, ScriptType script, @@ -940,17 +864,15 @@ class BackupSettingsCubit extends Cubit { final (seed, error) = await _walletSensitiveCreate.mnemonicSeed(mnemonic, network); if (seed == null) { - emit( - state.copyWith( - errorLoadingBackups: 'Failed to create seed', - loadingBackups: false, - ), - ); - return null; + return (null, Err('Failed to create seed: $error')); } try { - await _walletSensRepository.newSeed(seed: seed); + final error = await _walletSensRepository.newSeed(seed: seed); + + if (error != null && !error.message.toLowerCase().contains('exists')) { + return (null, Err(error.toString())); + } final wallet = await _createWalletFromSeed( layer, seed, @@ -962,27 +884,18 @@ class BackupSettingsCubit extends Cubit { ); if (wallet == null) { - emit( - state.copyWith( - errorLoadingBackups: 'Failed to create wallet', - loadingBackups: false, - ), - ); - return null; + return (null, Err('Failed to create wallet')); } - await _walletsStorageRepository + final walletRepoErr = await _walletsStorageRepository .newWallet(wallet.copyWith(vaultBackupTested: true)); - return wallet; + if (walletRepoErr != null && + !walletRepoErr.message.toLowerCase().contains('exists')) { + return (null, Err(walletRepoErr.toString())); + } + return (wallet, null); } catch (e) { - debugPrint('Wallet creation error: $e'); - emit( - state.copyWith( - errorLoadingBackups: 'Failed to save wallet: $e', - loadingBackups: false, - ), - ); - return null; + return (null, Err(e.toString())); } } @@ -1019,4 +932,28 @@ class BackupSettingsCubit extends Cubit { return wallet; } } + + // Helper method for safe state emission + void _emitSafe(BackupSettingsState newState) { + if (!isClosed) emit(newState); + } + + // Separate error handling methods for loading and saving + void _handleLoadError(String message, {bool loading = false}) { + _emitSafe( + state.copyWith( + errorLoadingBackups: message, + loadingBackups: loading, + ), + ); + } + + void _handleSaveError(String message, {bool saving = false}) { + _emitSafe( + state.copyWith( + errorSavingBackups: message, + savingBackups: saving, + ), + ); + } } From 7e28980fa7d1d8b61870eae9a35e2f6f87e6ea0d Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 17 Feb 2025 22:20:03 -0500 Subject: [PATCH 214/401] refactor: enhance homeWarnings to include backup status and routing --- lib/home/bloc/home_state.dart | 50 +++++++++++++++++++++++++---------- 1 file changed, 36 insertions(+), 14 deletions(-) diff --git a/lib/home/bloc/home_state.dart b/lib/home/bloc/home_state.dart index 81e493177..e6c29bdff 100644 --- a/lib/home/bloc/home_state.dart +++ b/lib/home/bloc/home_state.dart @@ -402,33 +402,55 @@ class HomeState with _$HomeState { : walletsWithEnoughBalance; } - Set<({String info, Wallet walletBloc})> homeWarnings(BBNetwork network) { + Set<({String info, Wallet walletBloc, String? route})> homeWarnings( + BBNetwork network, + ) { bool instantBalWarning(Wallet wb) { if (wb.isInstant() == false) return false; return wb.balanceSats() > 100000000; } - bool backupWarning(Wallet wb) => !wb.physicalBackupTested; + bool needsBackupWarning(Wallet wb) => + !wb.physicalBackupTested || !wb.vaultBackupTested; - final warnings = <({String info, Wallet walletBloc})>{}; - final List backupWalletFngrforBackupWarning = []; + final warnings = <({String info, Wallet walletBloc, String? route})>{}; + final List backupWalletFngrForBackupWarning = []; for (final walletBloc in walletsFromNetwork(network)) { if (instantBalWarning(walletBloc)) { - warnings.add( - (info: 'Instant wallet balance is high', walletBloc: walletBloc), - ); - } - if (backupWarning(walletBloc)) { - final fngr = walletBloc.sourceFingerprint; - if (backupWalletFngrforBackupWarning.contains(fngr)) continue; warnings.add( ( - info: 'Back up your wallet! Tap to test backup.', - walletBloc: walletBloc + info: 'Instant wallet balance is high', + walletBloc: walletBloc, + route: null ), ); - backupWalletFngrforBackupWarning.add(fngr); + } + + if (needsBackupWarning(walletBloc)) { + final fngr = walletBloc.sourceFingerprint; + if (backupWalletFngrForBackupWarning.contains(fngr)) continue; + + if (!walletBloc.physicalBackupTested) { + warnings.add( + ( + info: 'Physical backup needed! Tap to test backup.', + walletBloc: walletBloc, + route: '/wallet-settings/backup-settings/backup-options/physical' + ), + ); + } + if (!walletBloc.vaultBackupTested) { + warnings.add( + ( + info: 'Encrypted backup needed! Tap to test backup.', + walletBloc: walletBloc, + route: '/wallet-settings/backup-settings/backup-options/encrypted' + ), + ); + } + + backupWalletFngrForBackupWarning.add(fngr); } } From 83eaf891f5427ce1b36a49dc82e84a45b3c4a0c0 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 17 Feb 2025 22:20:16 -0500 Subject: [PATCH 215/401] refactor: update routing in HomeWarnings to use dynamic routes --- lib/home/home_page.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/home/home_page.dart b/lib/home/home_page.dart index a808ce0c7..d26d4007f 100644 --- a/lib/home/home_page.dart +++ b/lib/home/home_page.dart @@ -1149,14 +1149,13 @@ class HomeWarnings extends StatelessWidget { final network = context.select((NetworkBloc _) => _.state.getBBNetwork()); final warnings = context.select((HomeBloc _) => _.state.homeWarnings(network)); - return Column( children: [ for (final w in warnings) WarningBanner( onTap: () { context.push( - '/wallet-settings/backup-settings/backup-options', + w.route ?? '/home', extra: w.walletBloc.id, ); }, From 73cdac40b075057a5c6fc81bd5b288791db8f132 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 17 Feb 2025 22:20:33 -0500 Subject: [PATCH 216/401] refactor: enhance backup alert to include vault backup status --- lib/wallet/wallet_page.dart | 4 +++- lib/wallet/wallet_txs.dart | 37 ++++++++++++++++++++++++++++--------- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/lib/wallet/wallet_page.dart b/lib/wallet/wallet_page.dart index 1c398934c..0f61a7756 100644 --- a/lib/wallet/wallet_page.dart +++ b/lib/wallet/wallet_page.dart @@ -64,6 +64,8 @@ class _Screen extends StatelessWidget { Widget build(BuildContext context) { final physicalBackupTested = context.select((WalletBloc x) => x.state.wallet.physicalBackupTested); + final vaultBackupTested = + context.select((WalletBloc x) => x.state.wallet.vaultBackupTested); return RefreshIndicator( onRefresh: () async { @@ -77,7 +79,7 @@ class _Screen extends StatelessWidget { children: [ const WalletHeader(), const ActionsRow(), - if (!physicalBackupTested) ...[ + if (!physicalBackupTested || !vaultBackupTested) ...[ const Gap(24), const BackupAlertBanner(), // const Gap(24), diff --git a/lib/wallet/wallet_txs.dart b/lib/wallet/wallet_txs.dart index 7acdd189c..16b497339 100644 --- a/lib/wallet/wallet_txs.dart +++ b/lib/wallet/wallet_txs.dart @@ -232,17 +232,36 @@ class BackupAlertBanner extends StatelessWidget { final wallet = context.select((WalletBloc x) => x.state.wallet); final physicalBackupTested = context.select((WalletBloc x) => x.state.wallet.physicalBackupTested); + final vaultBackupTested = + context.select((WalletBloc x) => x.state.wallet.vaultBackupTested); - if (physicalBackupTested) return const SizedBox.shrink(); + if (physicalBackupTested && vaultBackupTested) { + return const SizedBox.shrink(); + } - return WarningBanner( - onTap: () { - context.push( - '/wallet-settings/backup-settings/backup-options', - extra: wallet.id, - ); - }, - info: 'Back up your wallet! Tap to test backup.', + return Column( + children: [ + if (!physicalBackupTested) + WarningBanner( + onTap: () { + context.push( + '/wallet-settings/backup-settings/backup-options/physical', + extra: wallet.id, + ); + }, + info: 'Physical backup not tested! Tap to test backup.', + ), + if (!vaultBackupTested) + WarningBanner( + onTap: () { + context.push( + '/wallet-settings/backup-settings/backup-options/encrypted', + extra: wallet.id, + ); + }, + info: 'Encrypted backup not tested! Tap to test backup.', + ), + ], ); } } From 8c88fff8492ca94c6c1ffb2bea447d133222fab3 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 17 Feb 2025 22:21:13 -0500 Subject: [PATCH 217/401] refactor: mark WalletSettingsListeners as const --- lib/wallet_settings/wallet_settings_page.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/wallet_settings/wallet_settings_page.dart b/lib/wallet_settings/wallet_settings_page.dart index cd7b4bcf5..ceebfdd2f 100644 --- a/lib/wallet_settings/wallet_settings_page.dart +++ b/lib/wallet_settings/wallet_settings_page.dart @@ -52,7 +52,7 @@ class WalletSettingsPage extends StatelessWidget { create: (BuildContext context) => createWalletSettingsCubit(wallet), ), ], - child: WalletSettingsListeners( + child: const WalletSettingsListeners( child: _Screen(), ), ); From 699efa6777aeca4aa78a9ee61e1a9376d2221587 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 17 Feb 2025 22:21:25 -0500 Subject: [PATCH 218/401] refactor: update success dialog message and navigation for backup recovery --- lib/wallet_settings/keychain_page.dart | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/wallet_settings/keychain_page.dart b/lib/wallet_settings/keychain_page.dart index db8e483ee..8741c3d78 100644 --- a/lib/wallet_settings/keychain_page.dart +++ b/lib/wallet_settings/keychain_page.dart @@ -783,14 +783,18 @@ class _SuccessDialog extends StatelessWidget { BBText.bodySmall( isRecovery ? 'Your wallet has been recovered successfully' - : 'Your wallet has been backed up successfully', + : 'Your wallet has been backed up successfully \n Please test your backup', textAlign: TextAlign.center, ), const Gap(24), FilledButton( onPressed: () { Navigator.of(context).pop(); - context.go('/home'); + context.go( + isRecovery + ? '/home' + : '/wallet-settings/backup-settings/recover-options/encrypted', + ); }, style: FilledButton.styleFrom( backgroundColor: context.colour.shadow, From 15bf138d4be7c3d47c4c0ad65bb670638b033529 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Thu, 20 Feb 2025 06:44:13 -0500 Subject: [PATCH 219/401] refactor: add bip85 dependency to Podfile.lock --- ios/Podfile.lock | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 258bad0ca..7c5f22682 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -7,6 +7,8 @@ PODS: - AppAuth/Core - bdk_flutter (0.31.2): - Flutter + - bip85 (0.0.1): + - Flutter - boltz (0.1.6) - DKImagePickerController/Core (4.3.9): - DKImagePickerController/ImageDataManager @@ -99,6 +101,7 @@ PODS: DEPENDENCIES: - bdk_flutter (from `.symlinks/plugins/bdk_flutter/ios`) + - bip85 (from `.symlinks/plugins/bip85/ios`) - boltz (from `.symlinks/plugins/boltz/ios`) - document_file_save_plus (from `.symlinks/plugins/document_file_save_plus/ios`) - file_picker (from `.symlinks/plugins/file_picker/ios`) @@ -134,6 +137,8 @@ SPEC REPOS: EXTERNAL SOURCES: bdk_flutter: :path: ".symlinks/plugins/bdk_flutter/ios" + bip85: + :path: ".symlinks/plugins/bip85/ios" boltz: :path: ".symlinks/plugins/boltz/ios" document_file_save_plus: @@ -176,6 +181,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: AppAuth: d4f13a8fe0baf391b2108511793e4b479691fb73 bdk_flutter: fb57a7400a7f3f181c5977bcdc2a5ef347ae4e7f + bip85: f656a7e6b23afda4960efb11c87d51d68e8be3db boltz: 6388ec2412f3753b63a9e65c97f87ea26f43bddc DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 @@ -188,7 +194,7 @@ SPEC CHECKSUMS: flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069 flutter_native_splash: f71420956eb811e6d310720fee915f1d42852e7a flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12 - google_sign_in_ios: 07375bfbf2620bc93a602c0e27160d6afc6ead38 + google_sign_in_ios: 4111e87aa5e24a4404f00ea13479f35e571969cc GoogleSignIn: d4281ab6cf21542b1cfaff85c191f230b399d2db GTMAppAuth: f69bd07d68cd3b766125f7e072c45d7340dea0de GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6 From 1ada102d92609f660d62ffaac3bf6ee3741abb37 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Thu, 20 Feb 2025 06:44:22 -0500 Subject: [PATCH 220/401] refactor: initialize bip85 library in main function --- lib/main.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/main.dart b/lib/main.dart index c731c0a06..cbcaefa2d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -20,6 +20,7 @@ import 'package:bb_mobile/settings/bloc/settings_cubit.dart'; import 'package:bb_mobile/styles.dart'; import 'package:bb_mobile/swap/listeners.dart'; import 'package:bb_mobile/swap/watcher_bloc/watchtxs_bloc.dart'; +import 'package:bip85/bip85.dart' as bip85; import 'package:boltz/boltz.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -40,6 +41,7 @@ Future main({bool fromTest = false}) async { await core.init(); await LibLwk.init(); await LibBoltz.init(); + bip85.LibBip85.init(); await dotenv.load(isOptional: true); Bloc.observer = BBlocObserver(); // await FlutterWindowManager.addFlags(FlutterWindowManager.FLAG_SECURE); From 634a63b70b0540bd0760d0e3b4150ece55ce92ed Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Thu, 20 Feb 2025 06:44:48 -0500 Subject: [PATCH 221/401] refactor: update backup key derivation to use BIP85 --- lib/_pkg/backup/_interface.dart | 60 +++++++++++++++++++++------------ 1 file changed, 39 insertions(+), 21 deletions(-) diff --git a/lib/_pkg/backup/_interface.dart b/lib/_pkg/backup/_interface.dart index d66cdac94..ab605a2ca 100644 --- a/lib/_pkg/backup/_interface.dart +++ b/lib/_pkg/backup/_interface.dart @@ -1,8 +1,12 @@ import 'dart:convert'; -import 'dart:math'; + import 'package:bb_mobile/_model/backup.dart'; +import 'package:bb_mobile/_model/wallet.dart'; import 'package:bb_mobile/_pkg/consts/configs.dart'; import 'package:bb_mobile/_pkg/error.dart'; +import 'package:bdk_flutter/bdk_flutter.dart'; +import 'package:bip85/bip85.dart' as bip85; +import 'package:flutter/foundation.dart'; import 'package:hex/hex.dart'; import 'package:recoverbull/recoverbull.dart' as recoverbull; @@ -10,23 +14,32 @@ abstract class IBackupManager { /// Encrypts a list of backups using BIP85 derivation Future<((String, String)?, Err?)> encryptBackups({ required List backups, + required List mnemonic, + required String network, }) async { if (backups.isEmpty) { return (null, Err('No backups provided')); } try { + final now = DateTime.now(); + final (derived, err) = await deriveBackupKey( + mnemonic, + network, + now, + ); final plaintext = json.encode(backups.map((i) => i.toJson()).toList()); - final key = await _deriveBackupKey(); - if (key == null) { + if (derived == null) { + debugPrint(err.toString()); return (null, Err('Failed to derive backup key')); } final encrypted = recoverbull.BackupService.createBackup( secret: utf8.encode(plaintext), - backupKey: key, + backupKey: derived, + createdAt: now, ); - return ((HEX.encode(key), encrypted), null); + return ((HEX.encode(derived), encrypted), null); } catch (e) { return (null, Err('Encryption failed: $e')); } @@ -35,13 +48,12 @@ abstract class IBackupManager { /// Decrypts an encrypted backup using the provided key Future<(List?, Err?)> decryptBackups({ required String encrypted, - required String backupKey, + required List backupKey, }) async { try { - final key = HEX.decode(backupKey); final plaintext = recoverbull.BackupService.restoreBackup( backup: encrypted, - backupKey: key, + backupKey: backupKey, ); return _parseBackups(plaintext); @@ -50,22 +62,28 @@ abstract class IBackupManager { } } - Future?> _deriveBackupKey() async { + Future<(List?, Err?)> deriveBackupKey( + List mnemonic, + String network, + DateTime now, + ) async { try { - final now = DateTime.now(); - final nowBytes = - utf8.encode(now.toUtc().millisecondsSinceEpoch.toString()); - - final secureRandom = Random.secure(); - final randomBytes = - List.generate(32, (_) => secureRandom.nextInt(256)); - final key = List.generate( - 32, - (i) => randomBytes[i] ^ nowBytes[i % nowBytes.length], + final descriptorSecretKey = await DescriptorSecretKey.create( + network: BBNetwork.fromString(network).toBdkNetwork(), + mnemonic: await Mnemonic.fromString(mnemonic.join(' ')), ); - return key; + // $index must remains within 0 to 2^31−1; ie. 0 to 2147483647 + final index = (now.toUtc().millisecondsSinceEpoch % 2147483647).abs(); + final path = "m/1608'/0'/$index"; + final key = bip85 + .derive( + xprv: descriptorSecretKey.toString().split('/*').first, + path: path, + ) + .sublist(0, 32); + return (key, null); } catch (e) { - return null; + return (null, Err(e.toString())); } } From b92b702838a88dd569968c920cf8b6bcd02ad90b Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Thu, 20 Feb 2025 06:45:33 -0500 Subject: [PATCH 222/401] refactor: add bip85 to Flutter FFI plugin list --- linux/flutter/generated_plugins.cmake | 1 + 1 file changed, 1 insertion(+) diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 32f64c052..407c9d317 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -8,6 +8,7 @@ list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + bip85 lwk ) From c42fed80318963024805718220e09a487e857fe8 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Thu, 20 Feb 2025 06:46:24 -0500 Subject: [PATCH 223/401] refactor: rename recoverBackupKey to fetchBackupKey in KeychainCubit --- lib/wallet_settings/bloc/keychain_cubit.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/wallet_settings/bloc/keychain_cubit.dart b/lib/wallet_settings/bloc/keychain_cubit.dart index d078bfe2d..f9e6a6dba 100644 --- a/lib/wallet_settings/bloc/keychain_cubit.dart +++ b/lib/wallet_settings/bloc/keychain_cubit.dart @@ -174,7 +174,7 @@ class KeychainCubit extends Cubit { try { emit(state.copyWith(loading: true, error: '')); - final backupKey = await _keyService.recoverBackupKey( + final backupKey = await _keyService.fetchBackupKey( backupId: state.backupId, password: state.secret, salt: state.backupSalt, From 74188439a5d8ed5508209f0618700f6b171e8d4a Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Thu, 20 Feb 2025 06:47:50 -0500 Subject: [PATCH 224/401] refactor: improve error handling in BackupSettingsCubit --- .../bloc/backup_settings_cubit.dart | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/lib/wallet_settings/bloc/backup_settings_cubit.dart b/lib/wallet_settings/bloc/backup_settings_cubit.dart index eb47afd8f..52b687e65 100644 --- a/lib/wallet_settings/bloc/backup_settings_cubit.dart +++ b/lib/wallet_settings/bloc/backup_settings_cubit.dart @@ -99,7 +99,7 @@ class BackupSettingsCubit extends Cubit { final (seed, err) = await _walletSensRepository.readSeed( fingerprintIndex: wallet.getRelatedSeedStorageString(), ); - return (seed, err?.toString()); + return (seed, err); } // physical backup & verification methods @@ -158,7 +158,7 @@ class BackupSettingsCubit extends Cubit { if (error != null || seed == null) { emit( state.copyWith( - errTestingBackup: error ?? 'Seed data not found', + errTestingBackup: error?.toString() ?? 'Seed data not found', loadingBackups: false, ), ); @@ -373,10 +373,13 @@ class BackupSettingsCubit extends Cubit { } _emitSafe(state.copyWith(savingBackups: true, errorSavingBackups: '')); - + if (_wallets.isEmpty) { + _handleLoadError('No wallets available for backup'); + return; + } final backups = await _createBackupsForAllWallets(); if (backups.isEmpty) { - _handleSaveError('No wallets available for backup'); + _handleSaveError('Failed to create backups'); return; } @@ -450,9 +453,13 @@ class BackupSettingsCubit extends Cubit { _emitSafe(state.copyWith(savingBackups: true, errorSavingBackups: '')); + if (_wallets.isEmpty) { + _handleLoadError('No wallets available for backup'); + return; + } final backups = await _createBackupsForAllWallets(); if (backups.isEmpty) { - _handleSaveError('No wallets available for backup'); + _handleSaveError('Failed to create backups'); return; } From cb318f0bbab32e97747d8e5dba09d919b19fff42 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Thu, 20 Feb 2025 06:48:53 -0500 Subject: [PATCH 225/401] refactor: enhance backup seed fetching --- .../bloc/backup_settings_cubit.dart | 31 +++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/lib/wallet_settings/bloc/backup_settings_cubit.dart b/lib/wallet_settings/bloc/backup_settings_cubit.dart index 52b687e65..9624ca729 100644 --- a/lib/wallet_settings/bloc/backup_settings_cubit.dart +++ b/lib/wallet_settings/bloc/backup_settings_cubit.dart @@ -518,12 +518,38 @@ class BackupSettingsCubit extends Cubit { ); } + Future<(Seed?, Err?)> _fetchMainSeed() async { + final mainWallet = _wallets.firstWhere( + (wallet) => + wallet.mainWallet && + wallet.type == BBWalletType.main && + wallet.baseWalletType == BaseWalletType.Bitcoin && + wallet.network == BBNetwork.Mainnet, + orElse: () => _wallets.firstWhere( + (wallet) => + wallet.mainWallet && + wallet.type == BBWalletType.main && + wallet.baseWalletType == BaseWalletType.Bitcoin && + wallet.network == BBNetwork.Testnet, + orElse: () => _wallets.first, + ), + ); + + return await _loadWalletSeed(mainWallet); + } + Future<((String, String)?, Err?)> _encryptBackups( List backups, ) async { try { + final (mainSeed, fetchMainMnemonicErr) = await _fetchMainSeed(); + if (fetchMainMnemonicErr != null || mainSeed == null) { + return (null, fetchMainMnemonicErr); + } final (encData, err) = await _manager.encryptBackups( backups: backups, + mnemonic: mainSeed.mnemonic.split(' '), + network: mainSeed.network.toString().toLowerCase(), ); if (err != null || encData == null) { @@ -564,7 +590,8 @@ class BackupSettingsCubit extends Cubit { } return backups; } catch (e) { - _emitBackupError('Failed to create backups: $e'); + debugPrint('Error creating backups: $e'); + _emitBackupError('Failed to create backups'); return []; } } @@ -778,7 +805,7 @@ class BackupSettingsCubit extends Cubit { final (backups, decryptErr) = await _manager.decryptBackups( encrypted: encrypted, - backupKey: backupKey, + backupKey: HEX.decode(backupKey), ); if (decryptErr != null || backups == null || backups.isEmpty) { From 20a76df76856c53df7de56ea6da83273e9815bd0 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Thu, 20 Feb 2025 06:49:57 -0500 Subject: [PATCH 226/401] feat: add recoverFromSecureStorage --- .../bloc/backup_settings_cubit.dart | 76 ++++++++++++++++++- 1 file changed, 75 insertions(+), 1 deletion(-) diff --git a/lib/wallet_settings/bloc/backup_settings_cubit.dart b/lib/wallet_settings/bloc/backup_settings_cubit.dart index 9624ca729..ea8db1881 100644 --- a/lib/wallet_settings/bloc/backup_settings_cubit.dart +++ b/lib/wallet_settings/bloc/backup_settings_cubit.dart @@ -600,7 +600,8 @@ class BackupSettingsCubit extends Cubit { try { final (seed, err) = await _loadWalletSeed(wallet); if (err != null || seed == null) { - _emitBackupError('Failed to read wallet ${wallet.name}: $err'); + debugPrint('Failed to read wallet ${wallet.name}: $err'); + _emitBackupError('Failed to read wallet ${wallet.name}'); return null; } @@ -704,6 +705,7 @@ class BackupSettingsCubit extends Cubit { ); if (mediaErr != null || loadedBackupMetaData == null) { + debugPrint('Error loading backups: ${mediaErr?.message}'); _handleLoadError("Failed to load backup data"); return; } @@ -834,6 +836,78 @@ class BackupSettingsCubit extends Cubit { ); } + Future recoverFromSecureStorage(String encrypted) async { + _emitSafe( + state.copyWith( + loadingBackups: true, + errorLoadingBackups: '', + ), + ); + + if (_wallets.isEmpty) { + _handleLoadError('Failed to get internal wallets'); + return; + } + + final (mainSeed, fetchMainSeedErr) = await _fetchMainSeed(); + + if (_wallets.isEmpty) { + _handleLoadError('No Wallets found'); + return; + } + + if (fetchMainSeedErr != null || mainSeed == null) { + debugPrint('Error fetching main seed: $fetchMainSeedErr'); + _handleLoadError('Failed to load seed data'); + return; + } + final decoded = jsonDecode(encrypted) as Map; + final createdAt = decoded['createdAt'] as int?; + if (createdAt == null) { + _handleLoadError('Invalid backup format'); + return; + } + final (backupKey, deriveBackupErr) = await _manager.deriveBackupKey( + mainSeed.mnemonic.split(' '), + mainSeed.network.toString(), + DateTime.fromMillisecondsSinceEpoch(createdAt), + ); + + if (backupKey == null) { + debugPrint('Error deriving backup key: $deriveBackupErr'); + _handleLoadError('Failed to derive backup key'); + return; + } + + final (backups, decryptErr) = await _manager.decryptBackups( + encrypted: encrypted, + backupKey: backupKey, + ); + if (decryptErr != null || backups == null || backups.isEmpty) { + _handleLoadError(decryptErr?.message ?? 'No wallets found in backup'); + return; + } + + for (final backup in backups) { + final err = await _processBackupRecovery(backup); + if (err != null) { + _handleLoadError(err.message); + return; + } + } + + // Update home state and sort wallets + locator().add(LoadWalletsFromStorage()); + await locator().sortWallets(); + _emitSafe( + state.copyWith( + loadingBackups: false, + loadedBackups: backups, + errorLoadingBackups: '', + ), + ); + } + Future _processBackupRecovery(Backup backup) async { final network = BBNetwork.fromString(backup.network); final layer = _getLayer(backup.layer); From c1e70dfbe715e3daeafa399bfcf93652a1101b79 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Thu, 20 Feb 2025 06:50:19 -0500 Subject: [PATCH 227/401] refactor: update _loadWalletSeed return type to include error handling --- lib/wallet_settings/bloc/backup_settings_cubit.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/wallet_settings/bloc/backup_settings_cubit.dart b/lib/wallet_settings/bloc/backup_settings_cubit.dart index ea8db1881..44704cb43 100644 --- a/lib/wallet_settings/bloc/backup_settings_cubit.dart +++ b/lib/wallet_settings/bloc/backup_settings_cubit.dart @@ -22,6 +22,7 @@ import 'package:bb_mobile/locator.dart'; import 'package:bb_mobile/wallet_settings/bloc/backup_settings_state.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:hex/hex.dart'; BackupSettingsCubit createBackupSettingsCubit({String? walletId}) { final appWalletsRepo = locator(); @@ -94,8 +95,7 @@ class BackupSettingsCubit extends Cubit { await super.close(); } - // Seed loading helper - Future<(Seed?, String?)> _loadWalletSeed(Wallet wallet) async { + Future<(Seed?, Err?)> _loadWalletSeed(Wallet wallet) async { final (seed, err) = await _walletSensRepository.readSeed( fingerprintIndex: wallet.getRelatedSeedStorageString(), ); From a7779b7fd15f6bd840bfb88b8dccc04314e798a3 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Thu, 20 Feb 2025 06:51:51 -0500 Subject: [PATCH 228/401] refactor: convert RecoveredBackupInfoPage to StatefulWidget --- .../encrypted_vault_backup.dart | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/lib/wallet_settings/encrypted_vault_backup.dart b/lib/wallet_settings/encrypted_vault_backup.dart index 75a0f6f8d..c5cdee26f 100644 --- a/lib/wallet_settings/encrypted_vault_backup.dart +++ b/lib/wallet_settings/encrypted_vault_backup.dart @@ -377,7 +377,7 @@ class _EncryptedVaultRecoverPageState extends State { } } -class RecoveredBackupInfoPage extends StatelessWidget { +class RecoveredBackupInfoPage extends StatefulWidget { const RecoveredBackupInfoPage({ super.key, required this.recoveredBackup, @@ -385,6 +385,26 @@ class RecoveredBackupInfoPage extends StatelessWidget { final Map recoveredBackup; + @override + State createState() => + _RecoveredBackupInfoPageState(); +} + +class _RecoveredBackupInfoPageState extends State { + late final BackupSettingsCubit _cubit; + + @override + void initState() { + super.initState(); + _cubit = createBackupSettingsCubit(); + } + + @override + void dispose() { + _cubit.close(); + super.dispose(); + } + Widget _buildErrorView(BuildContext context) { return Center( child: Column( @@ -440,7 +460,7 @@ class RecoveredBackupInfoPage extends StatelessWidget { @override Widget build(BuildContext context) { - if (recoveredBackup.isEmpty) { + if (widget.recoveredBackup.isEmpty) { return Scaffold( appBar: AppBar( elevation: 0, @@ -450,8 +470,8 @@ class RecoveredBackupInfoPage extends StatelessWidget { ), body: _buildErrorView(context), ); - } else if (recoveredBackup['id'] == null || - recoveredBackup['createdAt'] == null) { + } else if (widget.recoveredBackup['id'] == null || + widget.recoveredBackup['createdAt'] == null) { return Scaffold( appBar: AppBar( elevation: 0, From fad6b9f8dc9eeb427c516d943031ce8de92f0a3e Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Thu, 20 Feb 2025 06:53:28 -0500 Subject: [PATCH 229/401] feat: add button to recover key from secure storage --- .../encrypted_vault_backup.dart | 208 ++++++++++-------- 1 file changed, 121 insertions(+), 87 deletions(-) diff --git a/lib/wallet_settings/encrypted_vault_backup.dart b/lib/wallet_settings/encrypted_vault_backup.dart index c5cdee26f..88f991b63 100644 --- a/lib/wallet_settings/encrypted_vault_backup.dart +++ b/lib/wallet_settings/encrypted_vault_backup.dart @@ -483,111 +483,145 @@ class _RecoveredBackupInfoPageState extends State { ); } - return Scaffold( - appBar: AppBar( - elevation: 0, - automaticallyImplyLeading: false, - centerTitle: true, - flexibleSpace: BBAppBar(text: '', onBack: () => context.pop()), - ), - body: Padding( - padding: const EdgeInsets.symmetric(horizontal: 30), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - 'We have your file', - style: context.font.titleLarge!.copyWith( - fontWeight: FontWeight.w900, - ), + return BlocProvider.value( + value: _cubit, + child: BlocConsumer( + listenWhen: (previous, current) => + previous.errorLoadingBackups != current.errorLoadingBackups || + previous.loadingBackups != current.loadingBackups || + previous.loadedBackups != current.loadedBackups, + listener: (context, state) { + if (state.errorLoadingBackups.isNotEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + context.showToast(state.errorLoadingBackups), + ); + _cubit.clearError(); + return; + } + + if (!state.loadingBackups && state.loadedBackups.isNotEmpty) { + context.go('/home'); + } + }, + builder: (context, state) { + return Scaffold( + appBar: AppBar( + elevation: 0, + automaticallyImplyLeading: false, + centerTitle: true, + flexibleSpace: BBAppBar(text: '', onBack: () => context.pop()), ), - const Gap(20), - RichText( - textAlign: TextAlign.center, - text: TextSpan( + body: Padding( + padding: const EdgeInsets.symmetric(horizontal: 30), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, children: [ - TextSpan( - text: 'Backup ID:', - style: context.font.bodyMedium!.copyWith( - fontWeight: FontWeight.bold, + Text( + 'We have your file', + style: context.font.titleLarge!.copyWith( + fontWeight: FontWeight.w900, ), ), - TextSpan( - text: '${recoveredBackup['id']}', - style: context.font.bodyMedium!.copyWith( - fontWeight: FontWeight.bold, + const Gap(20), + RichText( + textAlign: TextAlign.center, + text: TextSpan( + children: [ + TextSpan( + text: 'Backup ID:', + style: context.font.bodyMedium!.copyWith( + fontWeight: FontWeight.bold, + ), + ), + TextSpan( + text: '${widget.recoveredBackup['id']}', + style: context.font.bodyMedium!.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ], ), ), - ], - ), - ), - const Gap(8), - RichText( - textAlign: TextAlign.center, - text: TextSpan( - children: [ - TextSpan( - text: 'Created at:', - style: context.font.bodyMedium!.copyWith( - fontWeight: FontWeight.bold, + const Gap(8), + RichText( + textAlign: TextAlign.center, + text: TextSpan( + children: [ + TextSpan( + text: 'Created at:', + style: context.font.bodyMedium!.copyWith( + fontWeight: FontWeight.bold, + ), + ), + TextSpan( + text: + ' ${DateFormat('MMM dd, yyyy HH:mm:ss').format(DateTime.fromMillisecondsSinceEpoch(widget.recoveredBackup['createdAt'] as int).toLocal())}', + style: context.font.bodyMedium!.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ], ), ), - TextSpan( - text: - ' ${DateFormat('MMM dd, yyyy HH:mm:ss').format(DateTime.fromMillisecondsSinceEpoch(recoveredBackup['createdAt'] as int).toLocal())}', + const Gap(16), + Text( + "Now let's decrypt", + textAlign: TextAlign.center, style: context.font.bodyMedium!.copyWith( fontWeight: FontWeight.bold, + color: NewColours.lightGray, ), ), - ], - ), - ), - const Gap(16), - Text( - "Now let's decrypt", - textAlign: TextAlign.center, - style: context.font.bodyMedium!.copyWith( - fontWeight: FontWeight.bold, - color: NewColours.lightGray, - ), - ), - const Gap(20), - FilledButton( - onPressed: () => context.push( - '/wallet-settings/backup-settings/keychain', - extra: ('', recoveredBackup), - ), - style: FilledButton.styleFrom( - backgroundColor: context.colour.shadow, - padding: const EdgeInsets.symmetric( - horizontal: 24, - vertical: 16, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - 'Decrypt Backup', - style: context.font.bodyMedium!.copyWith( - color: Colors.white, - fontWeight: FontWeight.w900, + const Gap(20), + FilledButton( + onPressed: () => context.push( + '/wallet-settings/backup-settings/keychain', + extra: ('', widget.recoveredBackup), + ), + style: FilledButton.styleFrom( + backgroundColor: context.colour.shadow, + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 16, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Decrypt Backup', + style: context.font.bodyMedium!.copyWith( + color: Colors.white, + fontWeight: FontWeight.w900, + ), + ), + const Gap(8), + const Icon( + Icons.arrow_forward, + color: Colors.white, + size: 20, + ), + ], ), ), - const Gap(8), - const Icon( - Icons.arrow_forward, - color: Colors.white, - size: 20, + const Gap(10), + InkWell( + onTap: () => _cubit.recoverFromSecureStorage( + jsonEncode(widget.recoveredBackup), + ), + child: const BBText.bodySmall( + 'Forgot your secret? Click to recover from storage!', + textAlign: TextAlign.center, + ), ), ], ), ), - ], - ), + ); + }, ), ); } From 69b61bc722daf37a4a860ee5a8cfc8db7584cb3b Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Thu, 20 Feb 2025 06:53:37 -0500 Subject: [PATCH 230/401] refactor: add import for JSON encoding/decoding --- lib/wallet_settings/encrypted_vault_backup.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/wallet_settings/encrypted_vault_backup.dart b/lib/wallet_settings/encrypted_vault_backup.dart index 88f991b63..8d9658c99 100644 --- a/lib/wallet_settings/encrypted_vault_backup.dart +++ b/lib/wallet_settings/encrypted_vault_backup.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'dart:io'; import 'package:bb_mobile/_ui/app_bar.dart'; From cc1b0360394c22b643f65832b2b25e848c06ad26 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Thu, 20 Feb 2025 17:31:44 -0500 Subject: [PATCH 231/401] feat: implement random index generation for backup key derivation --- lib/_pkg/backup/_interface.dart | 51 +++++++++++++++++++++++---------- 1 file changed, 36 insertions(+), 15 deletions(-) diff --git a/lib/_pkg/backup/_interface.dart b/lib/_pkg/backup/_interface.dart index ab605a2ca..eb345cfe2 100644 --- a/lib/_pkg/backup/_interface.dart +++ b/lib/_pkg/backup/_interface.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:math'; import 'package:bb_mobile/_model/backup.dart'; import 'package:bb_mobile/_model/wallet.dart'; @@ -22,12 +23,9 @@ abstract class IBackupManager { } try { - final now = DateTime.now(); - final (derived, err) = await deriveBackupKey( - mnemonic, - network, - now, - ); + final randomIndex = _deriveRandomIndex(); + final (derived, err) = + await deriveBackupKey(mnemonic, network, randomIndex); final plaintext = json.encode(backups.map((i) => i.toJson()).toList()); if (derived == null) { @@ -37,9 +35,12 @@ abstract class IBackupManager { final encrypted = recoverbull.BackupService.createBackup( secret: utf8.encode(plaintext), backupKey: derived, - createdAt: now, ); - return ((HEX.encode(derived), encrypted), null); + final encoded = jsonEncode({ + 'index': randomIndex, + 'encrypted': encrypted, + }); + return ((HEX.encode(derived), encoded), null); } catch (e) { return (null, Err('Encryption failed: $e')); } @@ -51,34 +52,54 @@ abstract class IBackupManager { required List backupKey, }) async { try { + final decodedBackup = jsonDecode(encrypted) as Map; + if (!decodedBackup.containsKey("encrypted")) { + return (null, Err('Invalid backup format')); + } + final plaintext = recoverbull.BackupService.restoreBackup( - backup: encrypted, + backup: decodedBackup['encrypted'] as String, backupKey: backupKey, ); - return _parseBackups(plaintext); + final decodedJson = jsonDecode(plaintext) as List; + final backups = decodedJson + .map((item) => Backup.fromJson(item as Map)) + .toList(); + + return (backups, null); } catch (e) { return (null, Err('Decryption failed: $e')); } } + int _deriveRandomIndex() { + final random = Uint8List(4); + final secureRandom = Random.secure(); + for (int i = 0; i < 4; i++) { + random[i] = secureRandom.nextInt(256); + } + final randomIndex = + ByteData.view(random.buffer).getUint32(0, Endian.little) & 0x7FFFFFFF; + + return randomIndex; + } + Future<(List?, Err?)> deriveBackupKey( List mnemonic, String network, - DateTime now, + int keyPathIndex, ) async { try { final descriptorSecretKey = await DescriptorSecretKey.create( network: BBNetwork.fromString(network).toBdkNetwork(), mnemonic: await Mnemonic.fromString(mnemonic.join(' ')), ); - // $index must remains within 0 to 2^31−1; ie. 0 to 2147483647 - final index = (now.toUtc().millisecondsSinceEpoch % 2147483647).abs(); - final path = "m/1608'/0'/$index"; + final key = bip85 .derive( xprv: descriptorSecretKey.toString().split('/*').first, - path: path, + path: "m/1608'/0'/$keyPathIndex", ) .sublist(0, 32); return (key, null); From 1795bc65c3aae16425e5de809b33dfa0a2bb3372 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Thu, 20 Feb 2025 17:31:51 -0500 Subject: [PATCH 232/401] refactor: remove unused _parseBackups method from IBackupManager interface --- lib/_pkg/backup/_interface.dart | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/lib/_pkg/backup/_interface.dart b/lib/_pkg/backup/_interface.dart index eb345cfe2..a0d185821 100644 --- a/lib/_pkg/backup/_interface.dart +++ b/lib/_pkg/backup/_interface.dart @@ -108,18 +108,6 @@ abstract class IBackupManager { } } - (List?, Err?) _parseBackups(String plaintext) { - try { - final decodedJson = jsonDecode(plaintext) as List; - final backups = decodedJson - .map((item) => Backup.fromJson(item as Map)) - .toList(); - return (backups, null); - } catch (e) { - return (null, Err('Failed to parse backups: $e')); - } - } - // Abstract methods to be implemented by concrete classes Future<(String?, Err?)> saveEncryptedBackup({ required String encrypted, From d3309b8dcbb225af1ea95b523d3e5449759212b4 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Thu, 20 Feb 2025 17:32:15 -0500 Subject: [PATCH 233/401] feat: enhance GoogleDriveBackupManager with silent sign-in and appDataFolder usage --- lib/_pkg/backup/google_drive.dart | 162 ++++++++++++++---------------- 1 file changed, 75 insertions(+), 87 deletions(-) diff --git a/lib/_pkg/backup/google_drive.dart b/lib/_pkg/backup/google_drive.dart index 357583c02..d93ccf700 100644 --- a/lib/_pkg/backup/google_drive.dart +++ b/lib/_pkg/backup/google_drive.dart @@ -10,7 +10,9 @@ import 'package:google_sign_in/google_sign_in.dart'; import 'package:googleapis/drive/v3.dart'; class GoogleDriveBackupManager extends IBackupManager { - static final _google = GoogleSignIn(scopes: [DriveApi.driveFileScope]); + static final _google = GoogleSignIn( + scopes: ['https://www.googleapis.com/auth/drive.appdata'], + ); static const _errorMessages = { 'connection': 'Google Sign-In was cancelled or failed. Please try again.', 'auth': 'Failed to authenticate with Google.', @@ -19,21 +21,24 @@ class GoogleDriveBackupManager extends IBackupManager { }; DriveApi? _api; - GoogleSignInAccount? _account; - Future<(String?, Err?)> connect() async { + Future<(DriveApi?, Err?)> connect() async { try { - final account = await _google.signIn(); - if (account == null) return (null, Err(_errorMessages['connection']!)); + // Try silent sign in first + var account = await _google.signInSilently(); + // If failed, try interactive sign in + if (account == null) { + account = await _google.signIn(); + if (account == null) return (null, Err(_errorMessages['connection']!)); + } final client = await _google.authenticatedClient(); if (client == null) return (null, Err(_errorMessages['auth']!)); _api = DriveApi(client); - _account = account; - - return await _setupBackupFolder(); + return (_api, null); } catch (e) { + debugPrint('Connection error: $e'); await disconnect(); return (null, Err('Connection error: $e')); } @@ -42,27 +47,44 @@ class GoogleDriveBackupManager extends IBackupManager { Future disconnect() async { await _google.disconnect(); _api = null; - _account = null; } - Future dispose() async { - await disconnect(); + // Helper method to check connection and reconnect if needed + Future<(DriveApi?, Err?)> _getApi() async { + if (_api == null) { + final (api, err) = await connect(); + if (err != null) return (null, err); + } + return (_api, null); } - // Helper method to ensure connection + // Update _withConnection to use _getApi Future<(T?, Err?)> _withConnection( Future<(T?, Err?)> Function(DriveApi api) operation, ) async { - if (_api == null) return (null, Err('Not connected')); + final (api, err) = await _getApi(); + if (err != null) return (null, err); + if (api == null) return (null, Err('Not connected')); try { - return await operation(_api!); + return await operation(api); } catch (e) { - await disconnect(); + // Only disconnect on auth errors + if (_isAuthError(e)) { + await disconnect(); + } return (null, Err('Operation failed: $e')); } } + bool _isAuthError(dynamic error) { + // Add logic to detect auth-related errors + final errorStr = error.toString().toLowerCase(); + return errorStr.contains('unauthorized') || + errorStr.contains('unauthenticated') || + errorStr.contains('invalid credentials'); + } + @override Future<(String?, Err?)> saveEncryptedBackup({ required String encrypted, @@ -71,19 +93,26 @@ class GoogleDriveBackupManager extends IBackupManager { return _withConnection((api) async { try { final data = jsonDecode(encrypted) as Map; - final backupId = data['id']?.toString(); + final encryptedData = data['encrypted'] as String; + final decodedEncrypted = + jsonDecode(encryptedData) as Map; + final backupId = decodedEncrypted['id']?.toString(); + if (backupId == null) return (null, Err('Invalid backup data')); final filename = '${DateTime.now().millisecondsSinceEpoch}_$backupId.json'; final file = File() ..name = filename - ..parents = [backupFolder]; + ..mimeType = 'application/json' + ..parents = ['appDataFolder']; await api.files.create( file, - uploadMedia: - Media(Stream.value(utf8.encode(encrypted)), encrypted.length), + uploadMedia: Media( + Stream.value(utf8.encode(encrypted)), + encrypted.length, + ), ); return (filename, null); @@ -93,52 +122,6 @@ class GoogleDriveBackupManager extends IBackupManager { }); } - Future<(String?, Err?)> _setupBackupFolder() async { - try { - const folderName = '.$defaultBackupPath'; - final existing = await _api!.files.list( - q: "name = '$folderName' and mimeType = 'application/vnd.google-apps.folder' and trashed = false", - spaces: 'drive', - $fields: 'files(id)', - ); - if (existing.files?.isNotEmpty == true) { - final backupFolderId = existing.files!.first.id; - return (backupFolderId, null); - } - - final folderMetadata = File() - ..name = folderName - ..mimeType = 'application/vnd.google-apps.folder' - ..appProperties = {'created': DateTime.now().toIso8601String()}; - final folder = await _api!.files.create(folderMetadata); - - final (success, err) = await _applyFolderPermissions(folder.id!); - if (!success) { - return (null, err); - } - - return (folder.id, null); - } catch (e) { - return (null, Err('Failed to initialize backup folder: $e')); - } - } - - Future<(bool, Err?)> _applyFolderPermissions(String folderId) async { - try { - await _api!.permissions.create( - Permission() - ..role = 'owner' - ..type = 'user' - ..emailAddress = _account!.email, - folderId, - transferOwnership: true, - ); - return (true, null); - } catch (e) { - return (false, Err('Failed to set folder permissions: $e')); - } - } - @override Future<(Map?, Err?)> loadEncryptedBackup({ required String encrypted, @@ -158,8 +141,8 @@ class GoogleDriveBackupManager extends IBackupManager { return _withConnection((api) async { try { final response = await api.files.list( - q: "'$backupFolder' in parents and trashed = false", - spaces: 'drive', + spaces: 'appDataFolder', + q: "mimeType='application/json' and trashed=false", // Add MIME type filter $fields: 'files(id, name, createdTime)', orderBy: 'createdTime desc', ); @@ -179,13 +162,15 @@ class GoogleDriveBackupManager extends IBackupManager { @override Future<(String?, Err?)> removeEncryptedBackup({ required String backupName, - String backupFolder = defaultBackupPath, + String backupFolder = + defaultBackupPath, // backupFolder is now ignored, always operates in appDataFolder }) async { return _withConnection((api) async { try { + // Find files in appDataFolder using spaces: 'appDataFolder' and query by name final files = await api.files.list( - q: "'$backupFolder' in parents and name = '$backupName' and trashed = false", - spaces: 'drive', + spaces: 'appDataFolder', + q: "name = '$backupName' and trashed = false", $fields: 'files(id)', ); @@ -194,7 +179,10 @@ class GoogleDriveBackupManager extends IBackupManager { return (null, Err('Backup not found')); } - await api.files.delete(firstFile.id!); + await api.files.update( + File()..trashed = true, // Set trashed to true to move to trash + firstFile.id!, + ); return (backupName, null); } catch (e) { return (null, Err('Failed to remove backup: $e')); @@ -203,22 +191,22 @@ class GoogleDriveBackupManager extends IBackupManager { } Future<(List?, Err?)> fetchMediaStream({required File file}) async { - if (_api == null) return (null, Err(_errorMessages['notConnected']!)); - - try { - final media = await _api!.files.get( - file.id!, - downloadOptions: DownloadOptions.fullMedia, - ) as Media; - - final bytes = await media.stream.fold>( - [], - (previous, element) => previous..addAll(element), - ); + return _withConnection((api) async { + try { + final media = await api.files.get( + file.id!, + downloadOptions: DownloadOptions.fullMedia, + ) as Media; + + final bytes = await media.stream.fold>( + [], + (previous, element) => previous..addAll(element), + ); - return (bytes, null); - } catch (e) { - return (null, Err('Failed to fetch backup data: $e')); - } + return (bytes, null); + } catch (e) { + return (null, Err('Failed to fetch backup data: $e')); + } + }); } } From b2e0d532634d159043108f8f46479821cd2756e8 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Thu, 20 Feb 2025 17:32:42 -0500 Subject: [PATCH 234/401] refactor: simplify backup file ID extraction in FileSystemBackupManager --- lib/_pkg/backup/local.dart | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/lib/_pkg/backup/local.dart b/lib/_pkg/backup/local.dart index 12c3f1304..ef6a7abbf 100644 --- a/lib/_pkg/backup/local.dart +++ b/lib/_pkg/backup/local.dart @@ -38,10 +38,6 @@ class FileSystemBackupManager extends IBackupManager { try { final decodeEncryptedFile = jsonDecode(utf8.decode(HEX.decode(encrypted))) as Map; - final id = decodeEncryptedFile['id']?.toString() ?? ''; - if (id.isEmpty) { - return (null, Err("Corrupted backup file")); - } return (decodeEncryptedFile, null); } catch (e) { return (null, Err('Failed to read encrypted backup: $e')); @@ -56,8 +52,9 @@ class FileSystemBackupManager extends IBackupManager { String backupFolder = defaultBackupPath, }) async { try { - final decodeEncryptedFile = jsonDecode(encrypted) as Map; - final backupId = decodeEncryptedFile['id']?.toString() ?? ''; + final decodeEncryptedFile = (jsonDecode(encrypted) + as Map)["encrypted"] as String; + final backupId = jsonDecode(decodeEncryptedFile)['id'] as String; final now = DateTime.now(); final formattedDate = now.millisecondsSinceEpoch; final filename = '${formattedDate}_$backupId.json'; From c9d6b12400dd4db5650775697d40bd1dd60a3ad9 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Thu, 20 Feb 2025 17:37:42 -0500 Subject: [PATCH 235/401] refactor: remove unused backupFolderId from BackupSettingsState --- lib/wallet_settings/bloc/backup_settings_state.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/wallet_settings/bloc/backup_settings_state.dart b/lib/wallet_settings/bloc/backup_settings_state.dart index a78d1544c..edf8d81bb 100644 --- a/lib/wallet_settings/bloc/backup_settings_state.dart +++ b/lib/wallet_settings/bloc/backup_settings_state.dart @@ -23,7 +23,6 @@ class BackupSettingsState with _$BackupSettingsState { @Default('') String errorSavingBackups, @Default('') String backupId, @Default('') String backupFolderPath, - @Default('') String backupFolderId, @Default('') String backupSalt, @Default('') String backupKey, @Default({}) Map latestRecoveredBackup, From d3630d10c1677959b4035451cff15c6fec4ed8d4 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Thu, 20 Feb 2025 17:38:23 -0500 Subject: [PATCH 236/401] refactor: streamline backup process and remove unused backupFolderId --- .../bloc/backup_settings_cubit.dart | 187 +++++++++--------- 1 file changed, 91 insertions(+), 96 deletions(-) diff --git a/lib/wallet_settings/bloc/backup_settings_cubit.dart b/lib/wallet_settings/bloc/backup_settings_cubit.dart index 44704cb43..5abf7df8f 100644 --- a/lib/wallet_settings/bloc/backup_settings_cubit.dart +++ b/lib/wallet_settings/bloc/backup_settings_cubit.dart @@ -423,7 +423,6 @@ class BackupSettingsCubit extends Cubit { _handleSaveError('Failed to extract backup salt'); return; } - _emitSafe( state.copyWith( backupId: backupId, @@ -438,9 +437,12 @@ class BackupSettingsCubit extends Cubit { String? _extractBackupSalt(String encrypted) { try { - final decoded = jsonDecode(encrypted) as Map; - return decoded['salt'] as String?; - } catch (_) { + final data = jsonDecode(encrypted) as Map; + final encryptedData = + jsonDecode(data['encrypted'] as String) as Map; + return encryptedData['salt'] as String?; + } catch (e) { + debugPrint('Failed to extract salt: $e'); return null; } } @@ -451,29 +453,27 @@ class BackupSettingsCubit extends Cubit { return; } - _emitSafe(state.copyWith(savingBackups: true, errorSavingBackups: '')); + _emitSafe( + state.copyWith( + savingBackups: true, + errorSavingBackups: '', + ), + ); if (_wallets.isEmpty) { _handleLoadError('No wallets available for backup'); return; } - final backups = await _createBackupsForAllWallets(); - if (backups.isEmpty) { - _handleSaveError('Failed to create backups'); - return; - } - if (state.backupFolderId.isEmpty) { - final (folderId, err) = await _driveManager.connect(); - if (err != null) { - _handleSaveError('Failed to connect to Google Drive: ${err.message}'); - return; - } - _emitSafe(state.copyWith(backupFolderId: folderId ?? '')); + final (api, connectErr) = await _driveManager.connect(); + if (connectErr != null) { + _handleSaveError(connectErr.message); + return; } - if (state.backupFolderId.isEmpty) { - _handleSaveError('Failed to initialize Google Drive backup folder'); + final backups = await _createBackupsForAllWallets(); + if (backups.isEmpty) { + _handleSaveError('Failed to create backups'); return; } @@ -482,7 +482,6 @@ class BackupSettingsCubit extends Cubit { _handleSaveError(encryptErr?.message ?? 'Encryption failed'); return; } - final backupSalt = _extractBackupSalt(encryptedData.$2); if (backupSalt == null) { _handleSaveError('Failed to extract backup salt'); @@ -491,7 +490,7 @@ class BackupSettingsCubit extends Cubit { final (filePath, saveErr) = await _driveManager.saveEncryptedBackup( encrypted: encryptedData.$2, - backupFolder: state.backupFolderId, + backupFolder: '', // No longer needed ); if (saveErr != null) { @@ -564,14 +563,15 @@ class BackupSettingsCubit extends Cubit { Future connectToGoogleDrive() async { try { - final (folderId, err) = await _driveManager.connect(); + final (api, err) = await _driveManager.connect(); if (err != null) { _emitBackupError('Failed to connect to Google Drive: ${err.message}'); + return; } - emit( + + _emitSafe( state.copyWith( - backupFolderId: folderId ?? '', errorSavingBackups: '', ), ); @@ -644,7 +644,11 @@ class BackupSettingsCubit extends Cubit { void disconnectGoogleDrive() { _driveManager.disconnect(); - emit(state.copyWith(backupFolderId: '')); + emit( + state.copyWith( + backupFolderPath: '', + ), + ); } // encrypted vault backup methods @@ -656,28 +660,22 @@ class BackupSettingsCubit extends Cubit { return; } - emit(state.copyWith(loadingBackups: true)); + _emitSafe( + state.copyWith( + loadingBackups: true, + ), + ); - // Connect if needed - if (state.backupFolderId.isEmpty) { - final (folderId, err) = await _driveManager.connect(); - if (err != null) { - _handleLoadError(err.message); - return; - } - emit(state.copyWith(backupFolderId: folderId ?? '')); - } + final (api, connectErr) = await _driveManager.connect(); + if (connectErr != null) { + _handleLoadError(connectErr.message); - // Ensure we have a folder ID - if (state.backupFolderId.isEmpty) { - _handleLoadError("Failed to initialize Google Drive folder"); return; } - // Rest of the existing code... final (availableBackups, err) = await _driveManager.loadAllEncryptedBackupFiles( - backupFolder: state.backupFolderId, + backupFolder: '', // No longer needed ); if (err != null) { @@ -784,7 +782,7 @@ class BackupSettingsCubit extends Cubit { } } - Future recoverBackup(String encrypted, String backupKey) async { + Future recoverWithKeyServer(String encrypted, String backupKey) async { _emitSafe( state.copyWith( loadingBackups: true, @@ -798,7 +796,8 @@ class BackupSettingsCubit extends Cubit { return; } final decoded = jsonDecode(encrypted) as Map; - final backupId = decoded['id'] as String?; + final backupId = + jsonDecode(decoded['encrypted'] as String)['id'] as String?; if (backupId == null) { _handleLoadError('Invalid backup format'); @@ -836,7 +835,7 @@ class BackupSettingsCubit extends Cubit { ); } - Future recoverFromSecureStorage(String encrypted) async { + Future recoverWithMnemonic(String encrypted) async { _emitSafe( state.copyWith( loadingBackups: true, @@ -844,68 +843,64 @@ class BackupSettingsCubit extends Cubit { ), ); - if (_wallets.isEmpty) { - _handleLoadError('Failed to get internal wallets'); - return; - } + try { + final data = jsonDecode(encrypted) as Map; - final (mainSeed, fetchMainSeedErr) = await _fetchMainSeed(); + final index = data['index'] as int?; - if (_wallets.isEmpty) { - _handleLoadError('No Wallets found'); - return; - } + if (index == null) { + _handleLoadError('Invalid backup format - missing index'); + return; + } - if (fetchMainSeedErr != null || mainSeed == null) { - debugPrint('Error fetching main seed: $fetchMainSeedErr'); - _handleLoadError('Failed to load seed data'); - return; - } - final decoded = jsonDecode(encrypted) as Map; - final createdAt = decoded['createdAt'] as int?; - if (createdAt == null) { - _handleLoadError('Invalid backup format'); - return; - } - final (backupKey, deriveBackupErr) = await _manager.deriveBackupKey( - mainSeed.mnemonic.split(' '), - mainSeed.network.toString(), - DateTime.fromMillisecondsSinceEpoch(createdAt), - ); + final (mainSeed, fetchMainSeedErr) = await _fetchMainSeed(); + if (fetchMainSeedErr != null || mainSeed == null) { + _handleLoadError('Failed to load seed data'); + return; + } - if (backupKey == null) { - debugPrint('Error deriving backup key: $deriveBackupErr'); - _handleLoadError('Failed to derive backup key'); - return; - } + final (backupKey, deriveErr) = await _manager.deriveBackupKey( + mainSeed.mnemonic.split(' '), + mainSeed.network.toString(), + index, + ); - final (backups, decryptErr) = await _manager.decryptBackups( - encrypted: encrypted, - backupKey: backupKey, - ); - if (decryptErr != null || backups == null || backups.isEmpty) { - _handleLoadError(decryptErr?.message ?? 'No wallets found in backup'); - return; - } + if (backupKey == null) { + debugPrint('Error deriving backup key: $deriveErr'); + _handleLoadError('Failed to derive backup key'); + return; + } - for (final backup in backups) { - final err = await _processBackupRecovery(backup); - if (err != null) { - _handleLoadError(err.message); + final (backups, decryptErr) = await _manager.decryptBackups( + encrypted: encrypted, + backupKey: backupKey, + ); + if (decryptErr != null || backups == null || backups.isEmpty) { + _handleLoadError(decryptErr?.message ?? 'No wallets found in backup'); return; } - } - // Update home state and sort wallets - locator().add(LoadWalletsFromStorage()); - await locator().sortWallets(); - _emitSafe( - state.copyWith( - loadingBackups: false, - loadedBackups: backups, - errorLoadingBackups: '', - ), - ); + for (final backup in backups) { + final err = await _processBackupRecovery(backup); + if (err != null) { + _handleLoadError(err.message); + return; + } + } + + // Update home state and sort wallets + locator().add(LoadWalletsFromStorage()); + await locator().sortWallets(); + _emitSafe( + state.copyWith( + loadingBackups: false, + loadedBackups: backups, + errorLoadingBackups: '', + ), + ); + } catch (e) { + _handleLoadError('Recovery failed: $e'); + } } Future _processBackupRecovery(Backup backup) async { From 5bc81359213f414dbdb5cd704fd56a3e8c74c0ba Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Thu, 20 Feb 2025 17:39:55 -0500 Subject: [PATCH 237/401] refactor: improve backup ID and salt extraction --- lib/wallet_settings/keychain_page.dart | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/lib/wallet_settings/keychain_page.dart b/lib/wallet_settings/keychain_page.dart index 8741c3d78..fef62bc2e 100644 --- a/lib/wallet_settings/keychain_page.dart +++ b/lib/wallet_settings/keychain_page.dart @@ -28,6 +28,23 @@ class KeychainBackupPage extends StatelessWidget { final Map backup; @override Widget build(BuildContext context) { + String? backupId; + String? backupSalt; + + if (backupKey != null && backupKey!.isNotEmpty) { + print("testing"); + print(backup); + backupId = backup['id']?.toString(); + backupSalt = backup['salt']?.toString(); + } else { + print("testing2"); + // Backup mode - extract from encrypted data + final encryptedData = + jsonDecode(backup["encrypted"] as String) as Map; + backupId = encryptedData["id"]?.toString(); + backupSalt = encryptedData["salt"] as String?; + } + return MultiBlocProvider( providers: [ BlocProvider( @@ -36,9 +53,9 @@ class KeychainBackupPage extends StatelessWidget { (backupKey == null || backupKey!.isEmpty) ? KeyChainPageState.recovery : KeyChainPageState.enter, - backup["id"] as String, + backupId ?? '', backupKey, - backup["salt"] as String, + backupSalt ?? '', ), ), BlocProvider.value( @@ -137,7 +154,7 @@ class _Screen extends StatelessWidget { !state.loading && !state.hasError) { if (encryptedBackup.isNotEmpty) { - context.read().recoverBackup( + context.read().recoverWithKeyServer( jsonEncode(encryptedBackup), state.backupKey, ); From 9b96ca012d2d542523bc8fe98f975629b55c8bf6 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Thu, 20 Feb 2025 17:40:05 -0500 Subject: [PATCH 238/401] refactor: remove debug print statements from KeychainBackupPage --- lib/wallet_settings/keychain_page.dart | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/wallet_settings/keychain_page.dart b/lib/wallet_settings/keychain_page.dart index fef62bc2e..e0280b6d0 100644 --- a/lib/wallet_settings/keychain_page.dart +++ b/lib/wallet_settings/keychain_page.dart @@ -32,13 +32,9 @@ class KeychainBackupPage extends StatelessWidget { String? backupSalt; if (backupKey != null && backupKey!.isNotEmpty) { - print("testing"); - print(backup); backupId = backup['id']?.toString(); backupSalt = backup['salt']?.toString(); } else { - print("testing2"); - // Backup mode - extract from encrypted data final encryptedData = jsonDecode(backup["encrypted"] as String) as Map; backupId = encryptedData["id"]?.toString(); From 53b88216d9979c254f01a7e69d50d9335c4c2603 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Thu, 20 Feb 2025 17:40:26 -0500 Subject: [PATCH 239/401] refactor: update recovered backup handling to use encrypted data --- .../encrypted_vault_backup.dart | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/lib/wallet_settings/encrypted_vault_backup.dart b/lib/wallet_settings/encrypted_vault_backup.dart index 8d9658c99..7e2057596 100644 --- a/lib/wallet_settings/encrypted_vault_backup.dart +++ b/lib/wallet_settings/encrypted_vault_backup.dart @@ -471,8 +471,8 @@ class _RecoveredBackupInfoPageState extends State { ), body: _buildErrorView(context), ); - } else if (widget.recoveredBackup['id'] == null || - widget.recoveredBackup['createdAt'] == null) { + } else if (widget.recoveredBackup['index'] == null || + widget.recoveredBackup['encrypted'] == null) { return Scaffold( appBar: AppBar( elevation: 0, @@ -505,6 +505,9 @@ class _RecoveredBackupInfoPageState extends State { } }, builder: (context, state) { + final recoveredBackupEncrypted = + jsonDecode(widget.recoveredBackup["encrypted"] as String) + as Map; return Scaffold( appBar: AppBar( elevation: 0, @@ -535,7 +538,7 @@ class _RecoveredBackupInfoPageState extends State { ), ), TextSpan( - text: '${widget.recoveredBackup['id']}', + text: '${recoveredBackupEncrypted['id']}', style: context.font.bodyMedium!.copyWith( fontWeight: FontWeight.bold, ), @@ -556,7 +559,7 @@ class _RecoveredBackupInfoPageState extends State { ), TextSpan( text: - ' ${DateFormat('MMM dd, yyyy HH:mm:ss').format(DateTime.fromMillisecondsSinceEpoch(widget.recoveredBackup['createdAt'] as int).toLocal())}', + ' ${DateFormat('MMM dd, yyyy HH:mm:ss').format(DateTime.fromMillisecondsSinceEpoch(recoveredBackupEncrypted['createdAt'] as int).toLocal())}', style: context.font.bodyMedium!.copyWith( fontWeight: FontWeight.bold, ), @@ -575,10 +578,12 @@ class _RecoveredBackupInfoPageState extends State { ), const Gap(20), FilledButton( - onPressed: () => context.push( - '/wallet-settings/backup-settings/keychain', - extra: ('', widget.recoveredBackup), - ), + onPressed: () => { + context.push( + '/wallet-settings/backup-settings/keychain', + extra: ('', widget.recoveredBackup), + ), + }, style: FilledButton.styleFrom( backgroundColor: context.colour.shadow, padding: const EdgeInsets.symmetric( @@ -610,7 +615,7 @@ class _RecoveredBackupInfoPageState extends State { ), const Gap(10), InkWell( - onTap: () => _cubit.recoverFromSecureStorage( + onTap: () => _cubit.recoverWithMnemonic( jsonEncode(widget.recoveredBackup), ), child: const BBText.bodySmall( From 68ba0671fc8ac3ba7f93d655245c819bb2e6733b Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Sat, 22 Feb 2025 22:58:39 -0500 Subject: [PATCH 240/401] refactor: add backupKey input type --- lib/wallet_settings/bloc/keychain_state.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/wallet_settings/bloc/keychain_state.dart b/lib/wallet_settings/bloc/keychain_state.dart index dc9604676..b1fbe9b6c 100644 --- a/lib/wallet_settings/bloc/keychain_state.dart +++ b/lib/wallet_settings/bloc/keychain_state.dart @@ -5,7 +5,7 @@ part 'keychain_state.freezed.dart'; enum KeyChainPageState { enter, confirm, recovery } -enum KeyChainInputType { pin, password } +enum KeyChainInputType { pin, password, backupKey } enum KeySecretState { saved, recovered, none } @@ -65,7 +65,7 @@ class KeychainState with _$KeychainState { bool get showButton => isValid; bool get hasError => error.isNotEmpty; bool get isRecovering => pageState == KeyChainPageState.recovery; - bool get canRecover => backupId.isNotEmpty && isValid && !loading; + bool get canRecoverKey => backupId.isNotEmpty && isValid && !loading; // Cache the compiled regex patterns static final _blacklistPattern = RegExp( From 38264de39b71911889c9cdcd517af80be1c9dfc9 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Sat, 22 Feb 2025 22:59:45 -0500 Subject: [PATCH 241/401] refactor: add updateBackupKey method and modify clickRecover logic --- lib/wallet_settings/bloc/keychain_cubit.dart | 21 +++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/lib/wallet_settings/bloc/keychain_cubit.dart b/lib/wallet_settings/bloc/keychain_cubit.dart index f9e6a6dba..68275808e 100644 --- a/lib/wallet_settings/bloc/keychain_cubit.dart +++ b/lib/wallet_settings/bloc/keychain_cubit.dart @@ -1,3 +1,4 @@ +import 'dart:math'; import 'package:bb_mobile/_pkg/consts/configs.dart'; import 'package:bb_mobile/wallet_settings/bloc/keychain_state.dart'; import 'package:flutter/material.dart'; @@ -72,6 +73,15 @@ class KeychainCubit extends Cubit { emit(state.copyWith(secret: value, error: '')); } + void updateBackupKey(String value) { + emit( + state.copyWith( + backupKey: value, + error: '', + ), + ); + } + void backspacePressed() { if (state.secret.isEmpty) return; emit( @@ -159,7 +169,16 @@ class KeychainCubit extends Cubit { emit(state.copyWith(backupId: id)); } - Future clickRecoverKey() async { + Future clickRecover() async { + if (state.backupKey.isNotEmpty) { + emit( + state.copyWith( + loading: false, + keySecretState: KeySecretState.recovered, + ), + ); + return; + } if (state.secret.length < 6) { state.inputType == KeyChainInputType.pin ? emit(state.copyWith(error: 'pin should be atleast 6 digits long')) From 57948f063eab663e345ae0bc225b7e837d0475fa Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Sat, 22 Feb 2025 23:00:04 -0500 Subject: [PATCH 242/401] refactor: rename recovery methods for clarity and update backup key handling --- .../bloc/backup_settings_cubit.dart | 35 +++---------------- 1 file changed, 5 insertions(+), 30 deletions(-) diff --git a/lib/wallet_settings/bloc/backup_settings_cubit.dart b/lib/wallet_settings/bloc/backup_settings_cubit.dart index 5abf7df8f..639270901 100644 --- a/lib/wallet_settings/bloc/backup_settings_cubit.dart +++ b/lib/wallet_settings/bloc/backup_settings_cubit.dart @@ -782,7 +782,7 @@ class BackupSettingsCubit extends Cubit { } } - Future recoverWithKeyServer(String encrypted, String backupKey) async { + Future recoverBackup(String encrypted, String backupKey) async { _emitSafe( state.copyWith( loadingBackups: true, @@ -835,7 +835,7 @@ class BackupSettingsCubit extends Cubit { ); } - Future recoverWithMnemonic(String encrypted) async { + Future recoverBackupKeyFromMnemonic(int? backupKeyIndex) async { _emitSafe( state.copyWith( loadingBackups: true, @@ -844,11 +844,7 @@ class BackupSettingsCubit extends Cubit { ); try { - final data = jsonDecode(encrypted) as Map; - - final index = data['index'] as int?; - - if (index == null) { + if (backupKeyIndex == null) { _handleLoadError('Invalid backup format - missing index'); return; } @@ -862,7 +858,7 @@ class BackupSettingsCubit extends Cubit { final (backupKey, deriveErr) = await _manager.deriveBackupKey( mainSeed.mnemonic.split(' '), mainSeed.network.toString(), - index, + backupKeyIndex, ); if (backupKey == null) { @@ -870,31 +866,10 @@ class BackupSettingsCubit extends Cubit { _handleLoadError('Failed to derive backup key'); return; } - - final (backups, decryptErr) = await _manager.decryptBackups( - encrypted: encrypted, - backupKey: backupKey, - ); - if (decryptErr != null || backups == null || backups.isEmpty) { - _handleLoadError(decryptErr?.message ?? 'No wallets found in backup'); - return; - } - - for (final backup in backups) { - final err = await _processBackupRecovery(backup); - if (err != null) { - _handleLoadError(err.message); - return; - } - } - - // Update home state and sort wallets - locator().add(LoadWalletsFromStorage()); - await locator().sortWallets(); _emitSafe( state.copyWith( loadingBackups: false, - loadedBackups: backups, + backupKey: HEX.encode(backupKey), errorLoadingBackups: '', ), ); From ec414d483fb779cce1efa2ddbad18ce2451fc1a9 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Sat, 22 Feb 2025 23:02:12 -0500 Subject: [PATCH 243/401] refactor: enhance backup key recovery & add AlertBox to show the key --- .../encrypted_vault_backup.dart | 54 ++++++++++++++++--- 1 file changed, 47 insertions(+), 7 deletions(-) diff --git a/lib/wallet_settings/encrypted_vault_backup.dart b/lib/wallet_settings/encrypted_vault_backup.dart index 7e2057596..c8949012e 100644 --- a/lib/wallet_settings/encrypted_vault_backup.dart +++ b/lib/wallet_settings/encrypted_vault_backup.dart @@ -10,6 +10,7 @@ import 'package:bb_mobile/wallet_settings/bloc/backup_settings_state.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:gap/gap.dart'; import 'package:go_router/go_router.dart'; @@ -490,7 +491,8 @@ class _RecoveredBackupInfoPageState extends State { listenWhen: (previous, current) => previous.errorLoadingBackups != current.errorLoadingBackups || previous.loadingBackups != current.loadingBackups || - previous.loadedBackups != current.loadedBackups, + previous.loadedBackups != current.loadedBackups || + previous.backupKey != current.backupKey, listener: (context, state) { if (state.errorLoadingBackups.isNotEmpty) { ScaffoldMessenger.of(context).showSnackBar( @@ -499,9 +501,47 @@ class _RecoveredBackupInfoPageState extends State { _cubit.clearError(); return; } - - if (!state.loadingBackups && state.loadedBackups.isNotEmpty) { - context.go('/home'); + if (!state.errorLoadingBackups.isNotEmpty && + !state.loadingBackups && + state.backupKey.isNotEmpty) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: BBText.titleLarge( + 'Secret key', + isBold: true, + ), + content: Row( + children: [ + Expanded( + child: Text( + state.backupKey, + style: context.font.bodySmall!.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ), + IconButton( + onPressed: () { + Clipboard.setData( + ClipboardData(text: state.backupKey), + ); + Navigator.of(context).pop(); + ScaffoldMessenger.of(context).showSnackBar( + context.showToast('Copied to clipboard'), + ); + }, + icon: const Icon( + Icons.copy, + color: Colors.black, + ), + ), + ], + ), + ), + ); + _cubit.clearError(); + return; } }, builder: (context, state) { @@ -615,11 +655,11 @@ class _RecoveredBackupInfoPageState extends State { ), const Gap(10), InkWell( - onTap: () => _cubit.recoverWithMnemonic( - jsonEncode(widget.recoveredBackup), + onTap: () => _cubit.recoverBackupKeyFromMnemonic( + widget.recoveredBackup['index'] as int?, ), child: const BBText.bodySmall( - 'Forgot your secret? Click to recover from storage!', + 'Forgot your secret? Click to recover', textAlign: TextAlign.center, ), ), From 18b1fac62181338b7269eef1e7f2b9898e10c548 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Sat, 22 Feb 2025 23:02:46 -0500 Subject: [PATCH 244/401] refactor: update key secret state condition --- lib/wallet_settings/keychain_page.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/wallet_settings/keychain_page.dart b/lib/wallet_settings/keychain_page.dart index e0280b6d0..a38ff5d5d 100644 --- a/lib/wallet_settings/keychain_page.dart +++ b/lib/wallet_settings/keychain_page.dart @@ -131,7 +131,7 @@ class _Screen extends StatelessWidget { if (state.isSecretConfirmed && !state.loading && !state.hasError && - state.keySecretState != KeySecretState.saved) { + state.keySecretState == KeySecretState.none) { context.read().secureKey(); } From bf2866bcfbf873ec69e046987e1532e909e9c1d8 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Sat, 22 Feb 2025 23:03:31 -0500 Subject: [PATCH 245/401] rename recoverWithKeyServer to recoverBackup --- lib/wallet_settings/keychain_page.dart | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/wallet_settings/keychain_page.dart b/lib/wallet_settings/keychain_page.dart index a38ff5d5d..46c990e3e 100644 --- a/lib/wallet_settings/keychain_page.dart +++ b/lib/wallet_settings/keychain_page.dart @@ -148,13 +148,13 @@ class _Screen extends StatelessWidget { if (state.keySecretState == KeySecretState.recovered && !state.loading && - !state.hasError) { - if (encryptedBackup.isNotEmpty) { - context.read().recoverWithKeyServer( - jsonEncode(encryptedBackup), - state.backupKey, - ); - } + !state.hasError && + encryptedBackup.isNotEmpty && + state.backupKey.isNotEmpty) { + context.read().recoverBackup( + jsonEncode(encryptedBackup), + state.backupKey, + ); } if (state.hasError) { From 3d7f75423da057a518e18a40613f65ff4cdf7c36 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Sat, 22 Feb 2025 23:04:05 -0500 Subject: [PATCH 246/401] refactor: improve recovery input prompts --- lib/wallet_settings/keychain_page.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/wallet_settings/keychain_page.dart b/lib/wallet_settings/keychain_page.dart index 46c990e3e..23ca82e17 100644 --- a/lib/wallet_settings/keychain_page.dart +++ b/lib/wallet_settings/keychain_page.dart @@ -311,13 +311,13 @@ class _RecoveryPage extends StatelessWidget { children: [ const Gap(50), BBText.titleLarge( - 'Enter Recovery ${inputType == KeyChainInputType.pin ? 'PIN' : 'Password'}', + 'Enter Recovery ${inputType == KeyChainInputType.pin ? 'PIN' : inputType == KeyChainInputType.password ? 'Password' : 'Key'}', textAlign: TextAlign.center, isBold: true, ), const Gap(8), BBText.bodySmall( - 'Enter the ${inputType == KeyChainInputType.pin ? 'PIN' : 'password'} you used to backup your keychain', + 'Enter the ${inputType == KeyChainInputType.pin ? 'PIN' : inputType == KeyChainInputType.password ? 'password' : 'backup key'} you used to backup your keychain', textAlign: TextAlign.center, ), const Gap(50), From 879c927b388d38cf626fd8edabbd93696fc7cea3 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Sat, 22 Feb 2025 23:04:43 -0500 Subject: [PATCH 247/401] refactor: update password/back key input handling --- lib/wallet_settings/keychain_page.dart | 27 ++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/lib/wallet_settings/keychain_page.dart b/lib/wallet_settings/keychain_page.dart index 23ca82e17..800086d0e 100644 --- a/lib/wallet_settings/keychain_page.dart +++ b/lib/wallet_settings/keychain_page.dart @@ -390,20 +390,31 @@ class _PinField extends StatelessWidget { class _PasswordField extends StatelessWidget { @override Widget build(BuildContext context) { - final state = context.select((KeychainCubit x) => x.state); - final error = state.getValidationError(); + final state = context.select( + (KeychainCubit x) => ( + x.state.secret, + x.state.obscure, + x.state.inputType, + x.state.backupKey, + x.state.getValidationError() + ), + ); + final (secret, obscure, inputType, backupKey, error) = state; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ BBTextInput.bigWithIcon( - value: state.secret, - onChanged: (value) => - context.read().updateInput(value), - obscure: state.obscure, - hint: 'Enter your password', + value: inputType == KeyChainInputType.backupKey ? backupKey : secret, + onChanged: (value) => inputType == KeyChainInputType.backupKey + ? context.read().updateBackupKey(value) + : context.read().updateInput(value), + obscure: obscure, + hint: inputType == KeyChainInputType.backupKey + ? 'Enter your backup key' + : 'Enter your password', rightIcon: Icon( - state.obscure ? Icons.visibility_off : Icons.visibility, + obscure ? Icons.visibility_off : Icons.visibility, color: context.colour.onPrimaryContainer, ), onRightTap: () => context.read().clickObscure(), From 8f9f50b1397fb4a394e29060446b786a598ca2bd Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Sat, 22 Feb 2025 23:05:04 -0500 Subject: [PATCH 248/401] refactor: enhance recovery button functionality and input type switching --- lib/wallet_settings/keychain_page.dart | 163 +++++++++++++++++-------- 1 file changed, 110 insertions(+), 53 deletions(-) diff --git a/lib/wallet_settings/keychain_page.dart b/lib/wallet_settings/keychain_page.dart index 800086d0e..13ae9d1da 100644 --- a/lib/wallet_settings/keychain_page.dart +++ b/lib/wallet_settings/keychain_page.dart @@ -302,7 +302,7 @@ class _RecoveryPage extends StatelessWidget { @override Widget build(BuildContext context) { return StackedPage( - bottomChildHeight: MediaQuery.of(context).size.height * 0.12, + bottomChildHeight: MediaQuery.of(context).size.height * 0.15, bottomChild: _RecoverButton(inputType: inputType), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 32), @@ -616,67 +616,124 @@ class _RecoverButton extends StatelessWidget { @override Widget build(BuildContext context) { - final canRecover = context.select((KeychainCubit x) => x.state.canRecover); - return AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - child: Column( - children: [ + return BlocSelector( + selector: (state) => state.canRecoverKey, + builder: (context, canRecoverKey) { + return AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: Column( + children: [ + _buildInputTypeSwitch(context), + const Gap(8), + _buildRecoverButton(context, canRecoverKey), + ], + ), + ); + }, + ); + } + + Widget _buildInputTypeSwitch(BuildContext context) { + return Column( + children: [ + // Switch between PIN and Password + InkWell( + onTap: () => _switchInputType(context), + child: BBText.bodySmall( + _getSwitchButtonText(), + isBold: true, + ), + ), + // Show backup key option only when not in backup key mode + if (inputType != KeyChainInputType.backupKey) ...[ + const Gap(8), InkWell( - onTap: () { - final cubit = context.read(); - if (inputType == KeyChainInputType.pin) { - cubit.updatePageState( - KeyChainInputType.password, - KeyChainPageState.recovery, - ); - } else { - cubit.updatePageState( - KeyChainInputType.pin, - KeyChainPageState.recovery, - ); - } - }, - child: BBText.bodySmall( - inputType == KeyChainInputType.pin - ? 'Use a password instead of a pin' - : 'Use a PIN instead of a password', + onTap: () => _switchToBackupKey(context), + child: const BBText.bodySmall( + 'Recover with backup key', + isBold: true, ), ), - const Gap(5), - FilledButton( - onPressed: () { - if (canRecover) context.read().clickRecoverKey(); - }, - style: FilledButton.styleFrom( - backgroundColor: - canRecover ? context.colour.shadow : context.colour.surface, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - 'Recover with ${inputType == KeyChainInputType.pin ? 'PIN' : 'password'}', - style: context.font.bodyMedium!.copyWith( - color: Colors.white, - fontWeight: FontWeight.w900, - ), - ), - const SizedBox(width: 8), - const Icon( - Icons.arrow_forward, - color: Colors.white, - size: 16, - ), - ], + ], + ], + ); + } + + Widget _buildRecoverButton(BuildContext context, bool canRecoverKey) { + return FilledButton( + onPressed: canRecoverKey + ? () => context.read().clickRecover() + : null, + style: FilledButton.styleFrom( + backgroundColor: _getButtonColor(context, canRecoverKey), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Recover with ${_getInputTypeText()}', + style: context.font.bodyMedium!.copyWith( + color: Colors.white, + fontWeight: FontWeight.w900, ), ), + const SizedBox(width: 8), + const Icon( + Icons.arrow_forward, + color: Colors.white, + size: 16, + ), ], ), ); } + + void _switchInputType(BuildContext context) { + final cubit = context.read(); + final newType = inputType == KeyChainInputType.pin + ? KeyChainInputType.password + : KeyChainInputType.pin; + cubit.updatePageState(newType, KeyChainPageState.recovery); + } + + void _switchToBackupKey(BuildContext context) { + context.read().updatePageState( + KeyChainInputType.backupKey, + KeyChainPageState.recovery, + ); + } + + Color _getButtonColor(BuildContext context, bool canRecoverKey) { + if (inputType == KeyChainInputType.backupKey || canRecoverKey) { + return context.colour.shadow; + } + return context.colour.surface; + } + + String _getSwitchButtonText() { + switch (inputType) { + case KeyChainInputType.pin: + return 'Use a password instead of a PIN'; + case KeyChainInputType.password: + return 'Use a PIN instead of a password'; + case KeyChainInputType.backupKey: + return 'Use a PIN instead of a backup key'; + } + } + + String _getInputTypeText() { + switch (inputType) { + case KeyChainInputType.pin: + return 'PIN'; + case KeyChainInputType.password: + return 'password'; + case KeyChainInputType.backupKey: + return 'backup key'; + } + } } class NumberButton extends StatefulWidget { From e0e3ea16c0c953e4b0296466b6d3f364e07480a4 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Sat, 22 Feb 2025 23:28:32 -0500 Subject: [PATCH 249/401] refactor: implement cooldown handling in keychain logic --- lib/wallet_settings/bloc/keychain_cubit.dart | 24 +++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/lib/wallet_settings/bloc/keychain_cubit.dart b/lib/wallet_settings/bloc/keychain_cubit.dart index 68275808e..1cb299f15 100644 --- a/lib/wallet_settings/bloc/keychain_cubit.dart +++ b/lib/wallet_settings/bloc/keychain_cubit.dart @@ -133,7 +133,7 @@ class KeychainCubit extends Cubit { Future secureKey() async { try { - emit(state.copyWith(loading: true, error: '')); + await serverInfo(); await _keyService.storeBackupKey( backupId: state.backupId, password: state.tempSecret, @@ -169,7 +169,29 @@ class KeychainCubit extends Cubit { emit(state.copyWith(backupId: id)); } + Future serverInfo() async { + emit(state.copyWith(loading: true)); + try { + final info = await _keyService.serverInfo(); + //TODO; Update the logic to check the cooldown + if (info.cooldown > 1) { + emit(state.copyWith(loading: false, error: 'Server is on cooldown')); + return; + } + if (state.tempSecret.length > info.secretMaxLength || + state.secret.length > info.secretMaxLength) { + emit(state.copyWith(loading: false, error: 'Secret is too long')); + } + return; + } catch (e) { + debugPrint('Failed to get server info: $e'); + emit(state.copyWith(loading: false, error: 'Failed to get server info')); + return; + } + } + Future clickRecover() async { + await serverInfo(); if (state.backupKey.isNotEmpty) { emit( state.copyWith( From bd5504fe0c804f5d479fd90ee688debe0dbc2988 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Sat, 22 Feb 2025 23:42:28 -0500 Subject: [PATCH 250/401] refactor: improve error handling for server info retrieval --- lib/wallet_settings/bloc/keychain_cubit.dart | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/wallet_settings/bloc/keychain_cubit.dart b/lib/wallet_settings/bloc/keychain_cubit.dart index 1cb299f15..c956b0dc5 100644 --- a/lib/wallet_settings/bloc/keychain_cubit.dart +++ b/lib/wallet_settings/bloc/keychain_cubit.dart @@ -173,7 +173,7 @@ class KeychainCubit extends Cubit { emit(state.copyWith(loading: true)); try { final info = await _keyService.serverInfo(); - //TODO; Update the logic to check the cooldown + //TODO; Update the logic to check the cooldown & server status if (info.cooldown > 1) { emit(state.copyWith(loading: false, error: 'Server is on cooldown')); return; @@ -185,7 +185,12 @@ class KeychainCubit extends Cubit { return; } catch (e) { debugPrint('Failed to get server info: $e'); - emit(state.copyWith(loading: false, error: 'Failed to get server info')); + emit( + state.copyWith( + loading: false, + error: 'Key server is not reachable!, Please try again later', + ), + ); return; } } From 0c5e375e2e396c0855b3cf53dd15ee72392ee4c5 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Sun, 23 Feb 2025 11:30:01 -0500 Subject: [PATCH 251/401] refactor: enhance server info retrieval and error handling --- lib/wallet_settings/bloc/keychain_cubit.dart | 92 ++++++++++---------- 1 file changed, 48 insertions(+), 44 deletions(-) diff --git a/lib/wallet_settings/bloc/keychain_cubit.dart b/lib/wallet_settings/bloc/keychain_cubit.dart index c956b0dc5..7d2a2c45a 100644 --- a/lib/wallet_settings/bloc/keychain_cubit.dart +++ b/lib/wallet_settings/bloc/keychain_cubit.dart @@ -1,4 +1,3 @@ -import 'dart:math'; import 'package:bb_mobile/_pkg/consts/configs.dart'; import 'package:bb_mobile/wallet_settings/bloc/keychain_state.dart'; import 'package:flutter/material.dart'; @@ -131,9 +130,37 @@ class KeychainCubit extends Cubit { emit(state.copyWith(isSecretConfirmed: true)); } + Future serverInfo() async { + emit(state.copyWith(loading: true)); + try { + final info = await _keyService.serverInfo(); + if (info.cooldown > 1) { + emit(state.copyWith(loading: false, error: 'Server is on cooldown')); + return false; + } + if (state.tempSecret.length > info.secretMaxLength || + state.secret.length > info.secretMaxLength) { + emit(state.copyWith(loading: false, error: 'Secret is too long')); + return false; + } + return true; + } catch (e) { + debugPrint('Failed to get server info: $e'); + emit( + state.copyWith( + loading: false, + error: 'Key server is not reachable! Please try again later', + ), + ); + return false; + } + } + Future secureKey() async { try { - await serverInfo(); + final isServerReady = await serverInfo(); + if (!isServerReady) return; + await _keyService.storeBackupKey( backupId: state.backupId, password: state.tempSecret, @@ -154,49 +181,10 @@ class KeychainCubit extends Cubit { } } - void clearSensitive() { - emit( - state.copyWith( - secret: '', - tempSecret: '', - isSecretConfirmed: false, - error: '', - ), - ); - } - - void setBackupId(String id) { - emit(state.copyWith(backupId: id)); - } - - Future serverInfo() async { - emit(state.copyWith(loading: true)); - try { - final info = await _keyService.serverInfo(); - //TODO; Update the logic to check the cooldown & server status - if (info.cooldown > 1) { - emit(state.copyWith(loading: false, error: 'Server is on cooldown')); - return; - } - if (state.tempSecret.length > info.secretMaxLength || - state.secret.length > info.secretMaxLength) { - emit(state.copyWith(loading: false, error: 'Secret is too long')); - } - return; - } catch (e) { - debugPrint('Failed to get server info: $e'); - emit( - state.copyWith( - loading: false, - error: 'Key server is not reachable!, Please try again later', - ), - ); - return; - } - } - Future clickRecover() async { - await serverInfo(); + final isServerReady = await serverInfo(); + if (!isServerReady) return; + if (state.backupKey.isNotEmpty) { emit( state.copyWith( @@ -206,6 +194,7 @@ class KeychainCubit extends Cubit { ); return; } + if (state.secret.length < 6) { state.inputType == KeyChainInputType.pin ? emit(state.copyWith(error: 'pin should be atleast 6 digits long')) @@ -243,4 +232,19 @@ class KeychainCubit extends Cubit { ); } } + + void clearSensitive() { + emit( + state.copyWith( + secret: '', + tempSecret: '', + isSecretConfirmed: false, + error: '', + ), + ); + } + + void setBackupId(String id) { + emit(state.copyWith(backupId: id)); + } } From 814c6a71fbadbb7903e05ad666c883c7662f8337 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Sun, 23 Feb 2025 11:47:05 -0500 Subject: [PATCH 252/401] refactor: code cleanup --- .../bloc/backup_settings_cubit.dart | 1123 ++++++++--------- 1 file changed, 548 insertions(+), 575 deletions(-) diff --git a/lib/wallet_settings/bloc/backup_settings_cubit.dart b/lib/wallet_settings/bloc/backup_settings_cubit.dart index 639270901..6fb1f2642 100644 --- a/lib/wallet_settings/bloc/backup_settings_cubit.dart +++ b/lib/wallet_settings/bloc/backup_settings_cubit.dart @@ -95,51 +95,141 @@ class BackupSettingsCubit extends Cubit { await super.close(); } - Future<(Seed?, Err?)> _loadWalletSeed(Wallet wallet) async { - final (seed, err) = await _walletSensRepository.readSeed( - fingerprintIndex: wallet.getRelatedSeedStorageString(), + // Public Methods (alphabetically) + void changePassword(String password) { + emit( + state.copyWith( + testBackupPassword: password, + errTestingBackup: '', + ), ); - return (seed, err); } - // physical backup & verification methods - - void _emitBackupState(Seed seed) { - if (_currentWallet == null) { - emit( + void clearError() => emit( state.copyWith( - errorLoadingBackups: 'No active wallet selected', - loadingBackups: false, + errTestingBackup: '', + errorLoadingBackups: '', + errorSavingBackups: '', ), ); - return; - } - - final words = seed.mnemonic.split(' '); - final shuffled = words.toList()..shuffle(); + void clearMnemonic() { emit( state.copyWith( - testMnemonicOrder: [], - mnemonic: words, - errTestingBackup: '', - password: seed - .getPassphraseFromIndex(_currentWallet!.sourceFingerprint) - .passphrase, - shuffledMnemonic: shuffled, - loadingBackups: false, + mnemonic: List.filled(12, ''), + testBackupPassword: '', ), ); } - void _emitBackupTestSuccessState() { + Future clearSensitive() async { + clearMnemonic(); emit( state.copyWith( - backupTested: true, - testingBackup: false, + mnemonic: [], + password: '', + shuffledMnemonic: [], + testMnemonicOrder: [], ), ); - clearSensitive(); + } + + Future connectToGoogleDrive() async { + try { + final (api, err) = await _driveManager.connect(); + if (err != null) { + _emitBackupError('Failed to connect to Google Drive: ${err.message}'); + return; + } + _emitSafe(state.copyWith(errorSavingBackups: '')); + } catch (e) { + _emitBackupError('Google Drive connection error: $e'); + } + } + + void disconnectGoogleDrive() { + _driveManager.disconnect(); + emit(state.copyWith(backupFolderPath: '')); + } + + Future fetchLatestBacup({bool forceRefresh = false}) async { + try { + if (!forceRefresh && state.loadedBackups.isNotEmpty) { + emit(state.copyWith(loadingBackups: false)); + return; + } + + _emitSafe( + state.copyWith( + loadingBackups: true, + ), + ); + + final (api, connectErr) = await _driveManager.connect(); + if (connectErr != null) { + _handleLoadError(connectErr.message); + + return; + } + + final (availableBackups, err) = + await _driveManager.loadAllEncryptedBackupFiles( + backupFolder: '', // No longer needed + ); + + if (err != null) { + debugPrint('Error loading backups: ${err.message}'); + _handleLoadError("Failed to get backup files"); + return; + } + + if (availableBackups != null && availableBackups.isNotEmpty) { + final latestBackup = availableBackups.reduce((a, b) { + final aTime = a.createdTime; + final bTime = b.createdTime; + if (aTime == null) return b; + if (bTime == null) return a; + return aTime.compareTo(bTime) > 0 ? a : b; + }); + final backupId = latestBackup.name?.split('_').last.split('.').first; + if (backupId == null) { + _handleLoadError("Corrupted backup file"); + return; + } + final (loadedBackupMetaData, mediaErr) = + await _driveManager.fetchMediaStream( + file: latestBackup, + ); + + if (mediaErr != null || loadedBackupMetaData == null) { + debugPrint('Error loading backups: ${mediaErr?.message}'); + _handleLoadError("Failed to load backup data"); + return; + } + + final (loadedBackup, err) = await _driveManager.loadEncryptedBackup( + encrypted: utf8.decode(loadedBackupMetaData), + ); + if (loadedBackup != null) { + emit( + state.copyWith( + loadingBackups: false, + latestRecoveredBackup: loadedBackup, + lastBackupAttempt: DateTime.now(), + ), + ); + return; + } else if ((err != null) || loadedBackup?["id"] == null) { + debugPrint('Error loading backups: ${err?.message}'); + _handleLoadError("Corrupted backup file"); + return; + } + } else { + _handleLoadError("Failed to get backup files"); + } + } catch (e) { + _handleLoadError('Failed to fetch backup: $e'); + } } Future loadBackupForVerification() async { @@ -168,204 +258,148 @@ class BackupSettingsCubit extends Cubit { _emitBackupState(seed); } - Future testBackupClicked() async { - emit(state.copyWith(testingBackup: true, errTestingBackup: '')); - - final words = state.testMneString(); - final password = state.testBackupPassword; - final seed = await _loadSeedData(_currentWallet!); + Future recoverBackup(String encrypted, String backupKey) async { + _emitSafe( + state.copyWith( + loadingBackups: true, + backupKey: backupKey, + errorLoadingBackups: '', + ), + ); - if (seed == null) { - emit( - state.copyWith( - errTestingBackup: 'Unable to load wallet data', - testingBackup: false, - ), - ); + if (backupKey.isEmpty) { + _handleLoadError('Backup key is missing'); return; } + final decoded = jsonDecode(encrypted) as Map; + final backupId = + jsonDecode(decoded['encrypted'] as String)['id'] as String?; - if (!_verifyWords(seed.mnemonic, words)) { - emit( - state.copyWith( - errTestingBackup: 'Invalid seed words', - testingBackup: false, - ), - ); + if (backupId == null) { + _handleLoadError('Invalid backup format'); return; } - if (!_verifyPassphrase(seed, password)) { - emit( - state.copyWith( - errTestingBackup: 'Invalid passphrase', - testingBackup: false, - ), - ); + final (backups, decryptErr) = await _manager.decryptBackups( + encrypted: encrypted, + backupKey: HEX.decode(backupKey), + ); + + if (decryptErr != null || backups == null || backups.isEmpty) { + _handleLoadError(decryptErr?.message ?? 'No wallets found in backup'); return; } - await _updateWalletBackupStatus( - _currentWallet!.copyWith( - physicalBackupTested: true, - lastPhysicalBackupTested: DateTime.now(), + for (final backup in backups) { + final err = await _processBackupRecovery(backup); + if (err != null) { + _handleLoadError(err.message); + return; + } + } + + // Update home state and sort wallets + locator().add(LoadWalletsFromStorage()); + await locator().sortWallets(); + + _emitSafe( + state.copyWith( + loadingBackups: false, + loadedBackups: backups, + errorLoadingBackups: '', ), ); - _emitBackupTestSuccessState(); } - bool _verifyWords(String seedMnemonic, String testWords) => - seedMnemonic == testWords; + Future recoverBackupKeyFromMnemonic(int? backupKeyIndex) async { + _emitSafe( + state.copyWith( + loadingBackups: true, + errorLoadingBackups: '', + ), + ); - bool _verifyPassphrase(Seed seed, String password) { - final storedPassphrase = seed - .getPassphraseFromIndex(_currentWallet!.sourceFingerprint) - .passphrase; - return storedPassphrase == password; - } + try { + if (backupKeyIndex == null) { + _handleLoadError('Invalid backup format - missing index'); + return; + } - Future _loadSeedData(Wallet wallet) async { - final (seed, err) = await _walletSensRepository.readSeed( - fingerprintIndex: wallet.getRelatedSeedStorageString(), - ); - if (err != null) { - emit(state.copyWith(errTestingBackup: err.toString())); - return null; - } - return seed; - } + final (mainSeed, fetchMainSeedErr) = await _fetchMainSeed(); + if (fetchMainSeedErr != null || mainSeed == null) { + _handleLoadError('Failed to load seed data'); + return; + } - Future _updateWalletBackupStatus(Wallet updatedWallet) async { - final service = - _appWalletsRepository.getWalletServiceById(updatedWallet.id); - if (service != null) { - await service.updateWallet( - updatedWallet, - updateTypes: [UpdateWalletTypes.settings], + final (backupKey, deriveErr) = await _manager.deriveBackupKey( + mainSeed.mnemonic.split(' '), + mainSeed.network.toString(), + backupKeyIndex, ); - _currentWallet = updatedWallet; + + if (backupKey == null) { + debugPrint('Error deriving backup key: $deriveErr'); + _handleLoadError('Failed to derive backup key'); + return; + } + _emitSafe( + state.copyWith( + loadingBackups: false, + backupKey: HEX.encode(backupKey), + errorLoadingBackups: '', + ), + ); + } catch (e) { + _handleLoadError('Recovery failed: $e'); } } - void word24Clicked(int shuffledIdx) { - emit(state.copyWith(errTestingBackup: '')); - final testMnemonic = state.testMnemonicOrder.toList(); - if (testMnemonic.length == 24) return; - - final (word, isSelected, actualIdx) = state.shuffleElementAt(shuffledIdx); - if (isSelected) return; - if (actualIdx != testMnemonic.length) { - invalidTestOrderClicked(); + Future recoverFromFs() async { + if (_filePicker == null) { return; } + final (file, error) = await _filePicker.pickFile(); - testMnemonic.add( - ( - word: word, - shuffleIdx: shuffledIdx, - selectedActualIdx: actualIdx, - ), - ); - - emit(state.copyWith(testMnemonicOrder: testMnemonic)); - } - - Future invalidTestOrderClicked() async { - emit( - state.copyWith( - testMnemonicOrder: [], - errTestingBackup: 'Invalid mnemonic order', - ), - ); - await Future.delayed(_kShuffleDelay); - final shuffled = state.mnemonic.toList()..shuffle(); - emit( - state.copyWith( - shuffledMnemonic: shuffled, - errTestingBackup: '', - ), - ); - } - - void changePassword(String password) { - emit( - state.copyWith( - testBackupPassword: password, - errTestingBackup: '', - ), - ); - } - - void wordClicked(int shuffledIdx) { - emit(state.copyWith(errTestingBackup: '')); - final testMnemonic = state.testMnemonicOrder.toList(); - if (testMnemonic.length == 12) return; - - final (word, isSelected, actualIdx) = state.shuffleElementAt(shuffledIdx); - if (isSelected) return; - if (actualIdx != testMnemonic.length) { - invalidTestOrderClicked(); + if (error != null) { + emit(state.copyWith(errorLoadingBackups: "Error picking file")); return; } - testMnemonic.add( - ( - word: word, - shuffleIdx: shuffledIdx, - selectedActualIdx: actualIdx, - ), + if (file == null || file.isEmpty) { + emit(state.copyWith(errorLoadingBackups: 'Corrupted backup file')); + return; + } + final (loadedBackup, err) = await _manager.loadEncryptedBackup( + encrypted: file, ); - - emit(state.copyWith(testMnemonicOrder: testMnemonic)); + if (loadedBackup != null) { + emit( + state.copyWith( + loadingBackups: false, + latestRecoveredBackup: loadedBackup, + lastBackupAttempt: DateTime.now(), + ), + ); + return; + } else if ((err != null) || loadedBackup?["id"] == null) { + debugPrint('Error loading backups: ${err?.message}'); + emit( + state.copyWith( + loadingBackups: false, + errorLoadingBackups: "Corrupted backup file", + ), + ); + return; + } } + Future refreshBackups() => fetchLatestBacup(forceRefresh: true); + Future resetBackupTested() async { await Future.delayed(_kDelayDuration); emit(state.copyWith(backupTested: false)); } - void clearMnemonic() { - emit( - state.copyWith( - mnemonic: List.filled(12, ''), - testBackupPassword: '', - ), - ); - } - - Future clearSensitive() async { - clearMnemonic(); - emit( - state.copyWith( - mnemonic: [], - password: '', - shuffledMnemonic: [], - testMnemonicOrder: [], - ), - ); - } - - // encrypted vault backup methods - void _emitBackupError(String message) { - emit( - state.copyWith( - savingBackups: false, - errorSavingBackups: message, - ), - ); - } - - bool _canStartBackup() { - final lastAttempt = state.lastBackupAttempt; - if (lastAttempt != null) { - final timeSinceLastBackup = DateTime.now().difference(lastAttempt); - if (timeSinceLastBackup < _kMinBackupInterval) { - return false; - } - } - return true; - } - Future saveFileSystemBackup() async { if (!_canStartBackup()) { _handleSaveError('Please wait before attempting another backup'); @@ -435,18 +469,6 @@ class BackupSettingsCubit extends Cubit { ); } - String? _extractBackupSalt(String encrypted) { - try { - final data = jsonDecode(encrypted) as Map; - final encryptedData = - jsonDecode(data['encrypted'] as String) as Map; - return encryptedData['salt'] as String?; - } catch (e) { - debugPrint('Failed to extract salt: $e'); - return null; - } - } - Future saveGoogleDriveBackup() async { if (!_canStartBackup()) { _handleSaveError('Please wait before attempting another backup'); @@ -517,75 +539,197 @@ class BackupSettingsCubit extends Cubit { ); } - Future<(Seed?, Err?)> _fetchMainSeed() async { - final mainWallet = _wallets.firstWhere( - (wallet) => - wallet.mainWallet && - wallet.type == BBWalletType.main && - wallet.baseWalletType == BaseWalletType.Bitcoin && - wallet.network == BBNetwork.Mainnet, - orElse: () => _wallets.firstWhere( - (wallet) => - wallet.mainWallet && - wallet.type == BBWalletType.main && - wallet.baseWalletType == BaseWalletType.Bitcoin && - wallet.network == BBNetwork.Testnet, - orElse: () => _wallets.first, - ), - ); + Future testBackupClicked() async { + emit(state.copyWith(testingBackup: true, errTestingBackup: '')); - return await _loadWalletSeed(mainWallet); - } + final words = state.testMneString(); + final password = state.testBackupPassword; + final seed = await _loadSeedData(_currentWallet!); - Future<((String, String)?, Err?)> _encryptBackups( - List backups, - ) async { - try { - final (mainSeed, fetchMainMnemonicErr) = await _fetchMainSeed(); - if (fetchMainMnemonicErr != null || mainSeed == null) { - return (null, fetchMainMnemonicErr); - } - final (encData, err) = await _manager.encryptBackups( - backups: backups, - mnemonic: mainSeed.mnemonic.split(' '), - network: mainSeed.network.toString().toLowerCase(), + if (seed == null) { + emit( + state.copyWith( + errTestingBackup: 'Unable to load wallet data', + testingBackup: false, + ), ); - - if (err != null || encData == null) { - return (null, err); - } - - return (encData, null); - } catch (e) { - return (null, Err(e.toString())); + return; } - } - - Future connectToGoogleDrive() async { - try { - final (api, err) = await _driveManager.connect(); - if (err != null) { - _emitBackupError('Failed to connect to Google Drive: ${err.message}'); - return; - } + if (!_verifyWords(seed.mnemonic, words)) { + emit( + state.copyWith( + errTestingBackup: 'Invalid seed words', + testingBackup: false, + ), + ); + return; + } - _emitSafe( + if (!_verifyPassphrase(seed, password)) { + emit( state.copyWith( - errorSavingBackups: '', + errTestingBackup: 'Invalid passphrase', + testingBackup: false, ), ); - } catch (e) { - _emitBackupError('Google Drive connection error: $e'); + return; } + + await _updateWalletBackupStatus( + _currentWallet!.copyWith( + physicalBackupTested: true, + lastPhysicalBackupTested: DateTime.now(), + ), + ); + _emitBackupTestSuccessState(); } - Future> _createBackupsForAllWallets() async { - final backups = []; + void word24Clicked(int shuffledIdx) { + emit(state.copyWith(errTestingBackup: '')); + final testMnemonic = state.testMnemonicOrder.toList(); + if (testMnemonic.length == 24) return; - try { - for (final wallet in _wallets) { - final backup = await _createBackupForWallet(wallet); + final (word, isSelected, actualIdx) = state.shuffleElementAt(shuffledIdx); + if (isSelected) return; + if (actualIdx != testMnemonic.length) { + invalidTestOrderClicked(); + return; + } + + testMnemonic.add( + ( + word: word, + shuffleIdx: shuffledIdx, + selectedActualIdx: actualIdx, + ), + ); + + emit(state.copyWith(testMnemonicOrder: testMnemonic)); + } + + void wordClicked(int shuffledIdx) { + emit(state.copyWith(errTestingBackup: '')); + final testMnemonic = state.testMnemonicOrder.toList(); + if (testMnemonic.length == 12) return; + + final (word, isSelected, actualIdx) = state.shuffleElementAt(shuffledIdx); + if (isSelected) return; + if (actualIdx != testMnemonic.length) { + invalidTestOrderClicked(); + return; + } + + testMnemonic.add( + ( + word: word, + shuffleIdx: shuffledIdx, + selectedActualIdx: actualIdx, + ), + ); + + emit(state.copyWith(testMnemonicOrder: testMnemonic)); + } + + // Private Helper Methods (alphabetically) + Future<(Wallet?, Err?)> _addOrUpdateWallet( + BBNetwork network, + BaseWalletType layer, + ScriptType script, + BBWalletType type, + String mnemonic, + String passphrase, + String publicDescriptors, + ) async { + final (seed, error) = + await _walletSensitiveCreate.mnemonicSeed(mnemonic, network); + if (seed == null) { + return (null, Err('Failed to create seed: $error')); + } + + try { + final error = await _walletSensRepository.newSeed(seed: seed); + + if (error != null && !error.message.toLowerCase().contains('exists')) { + return (null, Err(error.toString())); + } + final wallet = await _createWalletFromSeed( + layer, + seed, + passphrase, + script, + network, + type, + publicDescriptors, + ); + + if (wallet == null) { + return (null, Err('Failed to create wallet')); + } + + final walletRepoErr = await _walletsStorageRepository + .newWallet(wallet.copyWith(vaultBackupTested: true)); + if (walletRepoErr != null && + !walletRepoErr.message.toLowerCase().contains('exists')) { + return (null, Err(walletRepoErr.toString())); + } + return (wallet, null); + } catch (e) { + return (null, Err(e.toString())); + } + } + + bool _canStartBackup() { + final lastAttempt = state.lastBackupAttempt; + if (lastAttempt != null) { + final timeSinceLastBackup = DateTime.now().difference(lastAttempt); + if (timeSinceLastBackup < _kMinBackupInterval) { + return false; + } + } + return true; + } + + Future _createWalletFromSeed( + BaseWalletType layer, + Seed seed, + String passphrase, + ScriptType script, + BBNetwork network, + BBWalletType type, + String publicDescriptors, + ) async { + switch (layer) { + case BaseWalletType.Bitcoin: + final (wallet, error) = await _bdkSensitiveCreate.oneFromBIP39( + seed: seed, + passphrase: passphrase, + scriptType: script, + network: network, + walletType: type, + walletCreate: _walletCreate, + publicDescriptors: publicDescriptors, + ); + return wallet; + case BaseWalletType.Liquid: + final (wallet, error) = await _lwkSensitiveCreate.oneLiquidFromBIP39( + seed: seed, + passphrase: passphrase, + scriptType: script, + network: network, + walletType: type, + walletCreate: _walletCreate, + ); + return wallet; + } + } + + Future> _createBackupsForAllWallets() async { + final backups = []; + + try { + for (final wallet in _wallets) { + final backup = await _createBackupForWallet(wallet); if (backup != null) backups.add(backup); } return backups; @@ -642,240 +786,174 @@ class BackupSettingsCubit extends Cubit { } } - void disconnectGoogleDrive() { - _driveManager.disconnect(); + void _emitBackupError(String message) { emit( state.copyWith( - backupFolderPath: '', + savingBackups: false, + errorSavingBackups: message, ), ); } -// encrypted vault backup methods - - Future fetchLatestBacup({bool forceRefresh = false}) async { - try { - if (!forceRefresh && state.loadedBackups.isNotEmpty) { - emit(state.copyWith(loadingBackups: false)); - return; - } - - _emitSafe( + void _emitBackupState(Seed seed) { + if (_currentWallet == null) { + emit( state.copyWith( - loadingBackups: true, + errorLoadingBackups: 'No active wallet selected', + loadingBackups: false, ), ); + return; + } - final (api, connectErr) = await _driveManager.connect(); - if (connectErr != null) { - _handleLoadError(connectErr.message); + final words = seed.mnemonic.split(' '); + final shuffled = words.toList()..shuffle(); - return; - } + emit( + state.copyWith( + testMnemonicOrder: [], + mnemonic: words, + errTestingBackup: '', + password: seed + .getPassphraseFromIndex(_currentWallet!.sourceFingerprint) + .passphrase, + shuffledMnemonic: shuffled, + loadingBackups: false, + ), + ); + } - final (availableBackups, err) = - await _driveManager.loadAllEncryptedBackupFiles( - backupFolder: '', // No longer needed + void _emitBackupTestSuccessState() { + emit( + state.copyWith( + backupTested: true, + testingBackup: false, + ), + ); + clearSensitive(); + } + + void _emitSafe(BackupSettingsState newState) { + if (!isClosed) emit(newState); + } + + Future<((String, String)?, Err?)> _encryptBackups( + List backups) async { + try { + final (mainSeed, fetchMainMnemonicErr) = await _fetchMainSeed(); + if (fetchMainMnemonicErr != null || mainSeed == null) { + return (null, fetchMainMnemonicErr); + } + final (encData, err) = await _manager.encryptBackups( + backups: backups, + mnemonic: mainSeed.mnemonic.split(' '), + network: mainSeed.network.toString().toLowerCase(), ); - if (err != null) { - debugPrint('Error loading backups: ${err.message}'); - _handleLoadError("Failed to get backup files"); - return; + if (err != null || encData == null) { + return (null, err); } - if (availableBackups != null && availableBackups.isNotEmpty) { - final latestBackup = availableBackups.reduce((a, b) { - final aTime = a.createdTime; - final bTime = b.createdTime; - if (aTime == null) return b; - if (bTime == null) return a; - return aTime.compareTo(bTime) > 0 ? a : b; - }); - final backupId = latestBackup.name?.split('_').last.split('.').first; - if (backupId == null) { - _handleLoadError("Corrupted backup file"); - return; - } - final (loadedBackupMetaData, mediaErr) = - await _driveManager.fetchMediaStream( - file: latestBackup, - ); + return (encData, null); + } catch (e) { + return (null, Err(e.toString())); + } + } - if (mediaErr != null || loadedBackupMetaData == null) { - debugPrint('Error loading backups: ${mediaErr?.message}'); - _handleLoadError("Failed to load backup data"); - return; - } + String? _extractBackupSalt(String encrypted) { + try { + final data = jsonDecode(encrypted) as Map; + final encryptedData = + jsonDecode(data['encrypted'] as String) as Map; + return encryptedData['salt'] as String?; + } catch (e) { + debugPrint('Failed to extract salt: $e'); + return null; + } + } - final (loadedBackup, err) = await _driveManager.loadEncryptedBackup( - encrypted: utf8.decode(loadedBackupMetaData), - ); - if (loadedBackup != null) { - emit( - state.copyWith( - loadingBackups: false, - latestRecoveredBackup: loadedBackup, - lastBackupAttempt: DateTime.now(), - ), - ); - return; - } else if ((err != null) || loadedBackup?["id"] == null) { - debugPrint('Error loading backups: ${err?.message}'); - _handleLoadError("Corrupted backup file"); - return; - } - } else { - _handleLoadError("Failed to get backup files"); - } - } catch (e) { - _handleLoadError('Failed to fetch backup: $e'); - } - } - - Future refreshBackups() => fetchLatestBacup(forceRefresh: true); + Future<(Seed?, Err?)> _fetchMainSeed() async { + final mainWallet = _wallets.firstWhere( + (wallet) => + wallet.mainWallet && + wallet.type == BBWalletType.main && + wallet.baseWalletType == BaseWalletType.Bitcoin && + wallet.network == BBNetwork.Mainnet, + orElse: () => _wallets.firstWhere( + (wallet) => + wallet.mainWallet && + wallet.type == BBWalletType.main && + wallet.baseWalletType == BaseWalletType.Bitcoin && + wallet.network == BBNetwork.Testnet, + orElse: () => _wallets.first, + ), + ); - void clearError() => emit( - state.copyWith( - errTestingBackup: '', - errorLoadingBackups: '', - errorSavingBackups: '', - ), - ); + return await _loadWalletSeed(mainWallet); + } - Future recoverFromFs() async { - if (_filePicker == null) { - return; - } - final (file, error) = await _filePicker.pickFile(); + BaseWalletType? _getLayer(String layer) => switch (layer.toLowerCase()) { + 'bitcoin' => BaseWalletType.Bitcoin, + 'liquid' => BaseWalletType.Liquid, + _ => null + }; - if (error != null) { - emit(state.copyWith(errorLoadingBackups: "Error picking file")); - return; - } + ScriptType? _getScript(String script) => switch (script.toLowerCase()) { + 'bip44' => ScriptType.bip44, + 'bip49' => ScriptType.bip49, + 'bip84' => ScriptType.bip84, + _ => null + }; - if (file == null || file.isEmpty) { - emit(state.copyWith(errorLoadingBackups: 'Corrupted backup file')); - return; - } - final (loadedBackup, err) = await _manager.loadEncryptedBackup( - encrypted: file, - ); - if (loadedBackup != null) { - emit( - state.copyWith( - loadingBackups: false, - latestRecoveredBackup: loadedBackup, - lastBackupAttempt: DateTime.now(), - ), - ); - return; - } else if ((err != null) || loadedBackup?["id"] == null) { - debugPrint('Error loading backups: ${err?.message}'); - emit( - state.copyWith( - loadingBackups: false, - errorLoadingBackups: "Corrupted backup file", - ), - ); - return; - } - } + BBWalletType? _getWalletType(String type) => switch (type.toLowerCase()) { + 'main' => BBWalletType.main, + 'xpub' => BBWalletType.xpub, + 'words' => BBWalletType.words, + 'descriptors' => BBWalletType.descriptors, + 'coldcard' => BBWalletType.coldcard, + _ => null + }; - Future recoverBackup(String encrypted, String backupKey) async { + void _handleLoadError(String message, {bool loading = false}) { _emitSafe( state.copyWith( - loadingBackups: true, - backupKey: backupKey, - errorLoadingBackups: '', + errorLoadingBackups: message, + loadingBackups: loading, ), ); + } - if (backupKey.isEmpty) { - _handleLoadError('Backup key is missing'); - return; - } - final decoded = jsonDecode(encrypted) as Map; - final backupId = - jsonDecode(decoded['encrypted'] as String)['id'] as String?; - - if (backupId == null) { - _handleLoadError('Invalid backup format'); - return; - } - - final (backups, decryptErr) = await _manager.decryptBackups( - encrypted: encrypted, - backupKey: HEX.decode(backupKey), - ); - - if (decryptErr != null || backups == null || backups.isEmpty) { - _handleLoadError(decryptErr?.message ?? 'No wallets found in backup'); - return; - } - - for (final backup in backups) { - final err = await _processBackupRecovery(backup); - if (err != null) { - _handleLoadError(err.message); - return; - } - } - - // Update home state and sort wallets - locator().add(LoadWalletsFromStorage()); - await locator().sortWallets(); - + void _handleSaveError(String message, {bool saving = false}) { _emitSafe( state.copyWith( - loadingBackups: false, - loadedBackups: backups, - errorLoadingBackups: '', + errorSavingBackups: message, + savingBackups: saving, ), ); } - Future recoverBackupKeyFromMnemonic(int? backupKeyIndex) async { - _emitSafe( + Future invalidTestOrderClicked() async { + emit( state.copyWith( - loadingBackups: true, - errorLoadingBackups: '', + testMnemonicOrder: [], + errTestingBackup: 'Invalid mnemonic order', ), ); + await Future.delayed(_kShuffleDelay); + final shuffled = state.mnemonic.toList()..shuffle(); + emit( + state.copyWith( + shuffledMnemonic: shuffled, + errTestingBackup: '', + ), + ); + } - try { - if (backupKeyIndex == null) { - _handleLoadError('Invalid backup format - missing index'); - return; - } - - final (mainSeed, fetchMainSeedErr) = await _fetchMainSeed(); - if (fetchMainSeedErr != null || mainSeed == null) { - _handleLoadError('Failed to load seed data'); - return; - } - - final (backupKey, deriveErr) = await _manager.deriveBackupKey( - mainSeed.mnemonic.split(' '), - mainSeed.network.toString(), - backupKeyIndex, - ); - - if (backupKey == null) { - debugPrint('Error deriving backup key: $deriveErr'); - _handleLoadError('Failed to derive backup key'); - return; - } - _emitSafe( - state.copyWith( - loadingBackups: false, - backupKey: HEX.encode(backupKey), - errorLoadingBackups: '', - ), - ); - } catch (e) { - _handleLoadError('Recovery failed: $e'); - } + Future<(Seed?, Err?)> _loadWalletSeed(Wallet wallet) async { + final (seed, err) = await _walletSensRepository.readSeed( + fingerprintIndex: wallet.getRelatedSeedStorageString(), + ); + return (seed, err); } Future _processBackupRecovery(Backup backup) async { @@ -908,130 +986,25 @@ class BackupSettingsCubit extends Cubit { return err; } - BaseWalletType? _getLayer(String layer) => switch (layer.toLowerCase()) { - 'bitcoin' => BaseWalletType.Bitcoin, - 'liquid' => BaseWalletType.Liquid, - _ => null - }; - - ScriptType? _getScript(String script) => switch (script.toLowerCase()) { - 'bip44' => ScriptType.bip44, - 'bip49' => ScriptType.bip49, - 'bip84' => ScriptType.bip84, - _ => null - }; - - BBWalletType? _getWalletType(String type) => switch (type.toLowerCase()) { - 'main' => BBWalletType.main, - 'xpub' => BBWalletType.xpub, - 'words' => BBWalletType.words, - 'descriptors' => BBWalletType.descriptors, - 'coldcard' => BBWalletType.coldcard, - _ => null - }; - - Future<(Wallet?, Err?)> _addOrUpdateWallet( - BBNetwork network, - BaseWalletType layer, - ScriptType script, - BBWalletType type, - String mnemonic, - String passphrase, - String publicDescriptors, - ) async { - final (seed, error) = - await _walletSensitiveCreate.mnemonicSeed(mnemonic, network); - if (seed == null) { - return (null, Err('Failed to create seed: $error')); - } - - try { - final error = await _walletSensRepository.newSeed(seed: seed); - - if (error != null && !error.message.toLowerCase().contains('exists')) { - return (null, Err(error.toString())); - } - final wallet = await _createWalletFromSeed( - layer, - seed, - passphrase, - script, - network, - type, - publicDescriptors, + Future _updateWalletBackupStatus(Wallet updatedWallet) async { + final service = + _appWalletsRepository.getWalletServiceById(updatedWallet.id); + if (service != null) { + await service.updateWallet( + updatedWallet, + updateTypes: [UpdateWalletTypes.settings], ); - - if (wallet == null) { - return (null, Err('Failed to create wallet')); - } - - final walletRepoErr = await _walletsStorageRepository - .newWallet(wallet.copyWith(vaultBackupTested: true)); - if (walletRepoErr != null && - !walletRepoErr.message.toLowerCase().contains('exists')) { - return (null, Err(walletRepoErr.toString())); - } - return (wallet, null); - } catch (e) { - return (null, Err(e.toString())); - } - } - - Future _createWalletFromSeed( - BaseWalletType layer, - Seed seed, - String passphrase, - ScriptType script, - BBNetwork network, - BBWalletType type, - String publicDescriptors, - ) async { - switch (layer) { - case BaseWalletType.Bitcoin: - final (wallet, error) = await _bdkSensitiveCreate.oneFromBIP39( - seed: seed, - passphrase: passphrase, - scriptType: script, - network: network, - walletType: type, - walletCreate: _walletCreate, - publicDescriptors: publicDescriptors, - ); - return wallet; - case BaseWalletType.Liquid: - final (wallet, error) = await _lwkSensitiveCreate.oneLiquidFromBIP39( - seed: seed, - passphrase: passphrase, - scriptType: script, - network: network, - walletType: type, - walletCreate: _walletCreate, - ); - return wallet; + _currentWallet = updatedWallet; } } - // Helper method for safe state emission - void _emitSafe(BackupSettingsState newState) { - if (!isClosed) emit(newState); - } - - // Separate error handling methods for loading and saving - void _handleLoadError(String message, {bool loading = false}) { - _emitSafe( - state.copyWith( - errorLoadingBackups: message, - loadingBackups: loading, - ), - ); + bool _verifyPassphrase(Seed seed, String password) { + final storedPassphrase = seed + .getPassphraseFromIndex(_currentWallet!.sourceFingerprint) + .passphrase; + return storedPassphrase == password; } - void _handleSaveError(String message, {bool saving = false}) { - _emitSafe( - state.copyWith( - errorSavingBackups: message, - savingBackups: saving, - ), - ); - } + bool _verifyWords(String seedMnemonic, String testWords) => + seedMnemonic == testWords; } From 42e0a4801a14f37d3185dbd4c6b0d836fbd3f0db Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Sun, 23 Feb 2025 11:50:45 -0500 Subject: [PATCH 253/401] fix: name updated to _loadWalletSeed --- lib/wallet_settings/bloc/backup_settings_cubit.dart | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/wallet_settings/bloc/backup_settings_cubit.dart b/lib/wallet_settings/bloc/backup_settings_cubit.dart index 6fb1f2642..73f1c2810 100644 --- a/lib/wallet_settings/bloc/backup_settings_cubit.dart +++ b/lib/wallet_settings/bloc/backup_settings_cubit.dart @@ -544,8 +544,11 @@ class BackupSettingsCubit extends Cubit { final words = state.testMneString(); final password = state.testBackupPassword; - final seed = await _loadSeedData(_currentWallet!); - + final (seed, error) = await _loadWalletSeed(_currentWallet!); + if (error != null) { + debugPrint('Failed to read wallet ${_currentWallet!.name}: $error'); + return; + } if (seed == null) { emit( state.copyWith( @@ -838,7 +841,8 @@ class BackupSettingsCubit extends Cubit { } Future<((String, String)?, Err?)> _encryptBackups( - List backups) async { + List backups, + ) async { try { final (mainSeed, fetchMainMnemonicErr) = await _fetchMainSeed(); if (fetchMainMnemonicErr != null || mainSeed == null) { From aea7a49a4b59d0f2f6b1f3e73cded801aa4f751f Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Sun, 23 Feb 2025 11:59:42 -0500 Subject: [PATCH 254/401] refactor: rename backup managers and add backup deletion functionality --- .../bloc/backup_settings_cubit.dart | 157 +++++++++++------- 1 file changed, 100 insertions(+), 57 deletions(-) diff --git a/lib/wallet_settings/bloc/backup_settings_cubit.dart b/lib/wallet_settings/bloc/backup_settings_cubit.dart index 73f1c2810..adc914a2d 100644 --- a/lib/wallet_settings/bloc/backup_settings_cubit.dart +++ b/lib/wallet_settings/bloc/backup_settings_cubit.dart @@ -64,8 +64,8 @@ class BackupSettingsCubit extends Cubit { _appWalletsRepository = appWalletsRepository, _wallets = wallets, _currentWallet = currentWallet, - _manager = manager, - _driveManager = driveManager, + _fileSystemBackupManager = manager, + _googleDriveBackupManager = driveManager, _filePicker = locator(), _walletSensitiveCreate = walletSensitiveCreate, _bdkSensitiveCreate = bdkSensitiveCreate, @@ -83,8 +83,8 @@ class BackupSettingsCubit extends Cubit { final LWKSensitiveCreate _lwkSensitiveCreate; Wallet? _currentWallet; final List _wallets; - final FileSystemBackupManager _manager; - final GoogleDriveBackupManager _driveManager; + final FileSystemBackupManager _fileSystemBackupManager; + final GoogleDriveBackupManager _googleDriveBackupManager; final FilePick? _filePicker; static const _kDelayDuration = Duration(milliseconds: 800); static const _kShuffleDelay = Duration(milliseconds: 500); @@ -136,7 +136,7 @@ class BackupSettingsCubit extends Cubit { Future connectToGoogleDrive() async { try { - final (api, err) = await _driveManager.connect(); + final (api, err) = await _googleDriveBackupManager.connect(); if (err != null) { _emitBackupError('Failed to connect to Google Drive: ${err.message}'); return; @@ -148,11 +148,88 @@ class BackupSettingsCubit extends Cubit { } void disconnectGoogleDrive() { - _driveManager.disconnect(); + _googleDriveBackupManager.disconnect(); emit(state.copyWith(backupFolderPath: '')); } - Future fetchLatestBacup({bool forceRefresh = false}) async { + Future deleteFsBackup(String backupName) async { + if (state.backupFolderPath.isEmpty) { + emit(state.copyWith(errorSavingBackups: 'No backup to delete')); + return; + } + + final (deleted, err) = await _fileSystemBackupManager.removeEncryptedBackup( + backupName: backupName, + ); + + if (err != null) { + emit(state.copyWith(errorSavingBackups: 'Failed to delete backup')); + return; + } + + emit(state.copyWith(backupFolderPath: '')); + } + + Future deleteGoogleDriveBackup(String backupName) async { + if (state.backupFolderPath.isEmpty) { + emit(state.copyWith(errorSavingBackups: 'No backup to delete')); + return; + } + + final (deleted, err) = + await _googleDriveBackupManager.removeEncryptedBackup( + backupName: backupName, + ); + + if (err != null) { + emit(state.copyWith(errorSavingBackups: 'Failed to delete backup')); + return; + } + + emit(state.copyWith(backupFolderPath: '')); + } + + Future fetchFsBackup() async { + if (_filePicker == null) { + return; + } + final (file, error) = await _filePicker.pickFile(); + + if (error != null) { + emit(state.copyWith(errorLoadingBackups: "Error picking file")); + return; + } + + if (file == null || file.isEmpty) { + emit(state.copyWith(errorLoadingBackups: 'Corrupted backup file')); + return; + } + final (loadedBackup, err) = + await _fileSystemBackupManager.loadEncryptedBackup( + encrypted: file, + ); + if (loadedBackup != null) { + emit( + state.copyWith( + loadingBackups: false, + latestRecoveredBackup: loadedBackup, + lastBackupAttempt: DateTime.now(), + ), + ); + return; + } else if ((err != null) || loadedBackup?["id"] == null) { + debugPrint('Error loading backups: ${err?.message}'); + emit( + state.copyWith( + loadingBackups: false, + errorLoadingBackups: "Corrupted backup file", + ), + ); + return; + } + } + + Future fetchGoogleDriveBackup({bool forceRefresh = false}) async { try { if (!forceRefresh && state.loadedBackups.isNotEmpty) { emit(state.copyWith(loadingBackups: false)); @@ -165,7 +242,7 @@ class BackupSettingsCubit extends Cubit { ), ); - final (api, connectErr) = await _driveManager.connect(); + final (api, connectErr) = await _googleDriveBackupManager.connect(); if (connectErr != null) { _handleLoadError(connectErr.message); @@ -173,7 +250,7 @@ class BackupSettingsCubit extends Cubit { } final (availableBackups, err) = - await _driveManager.loadAllEncryptedBackupFiles( + await _googleDriveBackupManager.loadAllEncryptedBackupFiles( backupFolder: '', // No longer needed ); @@ -197,7 +274,7 @@ class BackupSettingsCubit extends Cubit { return; } final (loadedBackupMetaData, mediaErr) = - await _driveManager.fetchMediaStream( + await _googleDriveBackupManager.fetchMediaStream( file: latestBackup, ); @@ -207,7 +284,8 @@ class BackupSettingsCubit extends Cubit { return; } - final (loadedBackup, err) = await _driveManager.loadEncryptedBackup( + final (loadedBackup, err) = + await _googleDriveBackupManager.loadEncryptedBackup( encrypted: utf8.decode(loadedBackupMetaData), ); if (loadedBackup != null) { @@ -280,7 +358,7 @@ class BackupSettingsCubit extends Cubit { return; } - final (backups, decryptErr) = await _manager.decryptBackups( + final (backups, decryptErr) = await _fileSystemBackupManager.decryptBackups( encrypted: encrypted, backupKey: HEX.decode(backupKey), ); @@ -331,7 +409,8 @@ class BackupSettingsCubit extends Cubit { return; } - final (backupKey, deriveErr) = await _manager.deriveBackupKey( + final (backupKey, deriveErr) = + await _fileSystemBackupManager.deriveBackupKey( mainSeed.mnemonic.split(' '), mainSeed.network.toString(), backupKeyIndex, @@ -354,46 +433,8 @@ class BackupSettingsCubit extends Cubit { } } - Future recoverFromFs() async { - if (_filePicker == null) { - return; - } - final (file, error) = await _filePicker.pickFile(); - - if (error != null) { - emit(state.copyWith(errorLoadingBackups: "Error picking file")); - return; - } - - if (file == null || file.isEmpty) { - emit(state.copyWith(errorLoadingBackups: 'Corrupted backup file')); - return; - } - final (loadedBackup, err) = await _manager.loadEncryptedBackup( - encrypted: file, - ); - if (loadedBackup != null) { - emit( - state.copyWith( - loadingBackups: false, - latestRecoveredBackup: loadedBackup, - lastBackupAttempt: DateTime.now(), - ), - ); - return; - } else if ((err != null) || loadedBackup?["id"] == null) { - debugPrint('Error loading backups: ${err?.message}'); - emit( - state.copyWith( - loadingBackups: false, - errorLoadingBackups: "Corrupted backup file", - ), - ); - return; - } - } - - Future refreshBackups() => fetchLatestBacup(forceRefresh: true); + Future refreshGoogleDriveBackups() => + fetchGoogleDriveBackup(forceRefresh: true); Future resetBackupTested() async { await Future.delayed(_kDelayDuration); @@ -435,7 +476,8 @@ class BackupSettingsCubit extends Cubit { return; } - final (filePath, saveErr) = await _manager.saveEncryptedBackup( + final (filePath, saveErr) = + await _fileSystemBackupManager.saveEncryptedBackup( encrypted: encryptedData.$2, backupFolder: savePath, ); @@ -487,7 +529,7 @@ class BackupSettingsCubit extends Cubit { return; } - final (api, connectErr) = await _driveManager.connect(); + final (api, connectErr) = await _googleDriveBackupManager.connect(); if (connectErr != null) { _handleSaveError(connectErr.message); return; @@ -510,7 +552,8 @@ class BackupSettingsCubit extends Cubit { return; } - final (filePath, saveErr) = await _driveManager.saveEncryptedBackup( + final (filePath, saveErr) = + await _googleDriveBackupManager.saveEncryptedBackup( encrypted: encryptedData.$2, backupFolder: '', // No longer needed ); @@ -848,7 +891,7 @@ class BackupSettingsCubit extends Cubit { if (fetchMainMnemonicErr != null || mainSeed == null) { return (null, fetchMainMnemonicErr); } - final (encData, err) = await _manager.encryptBackups( + final (encData, err) = await _fileSystemBackupManager.encryptBackups( backups: backups, mnemonic: mainSeed.mnemonic.split(' '), network: mainSeed.network.toString().toLowerCase(), From 81339a08d1ebdd77c527e96ecf668d2d508ae898 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Sun, 23 Feb 2025 14:33:17 -0500 Subject: [PATCH 255/401] code cleanup --- lib/wallet_settings/bloc/backup_settings_cubit.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/wallet_settings/bloc/backup_settings_cubit.dart b/lib/wallet_settings/bloc/backup_settings_cubit.dart index adc914a2d..a14f641e4 100644 --- a/lib/wallet_settings/bloc/backup_settings_cubit.dart +++ b/lib/wallet_settings/bloc/backup_settings_cubit.dart @@ -95,7 +95,6 @@ class BackupSettingsCubit extends Cubit { await super.close(); } - // Public Methods (alphabetically) void changePassword(String password) { emit( state.copyWith( @@ -677,7 +676,6 @@ class BackupSettingsCubit extends Cubit { emit(state.copyWith(testMnemonicOrder: testMnemonic)); } - // Private Helper Methods (alphabetically) Future<(Wallet?, Err?)> _addOrUpdateWallet( BBNetwork network, BaseWalletType layer, From 5781040dc04d06317accc1027a7205a152644c36 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Sun, 23 Feb 2025 14:33:48 -0500 Subject: [PATCH 256/401] refactor: streamline keychain functionality and enhance error handling --- lib/wallet_settings/bloc/keychain_cubit.dart | 273 ++++++++++--------- 1 file changed, 147 insertions(+), 126 deletions(-) diff --git a/lib/wallet_settings/bloc/keychain_cubit.dart b/lib/wallet_settings/bloc/keychain_cubit.dart index 7d2a2c45a..0d78be186 100644 --- a/lib/wallet_settings/bloc/keychain_cubit.dart +++ b/lib/wallet_settings/bloc/keychain_cubit.dart @@ -8,12 +8,6 @@ import 'package:recoverbull/recoverbull.dart'; class KeychainCubit extends Cubit { KeychainCubit() : super(const KeychainState()) { shuffleAndEmit(); - _initKeyService(); - } - - late final KeyService _keyService; - - void _initKeyService() { if (keyServerUrl.isEmpty) { emit(state.copyWith(error: 'keychain api is not set')); return; @@ -24,81 +18,83 @@ class KeychainCubit extends Cubit { ); } - void shuffleAndEmit() { - final shuffledList = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]..shuffle(); - emit(state.copyWith(shuffledNumbers: shuffledList)); - } - - void clickObscure() { - emit( - state.copyWith(obscure: !state.obscure), - ); - } + late final KeyService _keyService; - void setChainState( - KeyChainPageState keyChainPageState, - String backupId, - String? backupKey, - String backupSalt, - ) { + void backspacePressed() { + if (state.secret.isEmpty) return; emit( state.copyWith( - pageState: keyChainPageState, - backupKey: backupKey ?? '', - backupId: backupId, - backupSalt: HEX.decode(backupSalt), + secret: state.secret.substring(0, state.secret.length - 1), + error: '', ), ); } - void updatePageState( - KeyChainInputType keyChainInputType, - KeyChainPageState keyChainPageState, - ) { + void clearSensitive() { emit( state.copyWith( - inputType: keyChainInputType, - pageState: keyChainPageState, - error: '', secret: '', tempSecret: '', isSecretConfirmed: false, - ), - ); - } - - void updateInput(String value) { - if (state.inputType == KeyChainInputType.pin && value.length > 6) return; - emit(state.copyWith(secret: value, error: '')); - } - - void updateBackupKey(String value) { - emit( - state.copyWith( - backupKey: value, error: '', ), ); } - void backspacePressed() { - if (state.secret.isEmpty) return; + void clickObscure() { emit( - state.copyWith( - secret: state.secret.substring(0, state.secret.length - 1), - error: '', - ), + state.copyWith(obscure: !state.obscure), ); } - void keyPressed(String key) { - if (state.secret.length >= 7) return; - emit( - state.copyWith( - secret: state.secret + key, - error: '', - ), - ); + Future clickRecover() async { + if (state.backupKey.isNotEmpty) { + emit( + state.copyWith( + loading: false, + keySecretState: KeySecretState.recovered, + ), + ); + return; + } + final isServerReady = await serverInfo(); + if (!isServerReady) return; + if (state.secret.length < 6) { + state.inputType == KeyChainInputType.pin + ? emit(state.copyWith(error: 'pin should be atleast 6 digits long')) + : emit( + state.copyWith( + error: 'password should be atleast 6 characters long', + ), + ); + return; + } + + try { + emit(state.copyWith(loading: true, error: '')); + + final backupKey = await _keyService.fetchBackupKey( + backupId: state.backupId, + password: state.secret, + salt: state.backupSalt, + ); + + emit( + state.copyWith( + backupKey: HEX.encode(backupKey), + loading: false, + keySecretState: KeySecretState.recovered, + ), + ); + } catch (e) { + debugPrint("Failed to recover backup key: $e"); + emit( + state.copyWith( + loading: false, + error: "Failed to recover backup key", + ), + ); + } } void confirmPressed() { @@ -130,32 +126,43 @@ class KeychainCubit extends Cubit { emit(state.copyWith(isSecretConfirmed: true)); } - Future serverInfo() async { - emit(state.copyWith(loading: true)); + Future deleteBackupKey() async { try { - final info = await _keyService.serverInfo(); - if (info.cooldown > 1) { - emit(state.copyWith(loading: false, error: 'Server is on cooldown')); - return false; - } - if (state.tempSecret.length > info.secretMaxLength || - state.secret.length > info.secretMaxLength) { - emit(state.copyWith(loading: false, error: 'Secret is too long')); - return false; - } - return true; + final isServerReady = await serverInfo(); + if (!isServerReady) return; + + await _keyService.trashBackupKey( + backupId: state.backupId, + password: state.secret, + salt: state.backupSalt, + ); + emit( + state.copyWith( + loading: false, + keySecretState: KeySecretState.deleted, + ), + ); } catch (e) { - debugPrint('Failed to get server info: $e'); + debugPrint('Failed to delete backup key: $e'); emit( state.copyWith( loading: false, - error: 'Key server is not reachable! Please try again later', + error: 'Failed to delete backup key', ), ); - return false; } } + void keyPressed(String key) { + if (state.secret.length >= 7) return; + emit( + state.copyWith( + secret: state.secret + key, + error: '', + ), + ); + } + Future secureKey() async { try { final isServerReady = await serverInfo(); @@ -181,70 +188,84 @@ class KeychainCubit extends Cubit { } } - Future clickRecover() async { - final isServerReady = await serverInfo(); - if (!isServerReady) return; - - if (state.backupKey.isNotEmpty) { - emit( - state.copyWith( - loading: false, - keySecretState: KeySecretState.recovered, - ), - ); - return; - } + void setBackupId(String id) { + emit(state.copyWith(backupId: id)); + } - if (state.secret.length < 6) { - state.inputType == KeyChainInputType.pin - ? emit(state.copyWith(error: 'pin should be atleast 6 digits long')) - : emit( - state.copyWith( - error: 'password should be atleast 6 characters long', - ), - ); - return; - } + void setChainState( + KeyChainPageState keyChainPageState, + String backupId, + String? backupKey, + String backupSalt, + ) { + emit( + state.copyWith( + pageState: keyChainPageState, + backupKey: backupKey ?? '', + backupId: backupId, + backupSalt: HEX.decode(backupSalt), + ), + ); + } - try { - emit(state.copyWith(loading: true, error: '')); + void shuffleAndEmit() { + final shuffledList = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]..shuffle(); + emit(state.copyWith(shuffledNumbers: shuffledList)); + } - final backupKey = await _keyService.fetchBackupKey( - backupId: state.backupId, - password: state.secret, - salt: state.backupSalt, - ); + void updateBackupKey(String value) { + emit( + state.copyWith( + backupKey: value, + error: '', + ), + ); + } - emit( - state.copyWith( - backupKey: HEX.encode(backupKey), - loading: false, - keySecretState: KeySecretState.recovered, - ), - ); - } catch (e) { - debugPrint("Failed to recover backup key: $e"); - emit( - state.copyWith( - loading: false, - error: "Failed to recover backup key", - ), - ); - } + void updateInput(String value) { + if (state.inputType == KeyChainInputType.pin && value.length > 6) return; + emit(state.copyWith(secret: value, error: '')); } - void clearSensitive() { + void updatePageState( + KeyChainInputType keyChainInputType, + KeyChainPageState keyChainPageState, + ) { emit( state.copyWith( + inputType: keyChainInputType, + pageState: keyChainPageState, + error: '', secret: '', tempSecret: '', isSecretConfirmed: false, - error: '', ), ); } - void setBackupId(String id) { - emit(state.copyWith(backupId: id)); + Future serverInfo() async { + emit(state.copyWith(loading: true)); + try { + final info = await _keyService.serverInfo(); + if (info.cooldown > 1) { + emit(state.copyWith(loading: false, error: 'Server is on cooldown')); + return false; + } + if (state.tempSecret.length > info.secretMaxLength || + state.secret.length > info.secretMaxLength) { + emit(state.copyWith(loading: false, error: 'Secret is too long')); + return false; + } + return true; + } catch (e) { + debugPrint('Failed to get server info: $e'); + emit( + state.copyWith( + loading: false, + error: 'Key server is not reachable! Please try again later', + ), + ); + return false; + } } } From 675f87ccf9e199ce3957703de49173e719e04397 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Sun, 23 Feb 2025 14:38:12 -0500 Subject: [PATCH 257/401] refactor: extend keychain state enums to include delete functionality --- lib/wallet_settings/bloc/keychain_state.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/wallet_settings/bloc/keychain_state.dart b/lib/wallet_settings/bloc/keychain_state.dart index b1fbe9b6c..f48b5ffc6 100644 --- a/lib/wallet_settings/bloc/keychain_state.dart +++ b/lib/wallet_settings/bloc/keychain_state.dart @@ -3,11 +3,11 @@ import 'package:freezed_annotation/freezed_annotation.dart'; part 'keychain_state.freezed.dart'; -enum KeyChainPageState { enter, confirm, recovery } +enum KeyChainPageState { enter, confirm, recovery, delete } enum KeyChainInputType { pin, password, backupKey } -enum KeySecretState { saved, recovered, none } +enum KeySecretState { none, saved, recovered, deleted } @freezed class KeychainState with _$KeychainState { From d25450f0b52b431a64a83bd926199c0953a0f60b Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 24 Feb 2025 00:19:36 -0500 Subject: [PATCH 258/401] refactor: update file picker to return File object and enhance error handling --- lib/_pkg/file_picker.dart | 8 ++------ lib/import/bloc/import_cubit.dart | 4 ++-- .../hardware_import_bloc/hardware_import_cubit.dart | 4 ++-- lib/settings/bloc/broadcasttx_cubit.dart | 8 +++++--- lib/wallet_settings/bloc/backup_settings_cubit.dart | 6 +++--- 5 files changed, 14 insertions(+), 16 deletions(-) diff --git a/lib/_pkg/file_picker.dart b/lib/_pkg/file_picker.dart index 5fffaede4..2e1720256 100644 --- a/lib/_pkg/file_picker.dart +++ b/lib/_pkg/file_picker.dart @@ -4,7 +4,7 @@ import 'package:bb_mobile/_pkg/error.dart'; import 'package:file_picker/file_picker.dart'; class FilePick { - Future<(String?, Err?)> pickFile() async { + Future<(File?, Err?)> pickFile() async { try { final result = await FilePicker.platform.pickFiles(); if (result == null) throw 'No file selected'; @@ -13,12 +13,8 @@ class FilePick { if (path == null) throw 'No data selected'; final file = File(path); - final dataStr = await file.readAsString(); - // final bytes = result.files.first.bytes; - // final dataStr = utf8.decode(bytes); - - return (dataStr, null); + return (file, null); } catch (e) { return (null, Err(e.toString())); } diff --git a/lib/import/bloc/import_cubit.dart b/lib/import/bloc/import_cubit.dart index 63dcf2adc..5a017c5b4 100644 --- a/lib/import/bloc/import_cubit.dart +++ b/lib/import/bloc/import_cubit.dart @@ -350,7 +350,7 @@ class ImportWalletCubit extends Cubit { ), ); final (file, err) = await _filePicker.pickFile(); - if (err != null) { + if (err != null || file == null) { emit( state.copyWith( importStep: ImportSteps.importXpub, @@ -361,7 +361,7 @@ class ImportWalletCubit extends Cubit { return; } - final ccObj = jsonDecode(file!) as Map; + final ccObj = jsonDecode(await file.readAsString()) as Map; final coldcard = ColdCard.fromJson(ccObj); diff --git a/lib/import/hardware_import_bloc/hardware_import_cubit.dart b/lib/import/hardware_import_bloc/hardware_import_cubit.dart index 9b012e541..c5cf7d5d6 100644 --- a/lib/import/hardware_import_bloc/hardware_import_cubit.dart +++ b/lib/import/hardware_import_bloc/hardware_import_cubit.dart @@ -61,12 +61,12 @@ class HardwareImportCubit extends Cubit { Future selectFile() async { final (file, err) = await _filePicker.pickFile(); - if (err != null) { + if (err != null || file == null) { emit(state.copyWith(errScanningInput: err.toString())); return; } - emit(state.copyWith(inputText: file!)); + emit(state.copyWith(inputText: await file.readAsString())); _processInput(); } diff --git a/lib/settings/bloc/broadcasttx_cubit.dart b/lib/settings/bloc/broadcasttx_cubit.dart index e257ce237..f7357c0e4 100644 --- a/lib/settings/bloc/broadcasttx_cubit.dart +++ b/lib/settings/bloc/broadcasttx_cubit.dart @@ -83,13 +83,15 @@ class BroadcastTxCubit extends Cubit { await clearErrors(); emit(state.copyWith(loadingFile: true, errLoadingFile: '')); final (file, err) = await _filePicker.pickFile(); - if (err != null) { + if (err != null || file == null) { emit(state.copyWith(loadingFile: false, errLoadingFile: err.toString())); return; } - final tx = - file!.replaceAll('\n', '').replaceAll('\r', '').replaceAll(' ', ''); + final tx = (await file.readAsString()) + .replaceAll('\n', '') + .replaceAll('\r', '') + .replaceAll(' ', ''); emit(state.copyWith(loadingFile: false, tx: tx)); } diff --git a/lib/wallet_settings/bloc/backup_settings_cubit.dart b/lib/wallet_settings/bloc/backup_settings_cubit.dart index a14f641e4..198d58e6e 100644 --- a/lib/wallet_settings/bloc/backup_settings_cubit.dart +++ b/lib/wallet_settings/bloc/backup_settings_cubit.dart @@ -198,14 +198,14 @@ class BackupSettingsCubit extends Cubit { emit(state.copyWith(errorLoadingBackups: "Error picking file")); return; } - - if (file == null || file.isEmpty) { + final fileContent = await file?.readAsString(); + if (file == null || fileContent == null) { emit(state.copyWith(errorLoadingBackups: 'Corrupted backup file')); return; } final (loadedBackup, err) = await _fileSystemBackupManager.loadEncryptedBackup( - encrypted: file, + encrypted: fileContent, ); if (loadedBackup != null) { emit( From ff2d3f13d80beb4c3867bb5965604cb0a5419915 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 24 Feb 2025 00:20:25 -0500 Subject: [PATCH 259/401] refactor: update removeEncryptedBackup method to use path parameter instead of backupName --- lib/_pkg/backup/_interface.dart | 3 +-- lib/_pkg/backup/google_drive.dart | 10 ++++------ lib/_pkg/backup/local.dart | 5 ++--- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/lib/_pkg/backup/_interface.dart b/lib/_pkg/backup/_interface.dart index a0d185821..a53ef9cde 100644 --- a/lib/_pkg/backup/_interface.dart +++ b/lib/_pkg/backup/_interface.dart @@ -119,7 +119,6 @@ abstract class IBackupManager { }); Future<(String?, Err?)> removeEncryptedBackup({ - required String backupName, - String backupFolder = defaultBackupPath, + required String path, }); } diff --git a/lib/_pkg/backup/google_drive.dart b/lib/_pkg/backup/google_drive.dart index d93ccf700..1e6dc08f7 100644 --- a/lib/_pkg/backup/google_drive.dart +++ b/lib/_pkg/backup/google_drive.dart @@ -142,7 +142,7 @@ class GoogleDriveBackupManager extends IBackupManager { try { final response = await api.files.list( spaces: 'appDataFolder', - q: "mimeType='application/json' and trashed=false", // Add MIME type filter + q: "mimeType='application/json' and trashed=false", $fields: 'files(id, name, createdTime)', orderBy: 'createdTime desc', ); @@ -161,16 +161,14 @@ class GoogleDriveBackupManager extends IBackupManager { @override Future<(String?, Err?)> removeEncryptedBackup({ - required String backupName, - String backupFolder = - defaultBackupPath, // backupFolder is now ignored, always operates in appDataFolder + required String path, }) async { return _withConnection((api) async { try { // Find files in appDataFolder using spaces: 'appDataFolder' and query by name final files = await api.files.list( spaces: 'appDataFolder', - q: "name = '$backupName' and trashed = false", + q: "name = '$path' and trashed = false", $fields: 'files(id)', ); @@ -183,7 +181,7 @@ class GoogleDriveBackupManager extends IBackupManager { File()..trashed = true, // Set trashed to true to move to trash firstFile.id!, ); - return (backupName, null); + return (path, null); } catch (e) { return (null, Err('Failed to remove backup: $e')); } diff --git a/lib/_pkg/backup/local.dart b/lib/_pkg/backup/local.dart index ef6a7abbf..52d347c4a 100644 --- a/lib/_pkg/backup/local.dart +++ b/lib/_pkg/backup/local.dart @@ -17,11 +17,10 @@ class FileSystemBackupManager extends IBackupManager { /// Returns the path to the deleted backup or an error message. @override Future<(String?, Err?)> removeEncryptedBackup({ - required String backupName, - String backupFolder = defaultBackupPath, + required String path, }) async { try { - final result = await fileStorage.deleteFile(backupName); + final result = await fileStorage.deleteFile(path); if (result == null) return (null, Err('Failed to delete file.')); return (result.message, null); } catch (e) { From a2ad58a4036f9eaa0cb142c103c634cef9bf673a Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 24 Feb 2025 00:21:09 -0500 Subject: [PATCH 260/401] refactor: add source and filename metadata to loaded backups --- lib/wallet_settings/bloc/backup_settings_cubit.dart | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/wallet_settings/bloc/backup_settings_cubit.dart b/lib/wallet_settings/bloc/backup_settings_cubit.dart index 198d58e6e..fd2efd7fc 100644 --- a/lib/wallet_settings/bloc/backup_settings_cubit.dart +++ b/lib/wallet_settings/bloc/backup_settings_cubit.dart @@ -208,6 +208,9 @@ class BackupSettingsCubit extends Cubit { encrypted: fileContent, ); if (loadedBackup != null) { + loadedBackup.addAll({ + 'source': 'fs', + }); emit( state.copyWith( loadingBackups: false, @@ -288,6 +291,10 @@ class BackupSettingsCubit extends Cubit { encrypted: utf8.decode(loadedBackupMetaData), ); if (loadedBackup != null) { + loadedBackup.addAll({ + 'source': 'drive', + 'filename': latestBackup.name, + }); emit( state.copyWith( loadingBackups: false, From 4d18450dbbb095dd4e359115d2628ff3b29ec6a5 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 24 Feb 2025 00:21:22 -0500 Subject: [PATCH 261/401] refactor: update delete backup methods to use file picker and path parameter --- .../bloc/backup_settings_cubit.dart | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/lib/wallet_settings/bloc/backup_settings_cubit.dart b/lib/wallet_settings/bloc/backup_settings_cubit.dart index fd2efd7fc..a8fdc4c1c 100644 --- a/lib/wallet_settings/bloc/backup_settings_cubit.dart +++ b/lib/wallet_settings/bloc/backup_settings_cubit.dart @@ -151,14 +151,24 @@ class BackupSettingsCubit extends Cubit { emit(state.copyWith(backupFolderPath: '')); } - Future deleteFsBackup(String backupName) async { - if (state.backupFolderPath.isEmpty) { - emit(state.copyWith(errorSavingBackups: 'No backup to delete')); + Future deleteFsBackup() async { + if (_filePicker == null) { + return; + } + final (file, error) = await _filePicker.pickFile(); + + if (error != null) { + debugPrint('Error picking the file: ${error.message}'); + emit(state.copyWith(errorLoadingBackups: "Error picking file")); + return; + } + if (file == null) { + emit(state.copyWith(errorLoadingBackups: 'Corrupted backup file')); return; } final (deleted, err) = await _fileSystemBackupManager.removeEncryptedBackup( - backupName: backupName, + path: file.path, ); if (err != null) { @@ -169,7 +179,7 @@ class BackupSettingsCubit extends Cubit { emit(state.copyWith(backupFolderPath: '')); } - Future deleteGoogleDriveBackup(String backupName) async { + Future deleteGoogleDriveBackup(String path) async { if (state.backupFolderPath.isEmpty) { emit(state.copyWith(errorSavingBackups: 'No backup to delete')); return; @@ -177,7 +187,7 @@ class BackupSettingsCubit extends Cubit { final (deleted, err) = await _googleDriveBackupManager.removeEncryptedBackup( - backupName: backupName, + path: path, ); if (err != null) { From 882162dc7294694c3d0c3d221077ec9be3d0f596 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 24 Feb 2025 00:22:44 -0500 Subject: [PATCH 262/401] refactor: add loading state in deleteBackupKey method --- lib/wallet_settings/bloc/keychain_cubit.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/wallet_settings/bloc/keychain_cubit.dart b/lib/wallet_settings/bloc/keychain_cubit.dart index 0d78be186..2d047c5e1 100644 --- a/lib/wallet_settings/bloc/keychain_cubit.dart +++ b/lib/wallet_settings/bloc/keychain_cubit.dart @@ -128,6 +128,7 @@ class KeychainCubit extends Cubit { Future deleteBackupKey() async { try { + emit(state.copyWith(loading: true, error: '')); final isServerReady = await serverInfo(); if (!isServerReady) return; From 3a9df84bce23ca11c59b09932e2dd9a4500bfaa5 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 24 Feb 2025 00:24:52 -0500 Subject: [PATCH 263/401] refactor: pass additional pState parameter to KeychainBackupPage --- lib/routes.dart | 6 ++++-- lib/wallet_settings/encrypted_vault_backup.dart | 9 +++++++-- lib/wallet_settings/keychain_page.dart | 7 +++++-- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/lib/routes.dart b/lib/routes.dart index 0013c68be..dd4d95940 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -266,11 +266,13 @@ GoRouter setupRouter() => GoRouter( GoRoute( path: 'keychain', builder: (context, state) { - final (backupKey, backup) = - state.extra! as (String?, Map); + final (backupKey, backup, pState) = + state.extra! as (String?, Map, String); + return KeychainBackupPage( backupKey: backupKey, backup: backup, + pState: pState, ); }, ), diff --git a/lib/wallet_settings/encrypted_vault_backup.dart b/lib/wallet_settings/encrypted_vault_backup.dart index c8949012e..53f9b9e99 100644 --- a/lib/wallet_settings/encrypted_vault_backup.dart +++ b/lib/wallet_settings/encrypted_vault_backup.dart @@ -100,7 +100,8 @@ class _EncryptedVaultBackupPageState extends State { '/wallet-settings/backup-settings/keychain', extra: ( state.backupKey, - {'id': state.backupId, 'salt': state.backupSalt} + {'id': state.backupId, 'salt': state.backupSalt}, + KeyChainPageState.enter.name.toLowerCase() ), ); _cubit.clearError(); @@ -621,7 +622,11 @@ class _RecoveredBackupInfoPageState extends State { onPressed: () => { context.push( '/wallet-settings/backup-settings/keychain', - extra: ('', widget.recoveredBackup), + extra: ( + '', + widget.recoveredBackup, + KeyChainPageState.recovery.name.toLowerCase() + ), ), }, style: FilledButton.styleFrom( diff --git a/lib/wallet_settings/keychain_page.dart b/lib/wallet_settings/keychain_page.dart index 13ae9d1da..c56af373b 100644 --- a/lib/wallet_settings/keychain_page.dart +++ b/lib/wallet_settings/keychain_page.dart @@ -18,14 +18,17 @@ import 'package:gap/gap.dart'; import 'package:go_router/go_router.dart'; class KeychainBackupPage extends StatelessWidget { - const KeychainBackupPage({ + KeychainBackupPage({ super.key, this.backupKey, + required String pState, required this.backup, - }); + }) : _pState = KeyChainPageState.fromString(pState); final String? backupKey; + final KeyChainPageState _pState; final Map backup; + @override Widget build(BuildContext context) { String? backupId; From 630c71cf88b9f67cbbc5fafc9178501838637b94 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 24 Feb 2025 00:25:03 -0500 Subject: [PATCH 264/401] refactor: add fromString method to KeyChainPageState enum for improved state handling --- lib/wallet_settings/bloc/keychain_state.dart | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/wallet_settings/bloc/keychain_state.dart b/lib/wallet_settings/bloc/keychain_state.dart index f48b5ffc6..c8a99c5b3 100644 --- a/lib/wallet_settings/bloc/keychain_state.dart +++ b/lib/wallet_settings/bloc/keychain_state.dart @@ -3,7 +3,19 @@ import 'package:freezed_annotation/freezed_annotation.dart'; part 'keychain_state.freezed.dart'; -enum KeyChainPageState { enter, confirm, recovery, delete } +enum KeyChainPageState { + enter, + confirm, + recovery, + delete; + + static KeyChainPageState fromString(String value) { + return KeyChainPageState.values.firstWhere( + (element) => element.name.toLowerCase() == value.toLowerCase(), + orElse: () => KeyChainPageState.enter, + ); + } +} enum KeyChainInputType { pin, password, backupKey } From c89e80e2d0542800ef8a6dcd57e605c0307b5a68 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 24 Feb 2025 00:26:57 -0500 Subject: [PATCH 265/401] refactor: add _DeletePage widget for backup deletion with PIN/password input --- lib/wallet_settings/keychain_page.dart | 40 ++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/lib/wallet_settings/keychain_page.dart b/lib/wallet_settings/keychain_page.dart index c56af373b..3a42707f1 100644 --- a/lib/wallet_settings/keychain_page.dart +++ b/lib/wallet_settings/keychain_page.dart @@ -337,6 +337,46 @@ class _RecoveryPage extends StatelessWidget { } } +class _DeletePage extends StatelessWidget { + const _DeletePage({super.key, required this.inputType}); + final KeyChainInputType inputType; + + @override + Widget build(BuildContext context) { + return StackedPage( + bottomChildHeight: MediaQuery.of(context).size.height * 0.15, + bottomChild: _DeleteButton(inputType: inputType), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Gap(50), + const BBText.titleLarge( + 'Delete Backup', + textAlign: TextAlign.center, + isBold: true, + ), + const Gap(8), + BBText.bodySmall( + 'Enter your ${inputType == KeyChainInputType.pin ? 'PIN' : 'password'} to delete this backup', + textAlign: TextAlign.center, + ), + const Gap(50), + if (inputType == KeyChainInputType.pin) ...[ + _PinField(), + const KeyPad(), + ] else + _PasswordField(), + const Gap(30), + ], + ), + ), + ); + } +} + +/// Input Widgets class _PinField extends StatelessWidget { @override Widget build(BuildContext context) { From 3f72cda1a79d159c2854312f940be70cf56df039 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 24 Feb 2025 00:27:31 -0500 Subject: [PATCH 266/401] refactor: enhance backup deletion process & improved user input handling --- lib/wallet_settings/keychain_page.dart | 501 ++++++++++++++++--------- 1 file changed, 315 insertions(+), 186 deletions(-) diff --git a/lib/wallet_settings/keychain_page.dart b/lib/wallet_settings/keychain_page.dart index 3a42707f1..0036547fe 100644 --- a/lib/wallet_settings/keychain_page.dart +++ b/lib/wallet_settings/keychain_page.dart @@ -31,52 +31,47 @@ class KeychainBackupPage extends StatelessWidget { @override Widget build(BuildContext context) { - String? backupId; - String? backupSalt; - - if (backupKey != null && backupKey!.isNotEmpty) { - backupId = backup['id']?.toString(); - backupSalt = backup['salt']?.toString(); - } else { - final encryptedData = - jsonDecode(backup["encrypted"] as String) as Map; - backupId = encryptedData["id"]?.toString(); - backupSalt = encryptedData["salt"] as String?; - } + // Extract backup data + final backupData = _extractBackupData(); return MultiBlocProvider( providers: [ BlocProvider( create: (context) => KeychainCubit() ..setChainState( - (backupKey == null || backupKey!.isEmpty) - ? KeyChainPageState.recovery - : KeyChainPageState.enter, - backupId ?? '', + _pState, // Use the provided state directly instead of determining it + backupData.$1 ?? '', backupKey, - backupSalt ?? '', + backupData.$2 ?? '', ), ), - BlocProvider.value( - value: createBackupSettingsCubit(), - ), + BlocProvider.value(value: createBackupSettingsCubit()), ], - child: _Screen( - backupKey: backupKey, - encryptedBackup: backup, - ), + child: _Screen(backupKey: backupKey, backup: backup), ); } + + (String?, String?) _extractBackupData() { + if (backupKey?.isNotEmpty ?? false) { + return (backup['id']?.toString(), backup['salt']?.toString()); + } + + final encryptedData = backup["encrypted"] is String + ? jsonDecode(backup["encrypted"] as String) as Map + : {}; + + return (encryptedData["id"]?.toString(), encryptedData["salt"] as String?); + } } class _Screen extends StatelessWidget { const _Screen({ this.backupKey, - required this.encryptedBackup, + required this.backup, }); final String? backupKey; - final Map encryptedBackup; + final Map backup; @override Widget build(BuildContext context) { return MultiBlocListener( @@ -131,6 +126,34 @@ class _Screen extends StatelessWidget { previous.keySecretState != current.keySecretState || previous.error != current.error, listener: (context, state) { + // Handle delete state + if (state.pageState == KeyChainPageState.delete && + state.keySecretState == KeySecretState.deleted && + !state.loading && + !state.hasError) { + context.read().clearSensitive(); + final source = backup['source'] as String?; + final fileName = backup['filename'] as String?; + if (source != null) { + if (source == 'drive' && fileName != null) { + context + .read() + .deleteGoogleDriveBackup(fileName); + } + if (source == 'fs') { + context.read().deleteFsBackup(); + } + } + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => const _SuccessDialog( + isRecovery: false, + isDelete: true, + ), + ); + } + if (state.isSecretConfirmed && !state.loading && !state.hasError && @@ -152,10 +175,10 @@ class _Screen extends StatelessWidget { if (state.keySecretState == KeySecretState.recovered && !state.loading && !state.hasError && - encryptedBackup.isNotEmpty && + backup.isNotEmpty && state.backupKey.isNotEmpty) { context.read().recoverBackup( - jsonEncode(encryptedBackup), + jsonEncode(backup), state.backupKey, ); } @@ -194,41 +217,41 @@ class _Screen extends StatelessWidget { ), body: AnimatedSwitcher( duration: 300.ms, - child: state.pageState == KeyChainPageState.recovery - ? _RecoveryPage( - key: const ValueKey('recovery'), - inputType: state.inputType, - ) - : state.pageState == KeyChainPageState.enter - ? _EnterPage( - key: const ValueKey('enter'), - inputType: state.inputType, - ) - : _ConfirmPage( - key: const ValueKey('confirm'), - inputType: state.inputType, - ), + child: _buildPageContent(state), ), ); }, ), ); } -} - -class _LoadingView extends StatelessWidget { - const _LoadingView(); - @override - Widget build(BuildContext context) { - return Scaffold( - body: Center( - child: CircularProgressIndicator(color: context.colour.primary), - ), - ); + Widget _buildPageContent(KeychainState state) { + switch (state.pageState) { + case KeyChainPageState.recovery: + return _RecoveryPage( + key: const ValueKey('recovery'), + inputType: state.inputType, + ); + case KeyChainPageState.enter: + return _EnterPage( + key: const ValueKey('enter'), + inputType: state.inputType, + ); + case KeyChainPageState.confirm: + return _ConfirmPage( + key: const ValueKey('confirm'), + inputType: state.inputType, + ); + case KeyChainPageState.delete: + return _DeletePage( + key: const ValueKey('delete'), + inputType: state.inputType, + ); + } } } +/// Page Type Widgets class _EnterPage extends StatelessWidget { const _EnterPage({super.key, required this.inputType}); final KeyChainInputType inputType; @@ -472,70 +495,107 @@ class _PasswordField extends StatelessWidget { } } -class _TitleText extends StatelessWidget { - const _TitleText(); +class KeyPad extends StatelessWidget { + const KeyPad({super.key}); @override Widget build(BuildContext context) { - final (inputState, type) = context - .select((KeychainCubit x) => (x.state.pageState, x.state.inputType)); - final text = inputState == KeyChainPageState.enter - ? 'Choose a backup ${type == KeyChainInputType.pin ? 'PIN' : 'password'}' - : 'Confirm backup ${type == KeyChainInputType.pin ? 'PIN' : 'password'}'; - return BBText.titleLarge( - textAlign: TextAlign.center, - text, - isBold: true, + final shuffledNumbers = + context.select((KeychainCubit x) => x.state.shuffledNumbers); + final shuffledNumberButtonList = [ + for (final i in shuffledNumbers) NumberButton(text: i.toString()), + ]; + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + ), + child: GridView( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + ), + children: [ + for (var i = 0; i < 9; i = i + 1) shuffledNumberButtonList[i], + Container(), + shuffledNumberButtonList[9], + ], + ), ); } } -class _ConfirmTitleText extends StatelessWidget { - const _ConfirmTitleText(); +class NumberButton extends StatefulWidget { + const NumberButton({super.key, required this.text}); - @override - Widget build(BuildContext context) { - final (pageState, inputType) = context - .select((KeychainCubit x) => (x.state.pageState, x.state.inputType)); - final text = - 'Confirm backup ${inputType == KeyChainInputType.pin ? 'PIN' : 'password'}'; + final String text; - return BBText.titleLarge( - textAlign: TextAlign.center, - text, - isBold: true, - ); - } + @override + State createState() => _NumberButtonState(); } -class _ConfirmSubtitleText extends StatelessWidget { - const _ConfirmSubtitleText(); +class _NumberButtonState extends State { + bool isRed = false; @override Widget build(BuildContext context) { - final inputType = context.select((KeychainCubit x) => x.state.inputType); - return BBText.bodySmall( - textAlign: TextAlign.center, - 'Enter the ${inputType == KeyChainInputType.pin ? 'PIN' : 'password'} again to confirm', + OutlinedButton.styleFrom( + shape: const CircleBorder(), + backgroundColor: context.colour.onPrimaryContainer, + foregroundColor: context.colour.primary, ); - } -} -class _SubtitleText extends StatelessWidget { - const _SubtitleText(); + OutlinedButton.styleFrom( + shape: const CircleBorder(), + backgroundColor: context.colour.primary, + foregroundColor: context.colour.primaryContainer, + ); - @override - Widget build(BuildContext context) { - final inputType = context.select((KeychainCubit x) => x.state.inputType); - final text = - 'You must memorize this ${inputType == KeyChainInputType.pin ? 'PIN' : 'password'} to recover access to your wallet. It must be at least 6 digits.'; - return BBText.bodySmall( - textAlign: TextAlign.center, - text, + return Center( + child: SizedBox( + height: 80, + width: 80, + child: GestureDetector( + onTapUp: (e) { + setState(() { + isRed = false; + }); + }, + onTapDown: (e) { + setState(() { + isRed = true; + }); + }, + onTapCancel: () { + setState(() { + isRed = false; + }); + }, + child: OutlinedButton( + style: OutlinedButton.styleFrom( + splashFactory: NoSplash.splashFactory, + ), + onPressed: () { + SystemSound.play(SystemSoundType.click); + HapticFeedback.mediumImpact(); + + context.read().keyPressed(widget.text); + }, + child: BBText.titleLarge( + widget.text, + isBold: true, + ), + ).animate().blur( + begin: const Offset(1, 1), + end: isRed ? const Offset(2, 2) : Offset.zero, + ), + ), + ), ); } } +/// Action Buttons class _SetButton extends StatelessWidget { final KeyChainInputType inputType; const _SetButton({required this.inputType}); @@ -706,7 +766,7 @@ class _RecoverButton extends StatelessWidget { return FilledButton( onPressed: canRecoverKey ? () => context.read().clickRecover() - : null, + : () => context.read().clickRecover(), style: FilledButton.styleFrom( backgroundColor: _getButtonColor(context, canRecoverKey), shape: RoundedRectangleBorder( @@ -779,112 +839,122 @@ class _RecoverButton extends StatelessWidget { } } -class NumberButton extends StatefulWidget { - const NumberButton({super.key, required this.text}); - - final String text; - - @override - State createState() => _NumberButtonState(); -} - -class _NumberButtonState extends State { - bool isRed = false; +class _DeleteButton extends StatelessWidget { + const _DeleteButton({required this.inputType}); + final KeyChainInputType inputType; @override Widget build(BuildContext context) { - OutlinedButton.styleFrom( - shape: const CircleBorder(), - backgroundColor: context.colour.onPrimaryContainer, - foregroundColor: context.colour.primary, - ); - - OutlinedButton.styleFrom( - shape: const CircleBorder(), - backgroundColor: context.colour.primary, - foregroundColor: context.colour.primaryContainer, - ); + final state = context.select((KeychainCubit x) => x.state); + final showButton = state.showButton; - return Center( - child: SizedBox( - height: 80, - width: 80, - child: GestureDetector( - onTapUp: (e) { - setState(() { - isRed = false; - }); - }, - onTapDown: (e) { - setState(() { - isRed = true; - }); - }, - onTapCancel: () { - setState(() { - isRed = false; - }); - }, - child: OutlinedButton( - style: OutlinedButton.styleFrom( - splashFactory: NoSplash.splashFactory, + return FilledButton( + onPressed: showButton ? () => _showDeleteConfirmation(context) : null, + style: FilledButton.styleFrom( + backgroundColor: context.colour.shadow, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Delete Backup', + style: context.font.bodyMedium!.copyWith( + color: Colors.white, + fontWeight: FontWeight.w900, ), - onPressed: () { - SystemSound.play(SystemSoundType.click); - HapticFeedback.mediumImpact(); + ), + const SizedBox(width: 8), + const Icon( + Icons.delete_forever, + color: Colors.white, + size: 20, + ), + ], + ), + ); + } - context.read().keyPressed(widget.text); + void _showDeleteConfirmation(BuildContext context) { + showDialog( + context: context, + builder: (dialogContext) => AlertDialog( + title: const BBText.title( + 'Delete Backup?', + isBold: true, + ), + content: const BBText.bodySmall( + 'This action cannot be undone. Are you sure you want to delete this backup?', + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(dialogContext).pop(), + child: const Text('Cancel'), + ), + FilledButton( + onPressed: () async { + // First close the dialog + Navigator.of(dialogContext).pop(); + // Then trigger the delete action using the original context + context.read().deleteBackupKey(); }, - child: BBText.titleLarge( - widget.text, - isBold: true, + style: FilledButton.styleFrom( + backgroundColor: context.colour.error, ), - ).animate().blur( - begin: const Offset(1, 1), - end: isRed ? const Offset(2, 2) : Offset.zero, - ), - ), + child: const Text('Delete'), + ), + ], ), ); } } -class KeyPad extends StatelessWidget { - const KeyPad({super.key}); +/// Dialog Widgets +class _LoadingView extends StatelessWidget { + const _LoadingView(); @override Widget build(BuildContext context) { - final shuffledNumbers = - context.select((KeychainCubit x) => x.state.shuffledNumbers); - final shuffledNumberButtonList = [ - for (final i in shuffledNumbers) NumberButton(text: i.toString()), - ]; - return Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16, - ), - child: GridView( - physics: const NeverScrollableScrollPhysics(), - shrinkWrap: true, - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 3, - ), - children: [ - for (var i = 0; i < 9; i = i + 1) shuffledNumberButtonList[i], - Container(), - shuffledNumberButtonList[9], - ], + return Scaffold( + body: Center( + child: CircularProgressIndicator(color: context.colour.primary), ), ); } } class _SuccessDialog extends StatelessWidget { - const _SuccessDialog({required this.isRecovery}); + const _SuccessDialog({ + required this.isRecovery, + this.isDelete = false, + }); + final bool isRecovery; + final bool isDelete; @override Widget build(BuildContext context) { + String title; + String message; + String route; + + if (isDelete) { + title = 'Backup Deleted'; + message = 'Your backup has been permanently deleted'; + route = '/wallet-settings'; + } else if (isRecovery) { + title = 'Recovery Successful'; + message = 'Your wallet has been recovered successfully'; + route = '/home'; + } else { + title = 'Backup Successful'; + message = + 'Your wallet has been backed up successfully \n Please test your backup'; + route = '/wallet-settings/backup-settings/recover-options/encrypted'; + } + return Dialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), child: Padding( @@ -899,26 +969,20 @@ class _SuccessDialog extends StatelessWidget { ), const Gap(16), BBText.title( - isRecovery ? 'Recovery Successful' : 'Backup Successful', + title, textAlign: TextAlign.center, isBold: true, ), const Gap(8), BBText.bodySmall( - isRecovery - ? 'Your wallet has been recovered successfully' - : 'Your wallet has been backed up successfully \n Please test your backup', + message, textAlign: TextAlign.center, ), const Gap(24), FilledButton( onPressed: () { Navigator.of(context).pop(); - context.go( - isRecovery - ? '/home' - : '/wallet-settings/backup-settings/recover-options/encrypted', - ); + context.go(route); }, style: FilledButton.styleFrom( backgroundColor: context.colour.shadow, @@ -987,3 +1051,68 @@ class _ErrorDialog extends StatelessWidget { ); } } + +/// Text Widgets +class _TitleText extends StatelessWidget { + const _TitleText(); + + @override + Widget build(BuildContext context) { + final (inputState, type) = context + .select((KeychainCubit x) => (x.state.pageState, x.state.inputType)); + final text = inputState == KeyChainPageState.enter + ? 'Choose a backup ${type == KeyChainInputType.pin ? 'PIN' : 'password'}' + : 'Confirm backup ${type == KeyChainInputType.pin ? 'PIN' : 'password'}'; + return BBText.titleLarge( + textAlign: TextAlign.center, + text, + isBold: true, + ); + } +} + +class _ConfirmTitleText extends StatelessWidget { + const _ConfirmTitleText(); + + @override + Widget build(BuildContext context) { + final (pageState, inputType) = context + .select((KeychainCubit x) => (x.state.pageState, x.state.inputType)); + final text = + 'Confirm backup ${inputType == KeyChainInputType.pin ? 'PIN' : 'password'}'; + + return BBText.titleLarge( + textAlign: TextAlign.center, + text, + isBold: true, + ); + } +} + +class _ConfirmSubtitleText extends StatelessWidget { + const _ConfirmSubtitleText(); + + @override + Widget build(BuildContext context) { + final inputType = context.select((KeychainCubit x) => x.state.inputType); + return BBText.bodySmall( + textAlign: TextAlign.center, + 'Enter the ${inputType == KeyChainInputType.pin ? 'PIN' : 'password'} again to confirm', + ); + } +} + +class _SubtitleText extends StatelessWidget { + const _SubtitleText(); + + @override + Widget build(BuildContext context) { + final inputType = context.select((KeychainCubit x) => x.state.inputType); + final text = + 'You must memorize this ${inputType == KeyChainInputType.pin ? 'PIN' : 'password'} to recover access to your wallet. It must be at least 6 digits.'; + return BBText.bodySmall( + textAlign: TextAlign.center, + text, + ); + } +} From 269c216ee9dee2a0374051228921c14073fe3cb0 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 24 Feb 2025 00:28:31 -0500 Subject: [PATCH 267/401] refactor: code cleanup --- .../encrypted_vault_backup.dart | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/lib/wallet_settings/encrypted_vault_backup.dart b/lib/wallet_settings/encrypted_vault_backup.dart index 53f9b9e99..4bbfd5568 100644 --- a/lib/wallet_settings/encrypted_vault_backup.dart +++ b/lib/wallet_settings/encrypted_vault_backup.dart @@ -7,6 +7,7 @@ import 'package:bb_mobile/_ui/toast.dart'; import 'package:bb_mobile/styles.dart'; import 'package:bb_mobile/wallet_settings/bloc/backup_settings_cubit.dart'; import 'package:bb_mobile/wallet_settings/bloc/backup_settings_state.dart'; +import 'package:bb_mobile/wallet_settings/bloc/keychain_state.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; @@ -299,11 +300,11 @@ class _EncryptedVaultRecoverPageState extends State { ) async { switch (provider) { case BackupProvider.googleDrive: - await _cubit.fetchLatestBacup(); + await _cubit.fetchGoogleDriveBackup(); case BackupProvider.iCloud: debugPrint('iCloud backup'); case BackupProvider.custom: - _cubit.recoverFromFs(); + _cubit.fetchFsBackup(); } } @@ -668,6 +669,23 @@ class _RecoveredBackupInfoPageState extends State { textAlign: TextAlign.center, ), ), + const Gap(10), + IconButton( + onPressed: () { + context.push( + '/wallet-settings/backup-settings/keychain', + extra: ( + '', + widget.recoveredBackup, + KeyChainPageState.delete.name.toLowerCase() + ), + ); + }, + icon: const Icon( + Icons.delete, + color: Colors.black, + ), + ), ], ), ), From 724d16788286bd9d4edaf8a19a3f7936a3604861 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 24 Feb 2025 00:33:35 -0500 Subject: [PATCH 268/401] fix: invalid route path --- lib/wallet_settings/keychain_page.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/wallet_settings/keychain_page.dart b/lib/wallet_settings/keychain_page.dart index 0036547fe..785e991fe 100644 --- a/lib/wallet_settings/keychain_page.dart +++ b/lib/wallet_settings/keychain_page.dart @@ -943,7 +943,7 @@ class _SuccessDialog extends StatelessWidget { if (isDelete) { title = 'Backup Deleted'; message = 'Your backup has been permanently deleted'; - route = '/wallet-settings'; + route = '/home'; } else if (isRecovery) { title = 'Recovery Successful'; message = 'Your wallet has been recovered successfully'; From c3f8d5045524d8107eeea13edf33bf59ce2f93d9 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 24 Feb 2025 04:24:05 -0500 Subject: [PATCH 269/401] chore: update dependencies and versions in pubspec.lock and pubspec.yaml --- pubspec.lock | 104 +++++++++++++++++++++++++++------------------------ pubspec.yaml | 2 + 2 files changed, 58 insertions(+), 48 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 6c9d62ebb..4944d10c1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -58,10 +58,10 @@ packages: dependency: transitive description: name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 url: "https://pub.dev" source: hosted - version: "2.11.0" + version: "2.12.0" auto_size_text: dependency: "direct main" description: @@ -110,6 +110,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.7" + bip85: + dependency: "direct main" + description: + name: bip85 + sha256: "1e556b32a6e2062a8e6f728bfdf1898058ecde9c9068f5272d4792af2477b10c" + url: "https://pub.dev" + source: hosted + version: "1.0.3" bitcoin_utils: dependency: "direct main" description: @@ -155,10 +163,10 @@ packages: dependency: transitive description: name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" bs58check: dependency: transitive description: @@ -251,10 +259,10 @@ packages: dependency: transitive description: name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" checked_yaml: dependency: transitive description: @@ -275,10 +283,10 @@ packages: dependency: transitive description: name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" code_builder: dependency: transitive description: @@ -291,10 +299,10 @@ packages: dependency: transitive description: name: collection - sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" url: "https://pub.dev" source: hosted - version: "1.19.0" + version: "1.19.1" convert: dependency: "direct main" description: @@ -419,10 +427,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.2" ffi: dependency: transitive description: @@ -435,10 +443,10 @@ packages: dependency: transitive description: name: file - sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "7.0.1" file_picker: dependency: "direct main" description: @@ -945,18 +953,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" + sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec url: "https://pub.dev" source: hosted - version: "10.0.7" + version: "10.0.8" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 url: "https://pub.dev" source: hosted - version: "3.0.8" + version: "3.0.9" leak_tracker_testing: dependency: transitive description: @@ -1025,10 +1033,10 @@ packages: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.17" material_color_utilities: dependency: transitive description: @@ -1041,10 +1049,10 @@ packages: dependency: transitive description: name: meta - sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.16.0" mime: dependency: transitive description: @@ -1130,10 +1138,10 @@ packages: dependency: transitive description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.9.1" path_parsing: dependency: transitive description: @@ -1267,10 +1275,10 @@ packages: dependency: transitive description: name: platform - sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" url: "https://pub.dev" source: hosted - version: "3.1.5" + version: "3.1.6" plugin_platform_interface: dependency: transitive description: @@ -1307,10 +1315,10 @@ packages: dependency: transitive description: name: process - sha256: "21e54fd2faf1b5bdd5102afd25012184a6793927648ea81eea80552ac9405b32" + sha256: "107d8be718f120bbba9dcd1e95e3bd325b1b4a4f07db64154635ba03f2567a0d" url: "https://pub.dev" source: hosted - version: "5.0.2" + version: "5.0.3" provider: dependency: transitive description: @@ -1355,7 +1363,7 @@ packages: dependency: "direct main" description: path: "." - ref: HEAD + ref: "6941511cce648d3478792f696874e057cac96ee7" resolved-ref: "6941511cce648d3478792f696874e057cac96ee7" url: "https://github.com/SatoshiPortal/recoverbull-client-dart.git" source: git @@ -1457,10 +1465,10 @@ packages: dependency: transitive description: name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.10.1" sprintf: dependency: transitive description: @@ -1473,18 +1481,18 @@ packages: dependency: transitive description: name: stack_trace - sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.12.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" stream_transform: dependency: transitive description: @@ -1497,10 +1505,10 @@ packages: dependency: transitive description: name: string_scanner - sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.1" sync_http: dependency: transitive description: @@ -1521,34 +1529,34 @@ packages: dependency: transitive description: name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" test: dependency: transitive description: name: test - sha256: "713a8789d62f3233c46b4a90b174737b2c04cb6ae4500f2aa8b1be8f03f5e67f" + sha256: "301b213cd241ca982e9ba50266bd3f5bd1ea33f1455554c5abb85d1be0e2d87e" url: "https://pub.dev" source: hosted - version: "1.25.8" + version: "1.25.15" test_api: dependency: transitive description: name: test_api - sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd url: "https://pub.dev" source: hosted - version: "0.7.3" + version: "0.7.4" test_core: dependency: transitive description: name: test_core - sha256: "12391302411737c176b0b5d6491f466b0dd56d4763e347b6714efbaa74d7953d" + sha256: "84d17c3486c8dfdbe5e12a50c8ae176d15e2a771b96909a9442b40173649ccaa" url: "https://pub.dev" source: hosted - version: "0.6.5" + version: "0.6.8" timeago: dependency: "direct main" description: @@ -1697,10 +1705,10 @@ packages: dependency: transitive description: name: vm_service - sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b + sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" url: "https://pub.dev" source: hosted - version: "14.3.0" + version: "14.3.1" watcher: dependency: transitive description: @@ -1782,5 +1790,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.6.0 <4.0.0" + dart: ">=3.7.0-0 <4.0.0" flutter: ">=3.27.0" diff --git a/pubspec.yaml b/pubspec.yaml index 08679353d..f4faaf847 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -100,6 +100,8 @@ dependencies: recoverbull: git: url: https://github.com/SatoshiPortal/recoverbull-client-dart.git + ref: 6941511cce648d3478792f696874e057cac96ee7 + bip85: ^1.0.3 dev_dependencies: From 95560280316b334917340b88a41e09e1da11affe Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 24 Feb 2025 04:28:08 -0500 Subject: [PATCH 270/401] chore: update recoverbull dependency reference in pubspec files --- pubspec.lock | 4 ++-- pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 4944d10c1..a7cb757e4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1363,8 +1363,8 @@ packages: dependency: "direct main" description: path: "." - ref: "6941511cce648d3478792f696874e057cac96ee7" - resolved-ref: "6941511cce648d3478792f696874e057cac96ee7" + ref: "1502b11eeef3a139d28a64d9f138d1ca51554d73" + resolved-ref: "1502b11eeef3a139d28a64d9f138d1ca51554d73" url: "https://github.com/SatoshiPortal/recoverbull-client-dart.git" source: git version: "1.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index f4faaf847..4be8d6d02 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -100,7 +100,7 @@ dependencies: recoverbull: git: url: https://github.com/SatoshiPortal/recoverbull-client-dart.git - ref: 6941511cce648d3478792f696874e057cac96ee7 + ref: 1502b11eeef3a139d28a64d9f138d1ca51554d73 bip85: ^1.0.3 From ca4c522f372cff8a0e8676053099a74ef38eaf19 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 24 Feb 2025 04:28:38 -0500 Subject: [PATCH 271/401] chore: update google_sign_in_ios dependency and enable GPU validation mode in Xcode scheme --- ios/Podfile.lock | 2 +- ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 7c5f22682..ea32cee0e 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -194,7 +194,7 @@ SPEC CHECKSUMS: flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069 flutter_native_splash: f71420956eb811e6d310720fee915f1d42852e7a flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12 - google_sign_in_ios: 4111e87aa5e24a4404f00ea13479f35e571969cc + google_sign_in_ios: 07375bfbf2620bc93a602c0e27160d6afc6ead38 GoogleSignIn: d4281ab6cf21542b1cfaff85c191f230b399d2db GTMAppAuth: f69bd07d68cd3b766125f7e072c45d7340dea0de GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6 diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 0533c716f..c469fe173 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -77,6 +77,7 @@ ignoresPersistentStateOnLaunch = "NO" debugDocumentVersioning = "YES" debugServiceExtension = "internal" + enableGPUValidationMode = "1" allowLocationSimulation = "YES"> From f4e77e92004fea3a930861e7b91527ebe07d6f42 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 24 Feb 2025 04:52:33 -0500 Subject: [PATCH 272/401] chore: update recoverbull dependency reference to use 'main' branch --- pubspec.lock | 4 ++-- pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index a7cb757e4..948842359 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1114,7 +1114,7 @@ packages: description: path: "." ref: develop - resolved-ref: "6197278729b2806f116e25e290e92019cf408a23" + resolved-ref: fc6c27280ad6e5513f24459009b465a0ef5c4b16 url: "https://github.com/ethicnology/dart-nostr" source: git version: "1.5.0" @@ -1363,7 +1363,7 @@ packages: dependency: "direct main" description: path: "." - ref: "1502b11eeef3a139d28a64d9f138d1ca51554d73" + ref: main resolved-ref: "1502b11eeef3a139d28a64d9f138d1ca51554d73" url: "https://github.com/SatoshiPortal/recoverbull-client-dart.git" source: git diff --git a/pubspec.yaml b/pubspec.yaml index 4be8d6d02..fb56efc8f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -100,7 +100,7 @@ dependencies: recoverbull: git: url: https://github.com/SatoshiPortal/recoverbull-client-dart.git - ref: 1502b11eeef3a139d28a64d9f138d1ca51554d73 + ref: main bip85: ^1.0.3 From d3a6f559d5b5429f72e138abbf1896761ada48bc Mon Sep 17 00:00:00 2001 From: ethicnology Date: Mon, 24 Feb 2025 08:57:01 -0500 Subject: [PATCH 273/401] refactor: use a Set for constant time operation and to save space --- lib/_pkg/consts/passwords.dart | 5 +++-- lib/wallet_settings/bloc/keychain_state.dart | 12 ++---------- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/lib/_pkg/consts/passwords.dart b/lib/_pkg/consts/passwords.dart index 1466ce159..d1ac56747 100644 --- a/lib/_pkg/consts/passwords.dart +++ b/lib/_pkg/consts/passwords.dart @@ -1,4 +1,5 @@ -const List passwordBlacklist = [ +// https://github.com/danielmiessler/SecLists/blob/master/Passwords/Common-Credentials/10-million-password-list-top-1000.txt +const Set commonPasswordsTop1000 = { "123456", "password", "12345678", @@ -999,4 +1000,4 @@ const List passwordBlacklist = [ "wildcat", "polina", "freepass", -]; +}; diff --git a/lib/wallet_settings/bloc/keychain_state.dart b/lib/wallet_settings/bloc/keychain_state.dart index c8a99c5b3..eadcc63c1 100644 --- a/lib/wallet_settings/bloc/keychain_state.dart +++ b/lib/wallet_settings/bloc/keychain_state.dart @@ -69,7 +69,7 @@ class KeychainState with _$KeychainState { return 'Password must contain at least 2 numbers'; } return validateSecret(secret) - ? 'Password contains a common word or pattern' + ? 'The password is among the top 1000 most common' : null; } @@ -79,13 +79,5 @@ class KeychainState with _$KeychainState { bool get isRecovering => pageState == KeyChainPageState.recovery; bool get canRecoverKey => backupId.isNotEmpty && isValid && !loading; - // Cache the compiled regex patterns - static final _blacklistPattern = RegExp( - r'\b(' + - passwordBlacklist.map((word) => RegExp.escape(word)).join('|') + - r')\b', - caseSensitive: false, - ); - - bool validateSecret(String secret) => _blacklistPattern.hasMatch(secret); + bool validateSecret(String secret) => commonPasswordsTop1000.contains(secret); } From 3981d035ca5dfe7de951538f5027d9d5c3f7280f Mon Sep 17 00:00:00 2001 From: ethicnology Date: Mon, 24 Feb 2025 09:08:36 -0500 Subject: [PATCH 274/401] refactor: password should be at least as strong as the PIN --- lib/wallet_settings/bloc/keychain_state.dart | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/lib/wallet_settings/bloc/keychain_state.dart b/lib/wallet_settings/bloc/keychain_state.dart index eadcc63c1..e1f6e484b 100644 --- a/lib/wallet_settings/bloc/keychain_state.dart +++ b/lib/wallet_settings/bloc/keychain_state.dart @@ -43,31 +43,17 @@ class KeychainState with _$KeychainState { String displayPin() => 'x' * secret.length; - static final _pinRegex = RegExp(r'^[0-9]{6,7}$'); - static final _uppercaseRegex = RegExp(r'(?=(?:.*[A-Z]){2})'); - static final _numbersRegex = RegExp(r'(?=(?:.*\d){2})'); - String? getValidationError() { if (secret.isEmpty) return null; if (inputType == KeyChainInputType.pin) { - if (!_pinRegex.hasMatch(secret)) { + if (!RegExp(r'^[0-9]{6,7}$').hasMatch(secret)) { return secret.length < 6 ? 'PIN must be at least 6 digits long' : 'PIN must be less than 8 digits'; } - return validateSecret(secret) ? 'PIN contains a common pattern' : null; } - if (secret.length < 7) { - return 'Password must be at greater than 6 characters long'; - } - if (!_uppercaseRegex.hasMatch(secret)) { - return 'Password must contain at least 2 uppercase letters'; - } - if (!_numbersRegex.hasMatch(secret)) { - return 'Password must contain at least 2 numbers'; - } return validateSecret(secret) ? 'The password is among the top 1000 most common' : null; From 317c64f2dfdfa91eac6e3faab6666f0b2a702b6c Mon Sep 17 00:00:00 2001 From: ethicnology Date: Mon, 24 Feb 2025 09:20:29 -0500 Subject: [PATCH 275/401] refactor: move recoverbull related file into it's own feature folder --- lib/address/pop_up.dart | 2 +- lib/{wallet_settings => recoverbull}/backup_settings.dart | 2 +- .../bloc/backup_settings_cubit.dart | 2 +- .../bloc/backup_settings_state.dart | 0 .../bloc/keychain_cubit.dart | 2 +- .../bloc/keychain_state.dart | 0 .../encrypted_vault_backup.dart | 6 +++--- lib/{wallet_settings => recoverbull}/keychain_page.dart | 8 ++++---- lib/routes.dart | 6 +++--- lib/wallet_settings/listeners.dart | 4 ++-- lib/wallet_settings/physical_backup.dart | 2 +- lib/wallet_settings/test_backup.dart | 2 +- 12 files changed, 18 insertions(+), 18 deletions(-) rename lib/{wallet_settings => recoverbull}/backup_settings.dart (99%) rename lib/{wallet_settings => recoverbull}/bloc/backup_settings_cubit.dart (99%) rename lib/{wallet_settings => recoverbull}/bloc/backup_settings_state.dart (100%) rename lib/{wallet_settings => recoverbull}/bloc/keychain_cubit.dart (98%) rename lib/{wallet_settings => recoverbull}/bloc/keychain_state.dart (100%) rename lib/{wallet_settings => recoverbull}/encrypted_vault_backup.dart (99%) rename lib/{wallet_settings => recoverbull}/keychain_page.dart (99%) diff --git a/lib/address/pop_up.dart b/lib/address/pop_up.dart index 44c0697e1..992104e70 100644 --- a/lib/address/pop_up.dart +++ b/lib/address/pop_up.dart @@ -14,10 +14,10 @@ import 'package:bb_mobile/address/bloc/address_state.dart'; import 'package:bb_mobile/currency/bloc/currency_cubit.dart'; import 'package:bb_mobile/locator.dart'; import 'package:bb_mobile/network/bloc/network_bloc.dart'; +import 'package:bb_mobile/wallet_settings/bloc/wallet_settings_cubit.dart'; import 'package:bb_mobile/settings/bloc/settings_cubit.dart'; import 'package:bb_mobile/styles.dart'; import 'package:bb_mobile/wallet/bloc/wallet_bloc.dart'; -import 'package:bb_mobile/wallet_settings/bloc/wallet_settings_cubit.dart'; import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; diff --git a/lib/wallet_settings/backup_settings.dart b/lib/recoverbull/backup_settings.dart similarity index 99% rename from lib/wallet_settings/backup_settings.dart rename to lib/recoverbull/backup_settings.dart index 5e841b99c..5aa578fea 100644 --- a/lib/wallet_settings/backup_settings.dart +++ b/lib/recoverbull/backup_settings.dart @@ -3,7 +3,7 @@ import 'package:bb_mobile/_ui/components/button.dart'; import 'package:bb_mobile/_ui/components/text.dart'; import 'package:bb_mobile/styles.dart'; import 'package:bb_mobile/wallet/bloc/wallet_bloc.dart'; -import 'package:bb_mobile/wallet_settings/bloc/backup_settings_cubit.dart'; +import 'package:bb_mobile/recoverbull/bloc/backup_settings_cubit.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:gap/gap.dart'; diff --git a/lib/wallet_settings/bloc/backup_settings_cubit.dart b/lib/recoverbull/bloc/backup_settings_cubit.dart similarity index 99% rename from lib/wallet_settings/bloc/backup_settings_cubit.dart rename to lib/recoverbull/bloc/backup_settings_cubit.dart index a8fdc4c1c..7a4bd9bd2 100644 --- a/lib/wallet_settings/bloc/backup_settings_cubit.dart +++ b/lib/recoverbull/bloc/backup_settings_cubit.dart @@ -19,7 +19,7 @@ import 'package:bb_mobile/_repository/wallet_service.dart'; import 'package:bb_mobile/home/bloc/home_bloc.dart'; import 'package:bb_mobile/home/bloc/home_event.dart'; import 'package:bb_mobile/locator.dart'; -import 'package:bb_mobile/wallet_settings/bloc/backup_settings_state.dart'; +import 'package:bb_mobile/recoverbull/bloc/backup_settings_state.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:hex/hex.dart'; diff --git a/lib/wallet_settings/bloc/backup_settings_state.dart b/lib/recoverbull/bloc/backup_settings_state.dart similarity index 100% rename from lib/wallet_settings/bloc/backup_settings_state.dart rename to lib/recoverbull/bloc/backup_settings_state.dart diff --git a/lib/wallet_settings/bloc/keychain_cubit.dart b/lib/recoverbull/bloc/keychain_cubit.dart similarity index 98% rename from lib/wallet_settings/bloc/keychain_cubit.dart rename to lib/recoverbull/bloc/keychain_cubit.dart index 2d047c5e1..971d207fb 100644 --- a/lib/wallet_settings/bloc/keychain_cubit.dart +++ b/lib/recoverbull/bloc/keychain_cubit.dart @@ -1,5 +1,5 @@ import 'package:bb_mobile/_pkg/consts/configs.dart'; -import 'package:bb_mobile/wallet_settings/bloc/keychain_state.dart'; +import 'package:bb_mobile/recoverbull/bloc/keychain_state.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:hex/hex.dart'; diff --git a/lib/wallet_settings/bloc/keychain_state.dart b/lib/recoverbull/bloc/keychain_state.dart similarity index 100% rename from lib/wallet_settings/bloc/keychain_state.dart rename to lib/recoverbull/bloc/keychain_state.dart diff --git a/lib/wallet_settings/encrypted_vault_backup.dart b/lib/recoverbull/encrypted_vault_backup.dart similarity index 99% rename from lib/wallet_settings/encrypted_vault_backup.dart rename to lib/recoverbull/encrypted_vault_backup.dart index 4bbfd5568..e41baaa79 100644 --- a/lib/wallet_settings/encrypted_vault_backup.dart +++ b/lib/recoverbull/encrypted_vault_backup.dart @@ -5,9 +5,9 @@ import 'package:bb_mobile/_ui/app_bar.dart'; import 'package:bb_mobile/_ui/components/text.dart'; import 'package:bb_mobile/_ui/toast.dart'; import 'package:bb_mobile/styles.dart'; -import 'package:bb_mobile/wallet_settings/bloc/backup_settings_cubit.dart'; -import 'package:bb_mobile/wallet_settings/bloc/backup_settings_state.dart'; -import 'package:bb_mobile/wallet_settings/bloc/keychain_state.dart'; +import 'package:bb_mobile/recoverbull/bloc/backup_settings_cubit.dart'; +import 'package:bb_mobile/recoverbull/bloc/backup_settings_state.dart'; +import 'package:bb_mobile/recoverbull/bloc/keychain_state.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; diff --git a/lib/wallet_settings/keychain_page.dart b/lib/recoverbull/keychain_page.dart similarity index 99% rename from lib/wallet_settings/keychain_page.dart rename to lib/recoverbull/keychain_page.dart index 785e991fe..9c855e21c 100644 --- a/lib/wallet_settings/keychain_page.dart +++ b/lib/recoverbull/keychain_page.dart @@ -4,11 +4,11 @@ import 'package:bb_mobile/_ui/app_bar.dart'; import 'package:bb_mobile/_ui/components/text.dart'; import 'package:bb_mobile/_ui/components/text_input.dart'; import 'package:bb_mobile/_ui/page_template.dart'; +import 'package:bb_mobile/recoverbull/bloc/backup_settings_cubit.dart'; +import 'package:bb_mobile/recoverbull/bloc/backup_settings_state.dart'; +import 'package:bb_mobile/recoverbull/bloc/keychain_cubit.dart'; +import 'package:bb_mobile/recoverbull/bloc/keychain_state.dart'; import 'package:bb_mobile/styles.dart'; -import 'package:bb_mobile/wallet_settings/bloc/backup_settings_cubit.dart'; -import 'package:bb_mobile/wallet_settings/bloc/backup_settings_state.dart'; -import 'package:bb_mobile/wallet_settings/bloc/keychain_cubit.dart'; -import 'package:bb_mobile/wallet_settings/bloc/keychain_state.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_animate/flutter_animate.dart'; diff --git a/lib/routes.dart b/lib/routes.dart index dd4d95940..fa548fdd9 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -12,6 +12,9 @@ import 'package:bb_mobile/import/hardware_page.dart'; import 'package:bb_mobile/import/page.dart'; import 'package:bb_mobile/locator.dart'; import 'package:bb_mobile/receive/receive_page.dart'; +import 'package:bb_mobile/recoverbull/backup_settings.dart'; +import 'package:bb_mobile/recoverbull/encrypted_vault_backup.dart'; +import 'package:bb_mobile/recoverbull/keychain_page.dart'; import 'package:bb_mobile/send/bloc/send_cubit.dart'; import 'package:bb_mobile/send/send_page.dart'; import 'package:bb_mobile/settings/application_settings_page.dart'; @@ -32,9 +35,6 @@ import 'package:bb_mobile/wallet/details.dart'; import 'package:bb_mobile/wallet/information_page.dart'; import 'package:bb_mobile/wallet/wallet_page.dart'; import 'package:bb_mobile/wallet_settings/accounting.dart'; -import 'package:bb_mobile/wallet_settings/backup_settings.dart'; -import 'package:bb_mobile/wallet_settings/encrypted_vault_backup.dart'; -import 'package:bb_mobile/wallet_settings/keychain_page.dart'; import 'package:bb_mobile/wallet_settings/physical_backup.dart'; import 'package:bb_mobile/wallet_settings/test_backup.dart'; import 'package:bb_mobile/wallet_settings/wallet_settings_page.dart'; diff --git a/lib/wallet_settings/listeners.dart b/lib/wallet_settings/listeners.dart index 11c8914d7..9d392f1fc 100644 --- a/lib/wallet_settings/listeners.dart +++ b/lib/wallet_settings/listeners.dart @@ -2,9 +2,9 @@ import 'package:bb_mobile/_repository/app_wallets_repository.dart'; import 'package:bb_mobile/_repository/wallet_service.dart'; import 'package:bb_mobile/home/bloc/home_bloc.dart'; import 'package:bb_mobile/home/bloc/home_event.dart'; +import 'package:bb_mobile/recoverbull/bloc/backup_settings_cubit.dart'; +import 'package:bb_mobile/recoverbull/bloc/backup_settings_state.dart'; import 'package:bb_mobile/wallet/bloc/wallet_bloc.dart'; -import 'package:bb_mobile/wallet_settings/bloc/backup_settings_cubit.dart'; -import 'package:bb_mobile/wallet_settings/bloc/backup_settings_state.dart'; import 'package:bb_mobile/wallet_settings/bloc/wallet_settings_cubit.dart'; import 'package:bb_mobile/wallet_settings/bloc/wallet_settings_state.dart'; import 'package:flutter/material.dart'; diff --git a/lib/wallet_settings/physical_backup.dart b/lib/wallet_settings/physical_backup.dart index 4e4681c0d..2a34c3f0f 100644 --- a/lib/wallet_settings/physical_backup.dart +++ b/lib/wallet_settings/physical_backup.dart @@ -3,8 +3,8 @@ import 'package:bb_mobile/_ui/app_bar.dart'; import 'package:bb_mobile/_ui/components/button.dart'; import 'package:bb_mobile/_ui/components/text.dart'; import 'package:bb_mobile/_ui/word_grid.dart'; +import 'package:bb_mobile/recoverbull/bloc/backup_settings_cubit.dart'; import 'package:bb_mobile/wallet/bloc/wallet_bloc.dart'; -import 'package:bb_mobile/wallet_settings/bloc/backup_settings_cubit.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:gap/gap.dart'; diff --git a/lib/wallet_settings/test_backup.dart b/lib/wallet_settings/test_backup.dart index 9598fdd57..72e73e4ce 100644 --- a/lib/wallet_settings/test_backup.dart +++ b/lib/wallet_settings/test_backup.dart @@ -2,9 +2,9 @@ import 'package:bb_mobile/_ui/app_bar.dart'; import 'package:bb_mobile/_ui/components/button.dart'; import 'package:bb_mobile/_ui/components/text.dart'; import 'package:bb_mobile/_ui/components/text_input.dart'; +import 'package:bb_mobile/recoverbull/bloc/backup_settings_cubit.dart'; import 'package:bb_mobile/styles.dart'; import 'package:bb_mobile/wallet/bloc/wallet_bloc.dart'; -import 'package:bb_mobile/wallet_settings/bloc/backup_settings_cubit.dart'; import 'package:bb_mobile/wallet_settings/listeners.dart'; import 'package:extra_alignments/extra_alignments.dart'; import 'package:flutter/material.dart'; From acd8a48cdc9620d3b07f7a41b5fb3ccf16ad589a Mon Sep 17 00:00:00 2001 From: ethicnology Date: Mon, 24 Feb 2025 09:24:13 -0500 Subject: [PATCH 276/401] refactor: better handling of signInSilently (commit by Stax) --- lib/_pkg/backup/google_drive.dart | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/lib/_pkg/backup/google_drive.dart b/lib/_pkg/backup/google_drive.dart index 1e6dc08f7..99bcb860f 100644 --- a/lib/_pkg/backup/google_drive.dart +++ b/lib/_pkg/backup/google_drive.dart @@ -23,18 +23,21 @@ class GoogleDriveBackupManager extends IBackupManager { DriveApi? _api; Future<(DriveApi?, Err?)> connect() async { + GoogleSignInAccount? account; try { - // Try silent sign in first - var account = await _google.signInSilently(); - // If failed, try interactive sign in - if (account == null) { - account = await _google.signIn(); - if (account == null) return (null, Err(_errorMessages['connection']!)); - } + account = await _google.signInSilently(); + } catch (e) { + debugPrint('Silent sign-in failed, trying interactive sign-in: $e'); + account = await _google.signIn(); + } + // If we still don't have an account after both attempts + if (account == null) { + return (null, Err(_errorMessages['connection']!)); + } + try { final client = await _google.authenticatedClient(); if (client == null) return (null, Err(_errorMessages['auth']!)); - _api = DriveApi(client); return (_api, null); } catch (e) { From 076826b658caadf4bbb5e3c304c09722db620138 Mon Sep 17 00:00:00 2001 From: ethicnology Date: Mon, 24 Feb 2025 10:19:28 -0500 Subject: [PATCH 277/401] refactor: enableGPUValidationMode to 0 before release --- ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme | 1 - 1 file changed, 1 deletion(-) diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index c469fe173..0533c716f 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -77,7 +77,6 @@ ignoresPersistentStateOnLaunch = "NO" debugDocumentVersioning = "YES" debugServiceExtension = "internal" - enableGPUValidationMode = "1" allowLocationSimulation = "YES"> From e797c64640a98bbcd1282f90790c609a2f1196e1 Mon Sep 17 00:00:00 2001 From: ethicnology Date: Mon, 24 Feb 2025 14:08:04 -0500 Subject: [PATCH 278/401] refactor: remove unused functions and useless parameter --- lib/_pkg/backup/google_drive.dart | 4 +--- lib/recoverbull/bloc/backup_settings_cubit.dart | 12 +----------- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/lib/_pkg/backup/google_drive.dart b/lib/_pkg/backup/google_drive.dart index 99bcb860f..0a2b51292 100644 --- a/lib/_pkg/backup/google_drive.dart +++ b/lib/_pkg/backup/google_drive.dart @@ -138,9 +138,7 @@ class GoogleDriveBackupManager extends IBackupManager { } } - Future<(List?, Err?)> loadAllEncryptedBackupFiles({ - required String backupFolder, - }) async { + Future<(List?, Err?)> loadAllEncryptedBackupFiles() async { return _withConnection((api) async { try { final response = await api.files.list( diff --git a/lib/recoverbull/bloc/backup_settings_cubit.dart b/lib/recoverbull/bloc/backup_settings_cubit.dart index 7a4bd9bd2..b24ee792a 100644 --- a/lib/recoverbull/bloc/backup_settings_cubit.dart +++ b/lib/recoverbull/bloc/backup_settings_cubit.dart @@ -90,11 +90,6 @@ class BackupSettingsCubit extends Cubit { static const _kShuffleDelay = Duration(milliseconds: 500); static const _kMinBackupInterval = Duration(seconds: 5); - @override - Future close() async { - await super.close(); - } - void changePassword(String password) { emit( state.copyWith( @@ -262,9 +257,7 @@ class BackupSettingsCubit extends Cubit { } final (availableBackups, err) = - await _googleDriveBackupManager.loadAllEncryptedBackupFiles( - backupFolder: '', // No longer needed - ); + await _googleDriveBackupManager.loadAllEncryptedBackupFiles(); if (err != null) { debugPrint('Error loading backups: ${err.message}'); @@ -449,9 +442,6 @@ class BackupSettingsCubit extends Cubit { } } - Future refreshGoogleDriveBackups() => - fetchGoogleDriveBackup(forceRefresh: true); - Future resetBackupTested() async { await Future.delayed(_kDelayDuration); emit(state.copyWith(backupTested: false)); From ccdee5271f7b18029bc72c8d08250e530f54f0d9 Mon Sep 17 00:00:00 2001 From: ethicnology Date: Tue, 25 Feb 2025 09:12:44 -0500 Subject: [PATCH 279/401] refactor: PIN is supposed to be from 6 to 8 digits with min and max variabilized --- lib/recoverbull/bloc/keychain_cubit.dart | 33 +++++++--------- lib/recoverbull/bloc/keychain_state.dart | 12 ++++-- lib/recoverbull/keychain_page.dart | 48 ++++++------------------ 3 files changed, 34 insertions(+), 59 deletions(-) diff --git a/lib/recoverbull/bloc/keychain_cubit.dart b/lib/recoverbull/bloc/keychain_cubit.dart index 971d207fb..fee479618 100644 --- a/lib/recoverbull/bloc/keychain_cubit.dart +++ b/lib/recoverbull/bloc/keychain_cubit.dart @@ -6,6 +6,9 @@ import 'package:hex/hex.dart'; import 'package:recoverbull/recoverbull.dart'; class KeychainCubit extends Cubit { + static const pinMin = 6; + static const pinMax = 8; + KeychainCubit() : super(const KeychainState()) { shuffleAndEmit(); if (keyServerUrl.isEmpty) { @@ -41,11 +44,7 @@ class KeychainCubit extends Cubit { ); } - void clickObscure() { - emit( - state.copyWith(obscure: !state.obscure), - ); - } + void clickObscure() => emit(state.copyWith(obscure: !state.obscure)); Future clickRecover() async { if (state.backupKey.isNotEmpty) { @@ -59,12 +58,16 @@ class KeychainCubit extends Cubit { } final isServerReady = await serverInfo(); if (!isServerReady) return; - if (state.secret.length < 6) { + if (state.secret.length < pinMin) { state.inputType == KeyChainInputType.pin - ? emit(state.copyWith(error: 'pin should be atleast 6 digits long')) + ? emit( + state.copyWith( + error: 'pin should be atleast $pinMin digits long', + ), + ) : emit( state.copyWith( - error: 'password should be atleast 6 characters long', + error: 'password should be atleast $pinMin characters long', ), ); return; @@ -89,10 +92,7 @@ class KeychainCubit extends Cubit { } catch (e) { debugPrint("Failed to recover backup key: $e"); emit( - state.copyWith( - loading: false, - error: "Failed to recover backup key", - ), + state.copyWith(loading: false, error: "Failed to recover backup key"), ); } } @@ -155,13 +155,8 @@ class KeychainCubit extends Cubit { } void keyPressed(String key) { - if (state.secret.length >= 7) return; - emit( - state.copyWith( - secret: state.secret + key, - error: '', - ), - ); + if (state.secret.length >= pinMax) return; + emit(state.copyWith(secret: state.secret + key, error: '')); } Future secureKey() async { diff --git a/lib/recoverbull/bloc/keychain_state.dart b/lib/recoverbull/bloc/keychain_state.dart index e1f6e484b..7ba471fa5 100644 --- a/lib/recoverbull/bloc/keychain_state.dart +++ b/lib/recoverbull/bloc/keychain_state.dart @@ -1,4 +1,5 @@ import 'package:bb_mobile/_pkg/consts/passwords.dart'; +import 'package:bb_mobile/recoverbull/bloc/keychain_cubit.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; part 'keychain_state.freezed.dart'; @@ -47,10 +48,13 @@ class KeychainState with _$KeychainState { if (secret.isEmpty) return null; if (inputType == KeyChainInputType.pin) { - if (!RegExp(r'^[0-9]{6,7}$').hasMatch(secret)) { - return secret.length < 6 - ? 'PIN must be at least 6 digits long' - : 'PIN must be less than 8 digits'; + const pinMin = KeychainCubit.pinMin; + const pinMax = KeychainCubit.pinMax; + + if (!RegExp('^[0-9]{$pinMin,$pinMax}\$').hasMatch(secret)) { + return secret.length < pinMin + ? 'PIN must be at least $pinMin digits long' + : 'Switch to password if you want more than $pinMax digits'; } } diff --git a/lib/recoverbull/keychain_page.dart b/lib/recoverbull/keychain_page.dart index 9c855e21c..73a339e76 100644 --- a/lib/recoverbull/keychain_page.dart +++ b/lib/recoverbull/keychain_page.dart @@ -416,10 +416,7 @@ class _PinField extends StatelessWidget { const SizedBox(width: 40), Expanded( child: Center( - child: BBText.titleLarge( - state.displayPin(), - isBold: true, - ), + child: BBText.titleLarge(state.displayPin(), isBold: true), ), ), SizedBox( @@ -506,15 +503,12 @@ class KeyPad extends StatelessWidget { for (final i in shuffledNumbers) NumberButton(text: i.toString()), ]; return Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16, - ), + padding: const EdgeInsets.symmetric(horizontal: 16), child: GridView( physics: const NeverScrollableScrollPhysics(), shrinkWrap: true, - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 3, - ), + gridDelegate: + const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3), children: [ for (var i = 0; i < 9; i = i + 1) shuffledNumberButtonList[i], Container(), @@ -651,11 +645,7 @@ class _SetButton extends StatelessWidget { ), ), const SizedBox(width: 8), - const Icon( - Icons.arrow_forward, - color: Colors.white, - size: 16, - ), + const Icon(Icons.arrow_forward, color: Colors.white, size: 16), ], ), ), @@ -674,11 +664,7 @@ class _ConfirmButton extends StatelessWidget { final err = context.select((KeychainCubit x) => x.state.error); if (err.isNotEmpty && inputType == KeyChainInputType.password) { - return Center( - child: BBText.errorSmall( - err, - ), - ); + return Center(child: BBText.errorSmall(err)); } return FilledButton( onPressed: () { @@ -742,20 +728,15 @@ class _RecoverButton extends StatelessWidget { // Switch between PIN and Password InkWell( onTap: () => _switchInputType(context), - child: BBText.bodySmall( - _getSwitchButtonText(), - isBold: true, - ), + child: BBText.bodySmall(_getSwitchButtonText(), isBold: true), ), // Show backup key option only when not in backup key mode if (inputType != KeyChainInputType.backupKey) ...[ const Gap(8), InkWell( onTap: () => _switchToBackupKey(context), - child: const BBText.bodySmall( - 'Recover with backup key', - isBold: true, - ), + child: + const BBText.bodySmall('Recover with backup key', isBold: true), ), ], ], @@ -769,9 +750,7 @@ class _RecoverButton extends StatelessWidget { : () => context.read().clickRecover(), style: FilledButton.styleFrom( backgroundColor: _getButtonColor(context, canRecoverKey), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, @@ -1109,10 +1088,7 @@ class _SubtitleText extends StatelessWidget { Widget build(BuildContext context) { final inputType = context.select((KeychainCubit x) => x.state.inputType); final text = - 'You must memorize this ${inputType == KeyChainInputType.pin ? 'PIN' : 'password'} to recover access to your wallet. It must be at least 6 digits.'; - return BBText.bodySmall( - textAlign: TextAlign.center, - text, - ); + 'You must memorize this ${inputType == KeyChainInputType.pin ? 'PIN' : 'password'} to recover access to your wallet. It must be at least ${KeychainCubit.pinMin} digits.'; + return BBText.bodySmall(textAlign: TextAlign.center, text); } } From e76ff1159405badee4a0bb2eaecea6aab8e635d1 Mon Sep 17 00:00:00 2001 From: ethicnology Date: Tue, 25 Feb 2025 09:33:13 -0500 Subject: [PATCH 280/401] refactor: format to improve readability --- lib/recoverbull/backup_settings.dart | 57 +++-------- .../bloc/backup_settings_cubit.dart | 56 +++-------- lib/recoverbull/encrypted_vault_backup.dart | 51 +++------- lib/recoverbull/keychain_page.dart | 98 ++++--------------- 4 files changed, 58 insertions(+), 204 deletions(-) diff --git a/lib/recoverbull/backup_settings.dart b/lib/recoverbull/backup_settings.dart index 5aa578fea..68329d535 100644 --- a/lib/recoverbull/backup_settings.dart +++ b/lib/recoverbull/backup_settings.dart @@ -1,9 +1,9 @@ import 'package:bb_mobile/_ui/app_bar.dart'; import 'package:bb_mobile/_ui/components/button.dart'; import 'package:bb_mobile/_ui/components/text.dart'; +import 'package:bb_mobile/recoverbull/bloc/backup_settings_cubit.dart'; import 'package:bb_mobile/styles.dart'; import 'package:bb_mobile/wallet/bloc/wallet_bloc.dart'; -import 'package:bb_mobile/recoverbull/bloc/backup_settings_cubit.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:gap/gap.dart'; @@ -26,9 +26,7 @@ class _BackupSettingsState extends State { Widget build(BuildContext context) { return MultiBlocProvider( providers: [ - BlocProvider.value( - value: createOrRetreiveWalletBloc(widget.wallet), - ), + BlocProvider.value(value: createOrRetreiveWalletBloc(widget.wallet)), BlocProvider( create: (BuildContext context) => createBackupSettingsCubit(walletId: widget.wallet), @@ -40,9 +38,7 @@ class _BackupSettingsState extends State { elevation: 0, automaticallyImplyLeading: false, flexibleSpace: BBAppBar( - onBack: () { - context.pop(); - }, + onBack: () => context.pop(), text: 'Backup settings', ), ), @@ -89,10 +85,7 @@ class _Screen extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const BBText.titleLarge( - "Backup settings", - isBold: true, - ), + const BBText.titleLarge("Backup settings", isBold: true), const Gap(10), if (!watchOnly) ...[ BBButton.textWithStatus( @@ -247,24 +240,13 @@ class BackupOptionsScreen extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - BBText.title( - title, - isBold: true, - ), + BBText.title(title, isBold: true), const Gap(4), - BBText.bodySmall( - description, - removeColourOpacity: true, - ), + BBText.bodySmall(description, removeColourOpacity: true), ], ), ), - const Expanded( - child: Icon( - Icons.arrow_forward_ios, - size: 16, - ), - ), + const Expanded(child: Icon(Icons.arrow_forward_ios, size: 16)), ], ), ), @@ -303,9 +285,7 @@ class RecoverOptionsScreen extends StatelessWidget { children: [ TextSpan( text: 'Testing your backup is ', - style: context.font.bodySmall!.copyWith( - fontSize: 12, - ), + style: context.font.bodySmall!.copyWith(fontSize: 12), ), TextSpan( text: 'critically important ', @@ -317,9 +297,7 @@ class RecoverOptionsScreen extends StatelessWidget { TextSpan( text: 'to ensure you can recover your wallet if needed. Choose your recovery method below.', - style: context.font.bodySmall!.copyWith( - fontSize: 12, - ), + style: context.font.bodySmall!.copyWith(fontSize: 12), ), ], ), @@ -377,24 +355,13 @@ class RecoverOptionsScreen extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - BBText.title( - title, - isBold: true, - ), + BBText.title(title, isBold: true), const Gap(4), - BBText.bodySmall( - description, - removeColourOpacity: true, - ), + BBText.bodySmall(description, removeColourOpacity: true), ], ), ), - const Expanded( - child: Icon( - Icons.arrow_forward_ios, - size: 16, - ), - ), + const Expanded(child: Icon(Icons.arrow_forward_ios, size: 16)), ], ), ), diff --git a/lib/recoverbull/bloc/backup_settings_cubit.dart b/lib/recoverbull/bloc/backup_settings_cubit.dart index b24ee792a..5287d2a94 100644 --- a/lib/recoverbull/bloc/backup_settings_cubit.dart +++ b/lib/recoverbull/bloc/backup_settings_cubit.dart @@ -147,9 +147,8 @@ class BackupSettingsCubit extends Cubit { } Future deleteFsBackup() async { - if (_filePicker == null) { - return; - } + if (_filePicker == null) return; + final (file, error) = await _filePicker.pickFile(); if (error != null) { @@ -181,9 +180,7 @@ class BackupSettingsCubit extends Cubit { } final (deleted, err) = - await _googleDriveBackupManager.removeEncryptedBackup( - path: path, - ); + await _googleDriveBackupManager.removeEncryptedBackup(path: path); if (err != null) { emit(state.copyWith(errorSavingBackups: 'Failed to delete backup')); @@ -194,9 +191,8 @@ class BackupSettingsCubit extends Cubit { } Future fetchFsBackup() async { - if (_filePicker == null) { - return; - } + if (_filePicker == null) return; + final (file, error) = await _filePicker.pickFile(); if (error != null) { @@ -213,9 +209,7 @@ class BackupSettingsCubit extends Cubit { encrypted: fileContent, ); if (loadedBackup != null) { - loadedBackup.addAll({ - 'source': 'fs', - }); + loadedBackup.addAll({'source': 'fs'}); emit( state.copyWith( loadingBackups: false, @@ -243,16 +237,11 @@ class BackupSettingsCubit extends Cubit { return; } - _emitSafe( - state.copyWith( - loadingBackups: true, - ), - ); + _emitSafe(state.copyWith(loadingBackups: true)); final (api, connectErr) = await _googleDriveBackupManager.connect(); if (connectErr != null) { _handleLoadError(connectErr.message); - return; } @@ -273,11 +262,13 @@ class BackupSettingsCubit extends Cubit { if (bTime == null) return a; return aTime.compareTo(bTime) > 0 ? a : b; }); + final backupId = latestBackup.name?.split('_').last.split('.').first; if (backupId == null) { _handleLoadError("Corrupted backup file"); return; } + final (loadedBackupMetaData, mediaErr) = await _googleDriveBackupManager.fetchMediaStream( file: latestBackup, @@ -298,6 +289,7 @@ class BackupSettingsCubit extends Cubit { 'source': 'drive', 'filename': latestBackup.name, }); + emit( state.copyWith( loadingBackups: false, @@ -399,12 +391,7 @@ class BackupSettingsCubit extends Cubit { } Future recoverBackupKeyFromMnemonic(int? backupKeyIndex) async { - _emitSafe( - state.copyWith( - loadingBackups: true, - errorLoadingBackups: '', - ), - ); + _emitSafe(state.copyWith(loadingBackups: true, errorLoadingBackups: '')); try { if (backupKeyIndex == null) { @@ -523,12 +510,7 @@ class BackupSettingsCubit extends Cubit { return; } - _emitSafe( - state.copyWith( - savingBackups: true, - errorSavingBackups: '', - ), - ); + _emitSafe(state.copyWith(savingBackups: true, errorSavingBackups: '')); if (_wallets.isEmpty) { _handleLoadError('No wallets available for backup'); @@ -838,12 +820,7 @@ class BackupSettingsCubit extends Cubit { } void _emitBackupError(String message) { - emit( - state.copyWith( - savingBackups: false, - errorSavingBackups: message, - ), - ); + emit(state.copyWith(savingBackups: false, errorSavingBackups: message)); } void _emitBackupState(Seed seed) { @@ -875,12 +852,7 @@ class BackupSettingsCubit extends Cubit { } void _emitBackupTestSuccessState() { - emit( - state.copyWith( - backupTested: true, - testingBackup: false, - ), - ); + emit(state.copyWith(backupTested: true, testingBackup: false)); clearSensitive(); } diff --git a/lib/recoverbull/encrypted_vault_backup.dart b/lib/recoverbull/encrypted_vault_backup.dart index e41baaa79..abc8e52a3 100644 --- a/lib/recoverbull/encrypted_vault_backup.dart +++ b/lib/recoverbull/encrypted_vault_backup.dart @@ -4,10 +4,10 @@ import 'dart:io'; import 'package:bb_mobile/_ui/app_bar.dart'; import 'package:bb_mobile/_ui/components/text.dart'; import 'package:bb_mobile/_ui/toast.dart'; -import 'package:bb_mobile/styles.dart'; import 'package:bb_mobile/recoverbull/bloc/backup_settings_cubit.dart'; import 'package:bb_mobile/recoverbull/bloc/backup_settings_state.dart'; import 'package:bb_mobile/recoverbull/bloc/keychain_state.dart'; +import 'package:bb_mobile/styles.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; @@ -313,10 +313,7 @@ class _EncryptedVaultRecoverPageState extends State { padding: const EdgeInsets.all(20), child: Column( children: [ - const BBText.titleLarge( - 'Where is your backup?', - isBold: true, - ), + const BBText.titleLarge('Where is your backup?', isBold: true), const Gap(20), ...BackupProvider.values.map( (provider) => Padding( @@ -370,9 +367,7 @@ class _EncryptedVaultRecoverPageState extends State { ), ), body: state.loadingBackups - ? const Center( - child: CircularProgressIndicator(), - ) + ? const Center(child: CircularProgressIndicator()) : _buildContent(context, state), ); }, @@ -421,19 +416,13 @@ class _RecoveredBackupInfoPageState extends State { ), ), const Gap(16), - const BBText.title( - 'This is not a backup file', - isBold: true, - ), + const BBText.title('This is not a backup file', isBold: true), const Gap(24), FilledButton( onPressed: () => context.pop(), style: FilledButton.styleFrom( backgroundColor: context.colour.shadow, - padding: const EdgeInsets.symmetric( - horizontal: 24, - vertical: 16, - ), + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), @@ -449,11 +438,7 @@ class _RecoveredBackupInfoPageState extends State { ), ), const Gap(8), - const Icon( - Icons.arrow_forward, - color: Colors.white, - size: 20, - ), + const Icon(Icons.arrow_forward, color: Colors.white, size: 20), ], ), ), @@ -509,34 +494,25 @@ class _RecoveredBackupInfoPageState extends State { showDialog( context: context, builder: (context) => AlertDialog( - title: BBText.titleLarge( - 'Secret key', - isBold: true, - ), + title: const BBText.titleLarge('Secret key', isBold: true), content: Row( children: [ Expanded( child: Text( state.backupKey, - style: context.font.bodySmall!.copyWith( - fontWeight: FontWeight.bold, - ), + style: context.font.bodySmall! + .copyWith(fontWeight: FontWeight.bold), ), ), IconButton( onPressed: () { - Clipboard.setData( - ClipboardData(text: state.backupKey), - ); + Clipboard.setData(ClipboardData(text: state.backupKey)); Navigator.of(context).pop(); ScaffoldMessenger.of(context).showSnackBar( context.showToast('Copied to clipboard'), ); }, - icon: const Icon( - Icons.copy, - color: Colors.black, - ), + icon: const Icon(Icons.copy, color: Colors.black), ), ], ), @@ -681,10 +657,7 @@ class _RecoveredBackupInfoPageState extends State { ), ); }, - icon: const Icon( - Icons.delete, - color: Colors.black, - ), + icon: const Icon(Icons.delete, color: Colors.black), ), ], ), diff --git a/lib/recoverbull/keychain_page.dart b/lib/recoverbull/keychain_page.dart index 73a339e76..6936fa278 100644 --- a/lib/recoverbull/keychain_page.dart +++ b/lib/recoverbull/keychain_page.dart @@ -65,10 +65,7 @@ class KeychainBackupPage extends StatelessWidget { } class _Screen extends StatelessWidget { - const _Screen({ - this.backupKey, - required this.backup, - }); + const _Screen({this.backupKey, required this.backup}); final String? backupKey; final Map backup; @@ -198,9 +195,7 @@ class _Screen extends StatelessWidget { ], child: BlocBuilder( builder: (context, state) { - if (state.loading) { - return const _LoadingView(); - } + if (state.loading) return const _LoadingView(); return Scaffold( appBar: AppBar( @@ -441,9 +436,7 @@ class _PinField extends StatelessWidget { if (error != null) Padding( padding: const EdgeInsets.only(top: 8), - child: Center( - child: BBText.errorSmall(error), - ), + child: Center(child: BBText.errorSmall(error)), ), ], ); @@ -550,35 +543,18 @@ class _NumberButtonState extends State { height: 80, width: 80, child: GestureDetector( - onTapUp: (e) { - setState(() { - isRed = false; - }); - }, - onTapDown: (e) { - setState(() { - isRed = true; - }); - }, - onTapCancel: () { - setState(() { - isRed = false; - }); - }, + onTapUp: (e) => setState(() => isRed = false), + onTapDown: (e) => setState(() => isRed = true), + onTapCancel: () => setState(() => isRed = false), child: OutlinedButton( - style: OutlinedButton.styleFrom( - splashFactory: NoSplash.splashFactory, - ), + style: + OutlinedButton.styleFrom(splashFactory: NoSplash.splashFactory), onPressed: () { SystemSound.play(SystemSoundType.click); HapticFeedback.mediumImpact(); - context.read().keyPressed(widget.text); }, - child: BBText.titleLarge( - widget.text, - isBold: true, - ), + child: BBText.titleLarge(widget.text, isBold: true), ).animate().blur( begin: const Offset(1, 1), end: isRed ? const Offset(2, 2) : Offset.zero, @@ -688,11 +664,7 @@ class _ConfirmButton extends StatelessWidget { ), ), const SizedBox(width: 8), - const Icon( - Icons.arrow_forward, - color: Colors.white, - size: 16, - ), + const Icon(Icons.arrow_forward, color: Colors.white, size: 16), ], ), ); @@ -763,11 +735,7 @@ class _RecoverButton extends StatelessWidget { ), ), const SizedBox(width: 8), - const Icon( - Icons.arrow_forward, - color: Colors.white, - size: 16, - ), + const Icon(Icons.arrow_forward, color: Colors.white, size: 16), ], ), ); @@ -846,11 +814,7 @@ class _DeleteButton extends StatelessWidget { ), ), const SizedBox(width: 8), - const Icon( - Icons.delete_forever, - color: Colors.white, - size: 20, - ), + const Icon(Icons.delete_forever, color: Colors.white, size: 20), ], ), ); @@ -860,10 +824,7 @@ class _DeleteButton extends StatelessWidget { showDialog( context: context, builder: (dialogContext) => AlertDialog( - title: const BBText.title( - 'Delete Backup?', - isBold: true, - ), + title: const BBText.title('Delete Backup?', isBold: true), content: const BBText.bodySmall( 'This action cannot be undone. Are you sure you want to delete this backup?', ), @@ -879,9 +840,8 @@ class _DeleteButton extends StatelessWidget { // Then trigger the delete action using the original context context.read().deleteBackupKey(); }, - style: FilledButton.styleFrom( - backgroundColor: context.colour.error, - ), + style: + FilledButton.styleFrom(backgroundColor: context.colour.error), child: const Text('Delete'), ), ], @@ -947,16 +907,9 @@ class _SuccessDialog extends StatelessWidget { size: 48, ), const Gap(16), - BBText.title( - title, - textAlign: TextAlign.center, - isBold: true, - ), + BBText.title(title, textAlign: TextAlign.center, isBold: true), const Gap(8), - BBText.bodySmall( - message, - textAlign: TextAlign.center, - ), + BBText.bodySmall(message, textAlign: TextAlign.center), const Gap(24), FilledButton( onPressed: () { @@ -1003,10 +956,7 @@ class _ErrorDialog extends StatelessWidget { isBold: true, ), const Gap(8), - BBText.bodySmall( - error, - textAlign: TextAlign.center, - ), + BBText.bodySmall(error, textAlign: TextAlign.center), const Gap(24), FilledButton( onPressed: () => Navigator.of(context).pop(), @@ -1042,11 +992,7 @@ class _TitleText extends StatelessWidget { final text = inputState == KeyChainPageState.enter ? 'Choose a backup ${type == KeyChainInputType.pin ? 'PIN' : 'password'}' : 'Confirm backup ${type == KeyChainInputType.pin ? 'PIN' : 'password'}'; - return BBText.titleLarge( - textAlign: TextAlign.center, - text, - isBold: true, - ); + return BBText.titleLarge(textAlign: TextAlign.center, text, isBold: true); } } @@ -1060,11 +1006,7 @@ class _ConfirmTitleText extends StatelessWidget { final text = 'Confirm backup ${inputType == KeyChainInputType.pin ? 'PIN' : 'password'}'; - return BBText.titleLarge( - textAlign: TextAlign.center, - text, - isBold: true, - ); + return BBText.titleLarge(textAlign: TextAlign.center, text, isBold: true); } } From b66efe87f9cd13ae7dac70a7a786f411a71224a7 Mon Sep 17 00:00:00 2001 From: ethicnology Date: Tue, 25 Feb 2025 09:56:21 -0500 Subject: [PATCH 281/401] refactor: rename function for consistency and use named tuple to improve code readability --- lib/_pkg/backup/_interface.dart | 14 +++----- .../bloc/backup_settings_cubit.dart | 32 ++++++++++--------- 2 files changed, 22 insertions(+), 24 deletions(-) diff --git a/lib/_pkg/backup/_interface.dart b/lib/_pkg/backup/_interface.dart index a53ef9cde..0fddc21f0 100644 --- a/lib/_pkg/backup/_interface.dart +++ b/lib/_pkg/backup/_interface.dart @@ -13,14 +13,12 @@ import 'package:recoverbull/recoverbull.dart' as recoverbull; abstract class IBackupManager { /// Encrypts a list of backups using BIP85 derivation - Future<((String, String)?, Err?)> encryptBackups({ + Future<(({String key, String file})?, Err?)> createEncryptedBackup({ required List backups, required List mnemonic, required String network, }) async { - if (backups.isEmpty) { - return (null, Err('No backups provided')); - } + if (backups.isEmpty) return (null, Err('No backups provided')); try { final randomIndex = _deriveRandomIndex(); @@ -40,14 +38,14 @@ abstract class IBackupManager { 'index': randomIndex, 'encrypted': encrypted, }); - return ((HEX.encode(derived), encoded), null); + return ((key: HEX.encode(derived), file: encoded), null); } catch (e) { return (null, Err('Encryption failed: $e')); } } /// Decrypts an encrypted backup using the provided key - Future<(List?, Err?)> decryptBackups({ + Future<(List?, Err?)> restoreEncryptedBackup({ required String encrypted, required List backupKey, }) async { @@ -118,7 +116,5 @@ abstract class IBackupManager { required String encrypted, }); - Future<(String?, Err?)> removeEncryptedBackup({ - required String path, - }); + Future<(String?, Err?)> removeEncryptedBackup({required String path}); } diff --git a/lib/recoverbull/bloc/backup_settings_cubit.dart b/lib/recoverbull/bloc/backup_settings_cubit.dart index 5287d2a94..8d4277065 100644 --- a/lib/recoverbull/bloc/backup_settings_cubit.dart +++ b/lib/recoverbull/bloc/backup_settings_cubit.dart @@ -359,7 +359,8 @@ class BackupSettingsCubit extends Cubit { return; } - final (backups, decryptErr) = await _fileSystemBackupManager.decryptBackups( + final (backups, decryptErr) = + await _fileSystemBackupManager.restoreEncryptedBackup( encrypted: encrypted, backupKey: HEX.decode(backupKey), ); @@ -451,8 +452,8 @@ class BackupSettingsCubit extends Cubit { return; } - final (encryptedData, err) = await _encryptBackups(backups); - if (err != null || encryptedData == null) { + final (backup, err) = await _createBackup(backups); + if (err != null || backup == null) { _handleSaveError(err?.message ?? 'Encryption failed'); return; } @@ -471,7 +472,7 @@ class BackupSettingsCubit extends Cubit { final (filePath, saveErr) = await _fileSystemBackupManager.saveEncryptedBackup( - encrypted: encryptedData.$2, + encrypted: backup.file, backupFolder: savePath, ); @@ -487,7 +488,7 @@ class BackupSettingsCubit extends Cubit { return; } - final backupSalt = _extractBackupSalt(encryptedData.$2); + final backupSalt = _extractBackupSalt(backup.file); if (backupSalt == null) { _handleSaveError('Failed to extract backup salt'); return; @@ -495,7 +496,7 @@ class BackupSettingsCubit extends Cubit { _emitSafe( state.copyWith( backupId: backupId, - backupKey: encryptedData.$1, + backupKey: backup.key, backupFolderPath: filePath ?? '', backupSalt: backupSalt, savingBackups: false, @@ -529,12 +530,12 @@ class BackupSettingsCubit extends Cubit { return; } - final (encryptedData, encryptErr) = await _encryptBackups(backups); - if (encryptErr != null || encryptedData == null) { + final (backup, encryptErr) = await _createBackup(backups); + if (encryptErr != null || backup == null) { _handleSaveError(encryptErr?.message ?? 'Encryption failed'); return; } - final backupSalt = _extractBackupSalt(encryptedData.$2); + final backupSalt = _extractBackupSalt(backup.file); if (backupSalt == null) { _handleSaveError('Failed to extract backup salt'); return; @@ -542,7 +543,7 @@ class BackupSettingsCubit extends Cubit { final (filePath, saveErr) = await _googleDriveBackupManager.saveEncryptedBackup( - encrypted: encryptedData.$2, + encrypted: backup.file, backupFolder: '', // No longer needed ); @@ -561,7 +562,7 @@ class BackupSettingsCubit extends Cubit { _emitSafe( state.copyWith( backupId: backupId, - backupKey: encryptedData.$1, + backupKey: backup.key, backupFolderPath: fileName, backupSalt: backupSalt, savingBackups: false, @@ -860,7 +861,7 @@ class BackupSettingsCubit extends Cubit { if (!isClosed) emit(newState); } - Future<((String, String)?, Err?)> _encryptBackups( + Future<(({String key, String file})?, Err?)> _createBackup( List backups, ) async { try { @@ -868,17 +869,18 @@ class BackupSettingsCubit extends Cubit { if (fetchMainMnemonicErr != null || mainSeed == null) { return (null, fetchMainMnemonicErr); } - final (encData, err) = await _fileSystemBackupManager.encryptBackups( + final (backup, err) = + await _fileSystemBackupManager.createEncryptedBackup( backups: backups, mnemonic: mainSeed.mnemonic.split(' '), network: mainSeed.network.toString().toLowerCase(), ); - if (err != null || encData == null) { + if (err != null || backup == null) { return (null, err); } - return (encData, null); + return (backup, null); } catch (e) { return (null, Err(e.toString())); } From 5b038a74faa0699f14cdb2ba086d9c3da111dec3 Mon Sep 17 00:00:00 2001 From: ethicnology Date: Tue, 25 Feb 2025 11:08:44 -0500 Subject: [PATCH 282/401] refactor: json backup is not supposed to be utf8 and hex decoded --- lib/_pkg/backup/local.dart | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/lib/_pkg/backup/local.dart b/lib/_pkg/backup/local.dart index 52d347c4a..0313868c8 100644 --- a/lib/_pkg/backup/local.dart +++ b/lib/_pkg/backup/local.dart @@ -35,8 +35,7 @@ class FileSystemBackupManager extends IBackupManager { required String encrypted, }) async { try { - final decodeEncryptedFile = jsonDecode(utf8.decode(HEX.decode(encrypted))) - as Map; + final decodeEncryptedFile = jsonDecode(encrypted) as Map; return (decodeEncryptedFile, null); } catch (e) { return (null, Err('Failed to read encrypted backup: $e')); @@ -66,10 +65,7 @@ class FileSystemBackupManager extends IBackupManager { final backupDir = await Directory(backupFolder).create(recursive: true); final file = File('${backupDir.path}/$filename'); - final (f, errSave) = await fileStorage.saveToFile( - file, - HEX.encode(utf8.encode(encrypted)), - ); + final (f, errSave) = await fileStorage.saveToFile(file, encrypted); if (errSave != null) { return (null, Err(errSave.message)); } From 97c7b89806aca0728c3e3b918bc1eca11b4f636b Mon Sep 17 00:00:00 2001 From: ethicnology Date: Tue, 25 Feb 2025 11:57:01 -0500 Subject: [PATCH 283/401] refactor: we shouldn't have another layer of json wrapping the backup, index should have been added alongside the other fields --- lib/_pkg/backup/_interface.dart | 19 +++----- lib/_pkg/backup/local.dart | 10 ++-- .../bloc/backup_settings_cubit.dart | 46 ++++++------------- lib/recoverbull/bloc/keychain_cubit.dart | 5 +- lib/recoverbull/encrypted_vault_backup.dart | 35 ++++++-------- lib/recoverbull/keychain_page.dart | 19 ++------ 6 files changed, 45 insertions(+), 89 deletions(-) diff --git a/lib/_pkg/backup/_interface.dart b/lib/_pkg/backup/_interface.dart index 0fddc21f0..6f99a5288 100644 --- a/lib/_pkg/backup/_interface.dart +++ b/lib/_pkg/backup/_interface.dart @@ -30,15 +30,15 @@ abstract class IBackupManager { debugPrint(err.toString()); return (null, Err('Failed to derive backup key')); } - final encrypted = recoverbull.BackupService.createBackup( + final jsonBackup = recoverbull.BackupService.createBackup( secret: utf8.encode(plaintext), backupKey: derived, ); - final encoded = jsonEncode({ - 'index': randomIndex, - 'encrypted': encrypted, - }); - return ((key: HEX.encode(derived), file: encoded), null); + + final backup = jsonDecode(jsonBackup); + backup['index'] = randomIndex; + + return ((key: HEX.encode(derived), file: jsonEncode(backup)), null); } catch (e) { return (null, Err('Encryption failed: $e')); } @@ -50,13 +50,8 @@ abstract class IBackupManager { required List backupKey, }) async { try { - final decodedBackup = jsonDecode(encrypted) as Map; - if (!decodedBackup.containsKey("encrypted")) { - return (null, Err('Invalid backup format')); - } - final plaintext = recoverbull.BackupService.restoreBackup( - backup: decodedBackup['encrypted'] as String, + backup: encrypted, backupKey: backupKey, ); diff --git a/lib/_pkg/backup/local.dart b/lib/_pkg/backup/local.dart index 0313868c8..8bf35de99 100644 --- a/lib/_pkg/backup/local.dart +++ b/lib/_pkg/backup/local.dart @@ -6,7 +6,6 @@ import 'package:bb_mobile/_pkg/consts/configs.dart'; import 'package:bb_mobile/_pkg/error.dart'; import 'package:bb_mobile/_pkg/file_storage.dart'; import 'package:bb_mobile/locator.dart'; -import 'package:hex/hex.dart'; class FileSystemBackupManager extends IBackupManager { final FileStorage fileStorage = locator(); @@ -50,9 +49,7 @@ class FileSystemBackupManager extends IBackupManager { String backupFolder = defaultBackupPath, }) async { try { - final decodeEncryptedFile = (jsonDecode(encrypted) - as Map)["encrypted"] as String; - final backupId = jsonDecode(decodeEncryptedFile)['id'] as String; + final backupId = jsonDecode(encrypted)['id'] as String; final now = DateTime.now(); final formattedDate = now.millisecondsSinceEpoch; final filename = '${formattedDate}_$backupId.json'; @@ -66,9 +63,8 @@ class FileSystemBackupManager extends IBackupManager { final file = File('${backupDir.path}/$filename'); final (f, errSave) = await fileStorage.saveToFile(file, encrypted); - if (errSave != null) { - return (null, Err(errSave.message)); - } + if (errSave != null) return (null, Err(errSave.message)); + return (file.path, null); } catch (e) { return (null, Err('Failed to write encrypted backup: $e')); diff --git a/lib/recoverbull/bloc/backup_settings_cubit.dart b/lib/recoverbull/bloc/backup_settings_cubit.dart index 8d4277065..22542c09b 100644 --- a/lib/recoverbull/bloc/backup_settings_cubit.dart +++ b/lib/recoverbull/bloc/backup_settings_cubit.dart @@ -350,9 +350,8 @@ class BackupSettingsCubit extends Cubit { _handleLoadError('Backup key is missing'); return; } - final decoded = jsonDecode(encrypted) as Map; - final backupId = - jsonDecode(decoded['encrypted'] as String)['id'] as String?; + + final backupId = jsonDecode(encrypted)['id'] as String?; if (backupId == null) { _handleLoadError('Invalid backup format'); @@ -481,18 +480,14 @@ class BackupSettingsCubit extends Cubit { return; } - final fileName = filePath?.split('/').last; - final backupId = fileName?.split('_').last.split('.').first; - if (backupId == null) { - _handleSaveError('Failed to extract backup ID'); + final theBackup = json.decode(backup.file); + final backupId = theBackup['id'] as String?; + final backupSalt = theBackup['salt'] as String?; + if (backupId == null || backupSalt == null) { + _handleSaveError('Failed to extract backup metadata'); return; } - final backupSalt = _extractBackupSalt(backup.file); - if (backupSalt == null) { - _handleSaveError('Failed to extract backup salt'); - return; - } _emitSafe( state.copyWith( backupId: backupId, @@ -535,11 +530,6 @@ class BackupSettingsCubit extends Cubit { _handleSaveError(encryptErr?.message ?? 'Encryption failed'); return; } - final backupSalt = _extractBackupSalt(backup.file); - if (backupSalt == null) { - _handleSaveError('Failed to extract backup salt'); - return; - } final (filePath, saveErr) = await _googleDriveBackupManager.saveEncryptedBackup( @@ -552,9 +542,11 @@ class BackupSettingsCubit extends Cubit { return; } - final fileName = filePath?.split('/').last; - final backupId = fileName?.split('_').last.split('.').first; - if (backupId == null || fileName == null) { + final theBackup = json.decode(backup.file); + final backupId = theBackup['id'] as String?; + final backupSalt = theBackup['salt'] as String?; + final filename = filePath?.split('/').last; + if (backupId == null || filename == null || backupSalt == null) { _handleSaveError('Failed to extract backup information'); return; } @@ -563,7 +555,7 @@ class BackupSettingsCubit extends Cubit { state.copyWith( backupId: backupId, backupKey: backup.key, - backupFolderPath: fileName, + backupFolderPath: filename, backupSalt: backupSalt, savingBackups: false, lastBackupAttempt: DateTime.now(), @@ -886,18 +878,6 @@ class BackupSettingsCubit extends Cubit { } } - String? _extractBackupSalt(String encrypted) { - try { - final data = jsonDecode(encrypted) as Map; - final encryptedData = - jsonDecode(data['encrypted'] as String) as Map; - return encryptedData['salt'] as String?; - } catch (e) { - debugPrint('Failed to extract salt: $e'); - return null; - } - } - Future<(Seed?, Err?)> _fetchMainSeed() async { final mainWallet = _wallets.firstWhere( (wallet) => diff --git a/lib/recoverbull/bloc/keychain_cubit.dart b/lib/recoverbull/bloc/keychain_cubit.dart index fee479618..11c9363cc 100644 --- a/lib/recoverbull/bloc/keychain_cubit.dart +++ b/lib/recoverbull/bloc/keychain_cubit.dart @@ -56,18 +56,19 @@ class KeychainCubit extends Cubit { ); return; } + final isServerReady = await serverInfo(); if (!isServerReady) return; if (state.secret.length < pinMin) { state.inputType == KeyChainInputType.pin ? emit( state.copyWith( - error: 'pin should be atleast $pinMin digits long', + error: 'pin should be at least $pinMin digits long', ), ) : emit( state.copyWith( - error: 'password should be atleast $pinMin characters long', + error: 'password should be at least $pinMin characters long', ), ); return; diff --git a/lib/recoverbull/encrypted_vault_backup.dart b/lib/recoverbull/encrypted_vault_backup.dart index abc8e52a3..c139cf223 100644 --- a/lib/recoverbull/encrypted_vault_backup.dart +++ b/lib/recoverbull/encrypted_vault_backup.dart @@ -449,7 +449,8 @@ class _RecoveredBackupInfoPageState extends State { @override Widget build(BuildContext context) { - if (widget.recoveredBackup.isEmpty) { + final recoveryFile = widget.recoveredBackup; + if (recoveryFile.isEmpty) { return Scaffold( appBar: AppBar( elevation: 0, @@ -459,8 +460,9 @@ class _RecoveredBackupInfoPageState extends State { ), body: _buildErrorView(context), ); - } else if (widget.recoveredBackup['index'] == null || - widget.recoveredBackup['encrypted'] == null) { + } else if (recoveryFile['id'] == null || + recoveryFile['ciphertext'] == null || + recoveryFile['salt'] == null) { return Scaffold( appBar: AppBar( elevation: 0, @@ -523,9 +525,6 @@ class _RecoveredBackupInfoPageState extends State { } }, builder: (context, state) { - final recoveredBackupEncrypted = - jsonDecode(widget.recoveredBackup["encrypted"] as String) - as Map; return Scaffold( appBar: AppBar( elevation: 0, @@ -551,15 +550,13 @@ class _RecoveredBackupInfoPageState extends State { children: [ TextSpan( text: 'Backup ID:', - style: context.font.bodyMedium!.copyWith( - fontWeight: FontWeight.bold, - ), + style: context.font.bodyMedium! + .copyWith(fontWeight: FontWeight.bold), ), TextSpan( - text: '${recoveredBackupEncrypted['id']}', - style: context.font.bodyMedium!.copyWith( - fontWeight: FontWeight.bold, - ), + text: '${recoveryFile['id']}', + style: context.font.bodyMedium! + .copyWith(fontWeight: FontWeight.bold), ), ], ), @@ -571,16 +568,14 @@ class _RecoveredBackupInfoPageState extends State { children: [ TextSpan( text: 'Created at:', - style: context.font.bodyMedium!.copyWith( - fontWeight: FontWeight.bold, - ), + style: context.font.bodyMedium! + .copyWith(fontWeight: FontWeight.bold), ), TextSpan( text: - ' ${DateFormat('MMM dd, yyyy HH:mm:ss').format(DateTime.fromMillisecondsSinceEpoch(recoveredBackupEncrypted['createdAt'] as int).toLocal())}', - style: context.font.bodyMedium!.copyWith( - fontWeight: FontWeight.bold, - ), + ' ${DateFormat('MMM dd, yyyy HH:mm:ss').format(DateTime.fromMillisecondsSinceEpoch(recoveryFile['createdAt'] as int).toLocal())}', + style: context.font.bodyMedium! + .copyWith(fontWeight: FontWeight.bold), ), ], ), diff --git a/lib/recoverbull/keychain_page.dart b/lib/recoverbull/keychain_page.dart index 6936fa278..a2d412f09 100644 --- a/lib/recoverbull/keychain_page.dart +++ b/lib/recoverbull/keychain_page.dart @@ -32,7 +32,8 @@ class KeychainBackupPage extends StatelessWidget { @override Widget build(BuildContext context) { // Extract backup data - final backupData = _extractBackupData(); + final backupId = backup['id'] as String?; + final backupSalt = backup['salt'] as String?; return MultiBlocProvider( providers: [ @@ -40,9 +41,9 @@ class KeychainBackupPage extends StatelessWidget { create: (context) => KeychainCubit() ..setChainState( _pState, // Use the provided state directly instead of determining it - backupData.$1 ?? '', + backupId ?? '', backupKey, - backupData.$2 ?? '', + backupSalt ?? '', ), ), BlocProvider.value(value: createBackupSettingsCubit()), @@ -50,18 +51,6 @@ class KeychainBackupPage extends StatelessWidget { child: _Screen(backupKey: backupKey, backup: backup), ); } - - (String?, String?) _extractBackupData() { - if (backupKey?.isNotEmpty ?? false) { - return (backup['id']?.toString(), backup['salt']?.toString()); - } - - final encryptedData = backup["encrypted"] is String - ? jsonDecode(backup["encrypted"] as String) as Map - : {}; - - return (encryptedData["id"]?.toString(), encryptedData["salt"] as String?); - } } class _Screen extends StatelessWidget { From 799fed45ab4746cf22c40b0243148bbfb7129466 Mon Sep 17 00:00:00 2001 From: ethicnology Date: Tue, 25 Feb 2025 13:43:01 -0500 Subject: [PATCH 284/401] refactor: rename package backup to recoverbull --- lib/_pkg/{backup => recoverbull}/_interface.dart | 0 lib/_pkg/{backup => recoverbull}/google_drive.dart | 2 +- lib/_pkg/{backup => recoverbull}/local.dart | 2 +- lib/locator.dart | 4 ++-- lib/recoverbull/bloc/backup_settings_cubit.dart | 4 ++-- lib/recoverbull/encrypted_vault_backup.dart | 1 - 6 files changed, 6 insertions(+), 7 deletions(-) rename lib/_pkg/{backup => recoverbull}/_interface.dart (100%) rename lib/_pkg/{backup => recoverbull}/google_drive.dart (99%) rename lib/_pkg/{backup => recoverbull}/local.dart (97%) diff --git a/lib/_pkg/backup/_interface.dart b/lib/_pkg/recoverbull/_interface.dart similarity index 100% rename from lib/_pkg/backup/_interface.dart rename to lib/_pkg/recoverbull/_interface.dart diff --git a/lib/_pkg/backup/google_drive.dart b/lib/_pkg/recoverbull/google_drive.dart similarity index 99% rename from lib/_pkg/backup/google_drive.dart rename to lib/_pkg/recoverbull/google_drive.dart index 0a2b51292..67101b600 100644 --- a/lib/_pkg/backup/google_drive.dart +++ b/lib/_pkg/recoverbull/google_drive.dart @@ -1,9 +1,9 @@ import 'dart:async'; import 'dart:convert'; -import 'package:bb_mobile/_pkg/backup/_interface.dart'; import 'package:bb_mobile/_pkg/consts/configs.dart'; import 'package:bb_mobile/_pkg/error.dart'; +import 'package:bb_mobile/_pkg/recoverbull/_interface.dart'; import 'package:extension_google_sign_in_as_googleapis_auth/extension_google_sign_in_as_googleapis_auth.dart'; import 'package:flutter/foundation.dart'; import 'package:google_sign_in/google_sign_in.dart'; diff --git a/lib/_pkg/backup/local.dart b/lib/_pkg/recoverbull/local.dart similarity index 97% rename from lib/_pkg/backup/local.dart rename to lib/_pkg/recoverbull/local.dart index 8bf35de99..2263b0df4 100644 --- a/lib/_pkg/backup/local.dart +++ b/lib/_pkg/recoverbull/local.dart @@ -1,10 +1,10 @@ import 'dart:convert'; import 'dart:io'; -import 'package:bb_mobile/_pkg/backup/_interface.dart'; import 'package:bb_mobile/_pkg/consts/configs.dart'; import 'package:bb_mobile/_pkg/error.dart'; import 'package:bb_mobile/_pkg/file_storage.dart'; +import 'package:bb_mobile/_pkg/recoverbull/_interface.dart'; import 'package:bb_mobile/locator.dart'; class FileSystemBackupManager extends IBackupManager { diff --git a/lib/locator.dart b/lib/locator.dart index 9d6924710..6b3c8f6f3 100644 --- a/lib/locator.dart +++ b/lib/locator.dart @@ -1,5 +1,3 @@ -import 'package:bb_mobile/_pkg/backup/google_drive.dart'; -import 'package:bb_mobile/_pkg/backup/local.dart'; import 'package:bb_mobile/_pkg/barcode.dart'; import 'package:bb_mobile/_pkg/boltz/swap.dart'; import 'package:bb_mobile/_pkg/bull_bitcoin_api.dart'; @@ -13,6 +11,8 @@ import 'package:bb_mobile/_pkg/mempool_api.dart'; import 'package:bb_mobile/_pkg/nfc.dart'; import 'package:bb_mobile/_pkg/payjoin/manager.dart'; import 'package:bb_mobile/_pkg/payjoin/storage.dart'; +import 'package:bb_mobile/_pkg/recoverbull/google_drive.dart'; +import 'package:bb_mobile/_pkg/recoverbull/local.dart'; import 'package:bb_mobile/_pkg/storage/hive.dart'; import 'package:bb_mobile/_pkg/storage/secure_storage.dart'; import 'package:bb_mobile/_pkg/storage/storage.dart'; diff --git a/lib/recoverbull/bloc/backup_settings_cubit.dart b/lib/recoverbull/bloc/backup_settings_cubit.dart index 22542c09b..5f25ffd6a 100644 --- a/lib/recoverbull/bloc/backup_settings_cubit.dart +++ b/lib/recoverbull/bloc/backup_settings_cubit.dart @@ -4,10 +4,10 @@ import 'dart:convert'; import 'package:bb_mobile/_model/backup.dart'; import 'package:bb_mobile/_model/seed.dart'; import 'package:bb_mobile/_model/wallet.dart'; -import 'package:bb_mobile/_pkg/backup/google_drive.dart'; -import 'package:bb_mobile/_pkg/backup/local.dart'; import 'package:bb_mobile/_pkg/error.dart'; import 'package:bb_mobile/_pkg/file_picker.dart'; +import 'package:bb_mobile/_pkg/recoverbull/google_drive.dart'; +import 'package:bb_mobile/_pkg/recoverbull/local.dart'; import 'package:bb_mobile/_pkg/wallet/bdk/sensitive_create.dart'; import 'package:bb_mobile/_pkg/wallet/create.dart'; import 'package:bb_mobile/_pkg/wallet/create_sensitive.dart'; diff --git a/lib/recoverbull/encrypted_vault_backup.dart b/lib/recoverbull/encrypted_vault_backup.dart index c139cf223..2a8ef7fde 100644 --- a/lib/recoverbull/encrypted_vault_backup.dart +++ b/lib/recoverbull/encrypted_vault_backup.dart @@ -1,4 +1,3 @@ -import 'dart:convert'; import 'dart:io'; import 'package:bb_mobile/_ui/app_bar.dart'; From 604b8090f16f5e68fafcd588e6637fe168b50c57 Mon Sep 17 00:00:00 2001 From: ethicnology Date: Tue, 25 Feb 2025 13:54:21 -0500 Subject: [PATCH 285/401] refactor: rename Backup to WalletSensitiveData to avoid confusion with the backup generated by recoverbull --- .../{backup.dart => wallet_sensitive_data.dart} | 15 ++++++++------- lib/_pkg/recoverbull/_interface.dart | 9 +++++---- lib/recoverbull/bloc/backup_settings_cubit.dart | 16 ++++++++-------- lib/recoverbull/bloc/backup_settings_state.dart | 4 ++-- 4 files changed, 23 insertions(+), 21 deletions(-) rename lib/_model/{backup.dart => wallet_sensitive_data.dart} (56%) diff --git a/lib/_model/backup.dart b/lib/_model/wallet_sensitive_data.dart similarity index 56% rename from lib/_model/backup.dart rename to lib/_model/wallet_sensitive_data.dart index 0e9c71583..d397c508c 100644 --- a/lib/_model/backup.dart +++ b/lib/_model/wallet_sensitive_data.dart @@ -1,11 +1,11 @@ import 'package:freezed_annotation/freezed_annotation.dart'; -part 'backup.freezed.dart'; -part 'backup.g.dart'; +part 'wallet_sensitive_data.freezed.dart'; +part 'wallet_sensitive_data.g.dart'; @freezed -class Backup with _$Backup { - const factory Backup({ +class WalletSensitiveData with _$WalletSensitiveData { + const factory WalletSensitiveData({ @Default(1) int version, @Default('') String name, @Default([]) List mnemonic, @@ -15,11 +15,12 @@ class Backup with _$Backup { @Default('') String type, @Default('') String script, @Default('') String publicDescriptors, - }) = _Backup; + }) = _WalletSensitiveData; - factory Backup.fromJson(Map json) => _$BackupFromJson(json); + factory WalletSensitiveData.fromJson(Map json) => + _$WalletSensitiveDataFromJson(json); - const Backup._(); + const WalletSensitiveData._(); bool get isEmpty => mnemonic.isEmpty && passphrase.isEmpty; } diff --git a/lib/_pkg/recoverbull/_interface.dart b/lib/_pkg/recoverbull/_interface.dart index 6f99a5288..2419e07d0 100644 --- a/lib/_pkg/recoverbull/_interface.dart +++ b/lib/_pkg/recoverbull/_interface.dart @@ -1,8 +1,8 @@ import 'dart:convert'; import 'dart:math'; -import 'package:bb_mobile/_model/backup.dart'; import 'package:bb_mobile/_model/wallet.dart'; +import 'package:bb_mobile/_model/wallet_sensitive_data.dart'; import 'package:bb_mobile/_pkg/consts/configs.dart'; import 'package:bb_mobile/_pkg/error.dart'; import 'package:bdk_flutter/bdk_flutter.dart'; @@ -14,7 +14,7 @@ import 'package:recoverbull/recoverbull.dart' as recoverbull; abstract class IBackupManager { /// Encrypts a list of backups using BIP85 derivation Future<(({String key, String file})?, Err?)> createEncryptedBackup({ - required List backups, + required List backups, required List mnemonic, required String network, }) async { @@ -45,7 +45,7 @@ abstract class IBackupManager { } /// Decrypts an encrypted backup using the provided key - Future<(List?, Err?)> restoreEncryptedBackup({ + Future<(List?, Err?)> restoreEncryptedBackup({ required String encrypted, required List backupKey, }) async { @@ -57,7 +57,8 @@ abstract class IBackupManager { final decodedJson = jsonDecode(plaintext) as List; final backups = decodedJson - .map((item) => Backup.fromJson(item as Map)) + .map((item) => + WalletSensitiveData.fromJson(item as Map)) .toList(); return (backups, null); diff --git a/lib/recoverbull/bloc/backup_settings_cubit.dart b/lib/recoverbull/bloc/backup_settings_cubit.dart index 5f25ffd6a..4effa33cf 100644 --- a/lib/recoverbull/bloc/backup_settings_cubit.dart +++ b/lib/recoverbull/bloc/backup_settings_cubit.dart @@ -1,9 +1,9 @@ import 'dart:async'; import 'dart:convert'; -import 'package:bb_mobile/_model/backup.dart'; import 'package:bb_mobile/_model/seed.dart'; import 'package:bb_mobile/_model/wallet.dart'; +import 'package:bb_mobile/_model/wallet_sensitive_data.dart'; import 'package:bb_mobile/_pkg/error.dart'; import 'package:bb_mobile/_pkg/file_picker.dart'; import 'package:bb_mobile/_pkg/recoverbull/google_drive.dart'; @@ -347,7 +347,7 @@ class BackupSettingsCubit extends Cubit { ); if (backupKey.isEmpty) { - _handleLoadError('Backup key is missing'); + _handleLoadError('WalletSensitiveData key is missing'); return; } @@ -750,8 +750,8 @@ class BackupSettingsCubit extends Cubit { } } - Future> _createBackupsForAllWallets() async { - final backups = []; + Future> _createBackupsForAllWallets() async { + final backups = []; try { for (final wallet in _wallets) { @@ -766,7 +766,7 @@ class BackupSettingsCubit extends Cubit { } } - Future _createBackupForWallet(Wallet wallet) async { + Future _createBackupForWallet(Wallet wallet) async { try { final (seed, err) = await _loadWalletSeed(wallet); if (err != null || seed == null) { @@ -775,7 +775,7 @@ class BackupSettingsCubit extends Cubit { return null; } - final backup = Backup( + final backup = WalletSensitiveData( name: wallet.name ?? '', network: wallet.network.name, layer: wallet.baseWalletType.name, @@ -854,7 +854,7 @@ class BackupSettingsCubit extends Cubit { } Future<(({String key, String file})?, Err?)> _createBackup( - List backups, + List backups, ) async { try { final (mainSeed, fetchMainMnemonicErr) = await _fetchMainSeed(); @@ -962,7 +962,7 @@ class BackupSettingsCubit extends Cubit { return (seed, err); } - Future _processBackupRecovery(Backup backup) async { + Future _processBackupRecovery(WalletSensitiveData backup) async { final network = BBNetwork.fromString(backup.network); final layer = _getLayer(backup.layer); final script = _getScript(backup.script); diff --git a/lib/recoverbull/bloc/backup_settings_state.dart b/lib/recoverbull/bloc/backup_settings_state.dart index edf8d81bb..53be51b14 100644 --- a/lib/recoverbull/bloc/backup_settings_state.dart +++ b/lib/recoverbull/bloc/backup_settings_state.dart @@ -1,4 +1,4 @@ -import 'package:bb_mobile/_model/backup.dart'; +import 'package:bb_mobile/_model/wallet_sensitive_data.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; part 'backup_settings_state.freezed.dart'; @@ -17,7 +17,7 @@ class BackupSettingsState with _$BackupSettingsState { @Default('') String errTestingBackup, @Default(false) bool backupTested, @Default(false) bool loadingBackups, - @Default([]) List loadedBackups, + @Default([]) List loadedBackups, @Default('') String errorLoadingBackups, @Default(false) bool savingBackups, @Default('') String errorSavingBackups, From 8973d1f266e01583e6c3e40254f6243a0b16c8ea Mon Sep 17 00:00:00 2001 From: ethicnology Date: Tue, 25 Feb 2025 13:55:44 -0500 Subject: [PATCH 286/401] refactor: rename IBackupManager to IRecoverbullManager to avoid confusion with future metadata backup --- lib/_pkg/recoverbull/_interface.dart | 2 +- lib/_pkg/recoverbull/google_drive.dart | 2 +- lib/_pkg/recoverbull/local.dart | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/_pkg/recoverbull/_interface.dart b/lib/_pkg/recoverbull/_interface.dart index 2419e07d0..7b01f1520 100644 --- a/lib/_pkg/recoverbull/_interface.dart +++ b/lib/_pkg/recoverbull/_interface.dart @@ -11,7 +11,7 @@ import 'package:flutter/foundation.dart'; import 'package:hex/hex.dart'; import 'package:recoverbull/recoverbull.dart' as recoverbull; -abstract class IBackupManager { +abstract class IRecoverbullManager { /// Encrypts a list of backups using BIP85 derivation Future<(({String key, String file})?, Err?)> createEncryptedBackup({ required List backups, diff --git a/lib/_pkg/recoverbull/google_drive.dart b/lib/_pkg/recoverbull/google_drive.dart index 67101b600..09ca8f59f 100644 --- a/lib/_pkg/recoverbull/google_drive.dart +++ b/lib/_pkg/recoverbull/google_drive.dart @@ -9,7 +9,7 @@ import 'package:flutter/foundation.dart'; import 'package:google_sign_in/google_sign_in.dart'; import 'package:googleapis/drive/v3.dart'; -class GoogleDriveBackupManager extends IBackupManager { +class GoogleDriveBackupManager extends IRecoverbullManager { static final _google = GoogleSignIn( scopes: ['https://www.googleapis.com/auth/drive.appdata'], ); diff --git a/lib/_pkg/recoverbull/local.dart b/lib/_pkg/recoverbull/local.dart index 2263b0df4..bf4665f38 100644 --- a/lib/_pkg/recoverbull/local.dart +++ b/lib/_pkg/recoverbull/local.dart @@ -7,7 +7,7 @@ import 'package:bb_mobile/_pkg/file_storage.dart'; import 'package:bb_mobile/_pkg/recoverbull/_interface.dart'; import 'package:bb_mobile/locator.dart'; -class FileSystemBackupManager extends IBackupManager { +class FileSystemBackupManager extends IRecoverbullManager { final FileStorage fileStorage = locator(); FileSystemBackupManager(); From 5d4c7caa30d623355e820d9be9a95c26d1fcbb2a Mon Sep 17 00:00:00 2001 From: ethicnology Date: Tue, 25 Feb 2025 14:01:28 -0500 Subject: [PATCH 287/401] refactor: rename encrypted backup and remove unnecessary backupFolder --- lib/_pkg/recoverbull/_interface.dart | 17 ++++++++------- lib/_pkg/recoverbull/google_drive.dart | 12 +++++------ lib/_pkg/recoverbull/local.dart | 10 ++++----- .../bloc/backup_settings_cubit.dart | 21 +++++++------------ 4 files changed, 28 insertions(+), 32 deletions(-) diff --git a/lib/_pkg/recoverbull/_interface.dart b/lib/_pkg/recoverbull/_interface.dart index 7b01f1520..99af20e24 100644 --- a/lib/_pkg/recoverbull/_interface.dart +++ b/lib/_pkg/recoverbull/_interface.dart @@ -14,22 +14,23 @@ import 'package:recoverbull/recoverbull.dart' as recoverbull; abstract class IRecoverbullManager { /// Encrypts a list of backups using BIP85 derivation Future<(({String key, String file})?, Err?)> createEncryptedBackup({ - required List backups, + required List wallets, required List mnemonic, required String network, }) async { - if (backups.isEmpty) return (null, Err('No backups provided')); + if (wallets.isEmpty) return (null, Err('No backups provided')); try { + final plaintext = json.encode(wallets.map((i) => i.toJson()).toList()); + final randomIndex = _deriveRandomIndex(); final (derived, err) = await deriveBackupKey(mnemonic, network, randomIndex); - final plaintext = json.encode(backups.map((i) => i.toJson()).toList()); - if (derived == null) { debugPrint(err.toString()); return (null, Err('Failed to derive backup key')); } + final jsonBackup = recoverbull.BackupService.createBackup( secret: utf8.encode(plaintext), backupKey: derived, @@ -46,12 +47,12 @@ abstract class IRecoverbullManager { /// Decrypts an encrypted backup using the provided key Future<(List?, Err?)> restoreEncryptedBackup({ - required String encrypted, + required String backup, required List backupKey, }) async { try { final plaintext = recoverbull.BackupService.restoreBackup( - backup: encrypted, + backup: backup, backupKey: backupKey, ); @@ -104,12 +105,12 @@ abstract class IRecoverbullManager { // Abstract methods to be implemented by concrete classes Future<(String?, Err?)> saveEncryptedBackup({ - required String encrypted, + required String backup, String backupFolder = defaultBackupPath, }); Future<(Map?, Err?)> loadEncryptedBackup({ - required String encrypted, + required String backup, }); Future<(String?, Err?)> removeEncryptedBackup({required String path}); diff --git a/lib/_pkg/recoverbull/google_drive.dart b/lib/_pkg/recoverbull/google_drive.dart index 09ca8f59f..4ad1e0540 100644 --- a/lib/_pkg/recoverbull/google_drive.dart +++ b/lib/_pkg/recoverbull/google_drive.dart @@ -90,12 +90,12 @@ class GoogleDriveBackupManager extends IRecoverbullManager { @override Future<(String?, Err?)> saveEncryptedBackup({ - required String encrypted, + required String backup, String backupFolder = defaultBackupPath, }) async { return _withConnection((api) async { try { - final data = jsonDecode(encrypted) as Map; + final data = jsonDecode(backup) as Map; final encryptedData = data['encrypted'] as String; final decodedEncrypted = jsonDecode(encryptedData) as Map; @@ -113,8 +113,8 @@ class GoogleDriveBackupManager extends IRecoverbullManager { await api.files.create( file, uploadMedia: Media( - Stream.value(utf8.encode(encrypted)), - encrypted.length, + Stream.value(utf8.encode(backup)), + backup.length, ), ); @@ -127,10 +127,10 @@ class GoogleDriveBackupManager extends IRecoverbullManager { @override Future<(Map?, Err?)> loadEncryptedBackup({ - required String encrypted, + required String backup, }) async { try { - final decodeEncryptedFile = jsonDecode(encrypted) as Map; + final decodeEncryptedFile = jsonDecode(backup) as Map; return (decodeEncryptedFile, null); } catch (e) { debugPrint('Failed to decode backup: $e'); diff --git a/lib/_pkg/recoverbull/local.dart b/lib/_pkg/recoverbull/local.dart index bf4665f38..4013aba8f 100644 --- a/lib/_pkg/recoverbull/local.dart +++ b/lib/_pkg/recoverbull/local.dart @@ -31,10 +31,10 @@ class FileSystemBackupManager extends IRecoverbullManager { /// Returns a map containing the backup data and the backup ID, or an error message. @override Future<(Map?, Err?)> loadEncryptedBackup({ - required String encrypted, + required String backup, }) async { try { - final decodeEncryptedFile = jsonDecode(encrypted) as Map; + final decodeEncryptedFile = jsonDecode(backup) as Map; return (decodeEncryptedFile, null); } catch (e) { return (null, Err('Failed to read encrypted backup: $e')); @@ -45,11 +45,11 @@ class FileSystemBackupManager extends IRecoverbullManager { /// Returns the path to the written backup or an error message. @override Future<(String?, Err?)> saveEncryptedBackup({ - required String encrypted, + required String backup, String backupFolder = defaultBackupPath, }) async { try { - final backupId = jsonDecode(encrypted)['id'] as String; + final backupId = jsonDecode(backup)['id'] as String; final now = DateTime.now(); final formattedDate = now.millisecondsSinceEpoch; final filename = '${formattedDate}_$backupId.json'; @@ -62,7 +62,7 @@ class FileSystemBackupManager extends IRecoverbullManager { final backupDir = await Directory(backupFolder).create(recursive: true); final file = File('${backupDir.path}/$filename'); - final (f, errSave) = await fileStorage.saveToFile(file, encrypted); + final (f, errSave) = await fileStorage.saveToFile(file, backup); if (errSave != null) return (null, Err(errSave.message)); return (file.path, null); diff --git a/lib/recoverbull/bloc/backup_settings_cubit.dart b/lib/recoverbull/bloc/backup_settings_cubit.dart index 4effa33cf..b38c3ace8 100644 --- a/lib/recoverbull/bloc/backup_settings_cubit.dart +++ b/lib/recoverbull/bloc/backup_settings_cubit.dart @@ -205,9 +205,7 @@ class BackupSettingsCubit extends Cubit { return; } final (loadedBackup, err) = - await _fileSystemBackupManager.loadEncryptedBackup( - encrypted: fileContent, - ); + await _fileSystemBackupManager.loadEncryptedBackup(backup: fileContent); if (loadedBackup != null) { loadedBackup.addAll({'source': 'fs'}); emit( @@ -282,7 +280,7 @@ class BackupSettingsCubit extends Cubit { final (loadedBackup, err) = await _googleDriveBackupManager.loadEncryptedBackup( - encrypted: utf8.decode(loadedBackupMetaData), + backup: utf8.decode(loadedBackupMetaData), ); if (loadedBackup != null) { loadedBackup.addAll({ @@ -360,7 +358,7 @@ class BackupSettingsCubit extends Cubit { final (backups, decryptErr) = await _fileSystemBackupManager.restoreEncryptedBackup( - encrypted: encrypted, + backup: encrypted, backupKey: HEX.decode(backupKey), ); @@ -471,7 +469,7 @@ class BackupSettingsCubit extends Cubit { final (filePath, saveErr) = await _fileSystemBackupManager.saveEncryptedBackup( - encrypted: backup.file, + backup: backup.file, backupFolder: savePath, ); @@ -531,11 +529,8 @@ class BackupSettingsCubit extends Cubit { return; } - final (filePath, saveErr) = - await _googleDriveBackupManager.saveEncryptedBackup( - encrypted: backup.file, - backupFolder: '', // No longer needed - ); + final (filePath, saveErr) = await _googleDriveBackupManager + .saveEncryptedBackup(backup: backup.file); if (saveErr != null) { _handleSaveError('Failed to save to Google Drive: ${saveErr.message}'); @@ -854,7 +849,7 @@ class BackupSettingsCubit extends Cubit { } Future<(({String key, String file})?, Err?)> _createBackup( - List backups, + List wallets, ) async { try { final (mainSeed, fetchMainMnemonicErr) = await _fetchMainSeed(); @@ -863,7 +858,7 @@ class BackupSettingsCubit extends Cubit { } final (backup, err) = await _fileSystemBackupManager.createEncryptedBackup( - backups: backups, + wallets: wallets, mnemonic: mainSeed.mnemonic.split(' '), network: mainSeed.network.toString().toLowerCase(), ); From c6df17dbfc56c817d344eda8411dad6d8d334891 Mon Sep 17 00:00:00 2001 From: ethicnology Date: Tue, 25 Feb 2025 14:09:54 -0500 Subject: [PATCH 288/401] refactor: the full bip85 path should be included in the backup not only the index --- lib/_pkg/recoverbull/_interface.dart | 10 ++++++---- lib/recoverbull/bloc/backup_settings_cubit.dart | 8 ++++---- lib/recoverbull/encrypted_vault_backup.dart | 2 +- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/lib/_pkg/recoverbull/_interface.dart b/lib/_pkg/recoverbull/_interface.dart index 99af20e24..f598ba532 100644 --- a/lib/_pkg/recoverbull/_interface.dart +++ b/lib/_pkg/recoverbull/_interface.dart @@ -24,8 +24,10 @@ abstract class IRecoverbullManager { final plaintext = json.encode(wallets.map((i) => i.toJson()).toList()); final randomIndex = _deriveRandomIndex(); + final derivationPath = "m/1608'/0'/$randomIndex"; + final (derived, err) = - await deriveBackupKey(mnemonic, network, randomIndex); + await deriveBackupKey(mnemonic, network, derivationPath); if (derived == null) { debugPrint(err.toString()); return (null, Err('Failed to derive backup key')); @@ -37,7 +39,7 @@ abstract class IRecoverbullManager { ); final backup = jsonDecode(jsonBackup); - backup['index'] = randomIndex; + backup['path'] = derivationPath; return ((key: HEX.encode(derived), file: jsonEncode(backup)), null); } catch (e) { @@ -83,7 +85,7 @@ abstract class IRecoverbullManager { Future<(List?, Err?)> deriveBackupKey( List mnemonic, String network, - int keyPathIndex, + String derivationPath, ) async { try { final descriptorSecretKey = await DescriptorSecretKey.create( @@ -94,7 +96,7 @@ abstract class IRecoverbullManager { final key = bip85 .derive( xprv: descriptorSecretKey.toString().split('/*').first, - path: "m/1608'/0'/$keyPathIndex", + path: derivationPath, ) .sublist(0, 32); return (key, null); diff --git a/lib/recoverbull/bloc/backup_settings_cubit.dart b/lib/recoverbull/bloc/backup_settings_cubit.dart index b38c3ace8..6250f10cc 100644 --- a/lib/recoverbull/bloc/backup_settings_cubit.dart +++ b/lib/recoverbull/bloc/backup_settings_cubit.dart @@ -388,12 +388,12 @@ class BackupSettingsCubit extends Cubit { ); } - Future recoverBackupKeyFromMnemonic(int? backupKeyIndex) async { + Future recoverBackupKeyFromMnemonic(String? derivationPath) async { _emitSafe(state.copyWith(loadingBackups: true, errorLoadingBackups: '')); try { - if (backupKeyIndex == null) { - _handleLoadError('Invalid backup format - missing index'); + if (derivationPath == null) { + _handleLoadError('Invalid backup format - missing derivation path'); return; } @@ -407,7 +407,7 @@ class BackupSettingsCubit extends Cubit { await _fileSystemBackupManager.deriveBackupKey( mainSeed.mnemonic.split(' '), mainSeed.network.toString(), - backupKeyIndex, + derivationPath, ); if (backupKey == null) { diff --git a/lib/recoverbull/encrypted_vault_backup.dart b/lib/recoverbull/encrypted_vault_backup.dart index 2a8ef7fde..fc9905c22 100644 --- a/lib/recoverbull/encrypted_vault_backup.dart +++ b/lib/recoverbull/encrypted_vault_backup.dart @@ -632,7 +632,7 @@ class _RecoveredBackupInfoPageState extends State { const Gap(10), InkWell( onTap: () => _cubit.recoverBackupKeyFromMnemonic( - widget.recoveredBackup['index'] as int?, + widget.recoveredBackup['path'] as String?, ), child: const BBText.bodySmall( 'Forgot your secret? Click to recover', From d6835bbd3ead1f6163efc3f8b83bd0b9065a6746 Mon Sep 17 00:00:00 2001 From: ethicnology Date: Tue, 25 Feb 2025 15:41:21 -0500 Subject: [PATCH 289/401] refactor: use BullBackup struct to avoid serialization/deserialization and check existance of backup attributes --- lib/_pkg/recoverbull/_interface.dart | 38 ++++++--- lib/_pkg/recoverbull/google_drive.dart | 33 ++----- lib/_pkg/recoverbull/local.dart | 23 +---- .../bloc/backup_settings_cubit.dart | 85 +++++++++---------- lib/recoverbull/encrypted_vault_backup.dart | 2 +- 5 files changed, 77 insertions(+), 104 deletions(-) diff --git a/lib/_pkg/recoverbull/_interface.dart b/lib/_pkg/recoverbull/_interface.dart index f598ba532..9fb4af909 100644 --- a/lib/_pkg/recoverbull/_interface.dart +++ b/lib/_pkg/recoverbull/_interface.dart @@ -9,11 +9,11 @@ import 'package:bdk_flutter/bdk_flutter.dart'; import 'package:bip85/bip85.dart' as bip85; import 'package:flutter/foundation.dart'; import 'package:hex/hex.dart'; -import 'package:recoverbull/recoverbull.dart' as recoverbull; +import 'package:recoverbull/recoverbull.dart'; abstract class IRecoverbullManager { /// Encrypts a list of backups using BIP85 derivation - Future<(({String key, String file})?, Err?)> createEncryptedBackup({ + Future<(({String key, BullBackup backup})?, Err?)> createEncryptedBackup({ required List wallets, required List mnemonic, required String network, @@ -33,15 +33,17 @@ abstract class IRecoverbullManager { return (null, Err('Failed to derive backup key')); } - final jsonBackup = recoverbull.BackupService.createBackup( + final backup = BackupService.createBackup( secret: utf8.encode(plaintext), backupKey: derived, ); - final backup = jsonDecode(jsonBackup); - backup['path'] = derivationPath; + final mapBackup = backup.toMap(); + mapBackup['path'] = derivationPath; - return ((key: HEX.encode(derived), file: jsonEncode(backup)), null); + final backupWithPath = BullBackup.fromMap(mapBackup); + + return ((key: HEX.encode(derived), backup: backupWithPath), null); } catch (e) { return (null, Err('Encryption failed: $e')); } @@ -49,19 +51,21 @@ abstract class IRecoverbullManager { /// Decrypts an encrypted backup using the provided key Future<(List?, Err?)> restoreEncryptedBackup({ - required String backup, + required BullBackup backup, required List backupKey, }) async { try { - final plaintext = recoverbull.BackupService.restoreBackup( + final plaintext = BackupService.restoreBackup( backup: backup, backupKey: backupKey, ); final decodedJson = jsonDecode(plaintext) as List; final backups = decodedJson - .map((item) => - WalletSensitiveData.fromJson(item as Map)) + .map( + (item) => + WalletSensitiveData.fromJson(item as Map), + ) .toList(); return (backups, null); @@ -107,13 +111,19 @@ abstract class IRecoverbullManager { // Abstract methods to be implemented by concrete classes Future<(String?, Err?)> saveEncryptedBackup({ - required String backup, + required BullBackup backup, String backupFolder = defaultBackupPath, }); - Future<(Map?, Err?)> loadEncryptedBackup({ - required String backup, - }); + (BullBackup?, Err?) loadEncryptedBackup({required String file}) { + try { + final backup = BullBackup.fromJson(file); + return (backup, null); + } catch (e) { + debugPrint('Failed to decode backup: $e'); + return (null, Err('Failed to decode backup')); + } + } Future<(String?, Err?)> removeEncryptedBackup({required String path}); } diff --git a/lib/_pkg/recoverbull/google_drive.dart b/lib/_pkg/recoverbull/google_drive.dart index 4ad1e0540..c05b58f0b 100644 --- a/lib/_pkg/recoverbull/google_drive.dart +++ b/lib/_pkg/recoverbull/google_drive.dart @@ -8,6 +8,7 @@ import 'package:extension_google_sign_in_as_googleapis_auth/extension_google_sig import 'package:flutter/foundation.dart'; import 'package:google_sign_in/google_sign_in.dart'; import 'package:googleapis/drive/v3.dart'; +import 'package:recoverbull/recoverbull.dart'; class GoogleDriveBackupManager extends IRecoverbullManager { static final _google = GoogleSignIn( @@ -90,31 +91,26 @@ class GoogleDriveBackupManager extends IRecoverbullManager { @override Future<(String?, Err?)> saveEncryptedBackup({ - required String backup, + required BullBackup backup, String backupFolder = defaultBackupPath, }) async { return _withConnection((api) async { try { - final data = jsonDecode(backup) as Map; - final encryptedData = data['encrypted'] as String; - final decodedEncrypted = - jsonDecode(encryptedData) as Map; - final backupId = decodedEncrypted['id']?.toString(); - - if (backupId == null) return (null, Err('Invalid backup data')); - final filename = - '${DateTime.now().millisecondsSinceEpoch}_$backupId.json'; + '${DateTime.now().millisecondsSinceEpoch}_${backup.id}.json'; + final file = File() ..name = filename ..mimeType = 'application/json' ..parents = ['appDataFolder']; + final jsonBackup = backup.toJson(); + await api.files.create( file, uploadMedia: Media( - Stream.value(utf8.encode(backup)), - backup.length, + Stream.value(utf8.encode(jsonBackup)), + jsonBackup.length, ), ); @@ -125,19 +121,6 @@ class GoogleDriveBackupManager extends IRecoverbullManager { }); } - @override - Future<(Map?, Err?)> loadEncryptedBackup({ - required String backup, - }) async { - try { - final decodeEncryptedFile = jsonDecode(backup) as Map; - return (decodeEncryptedFile, null); - } catch (e) { - debugPrint('Failed to decode backup: $e'); - return (null, Err('Failed to decode backup')); - } - } - Future<(List?, Err?)> loadAllEncryptedBackupFiles() async { return _withConnection((api) async { try { diff --git a/lib/_pkg/recoverbull/local.dart b/lib/_pkg/recoverbull/local.dart index 4013aba8f..5f31d8412 100644 --- a/lib/_pkg/recoverbull/local.dart +++ b/lib/_pkg/recoverbull/local.dart @@ -1,4 +1,3 @@ -import 'dart:convert'; import 'dart:io'; import 'package:bb_mobile/_pkg/consts/configs.dart'; @@ -6,6 +5,7 @@ import 'package:bb_mobile/_pkg/error.dart'; import 'package:bb_mobile/_pkg/file_storage.dart'; import 'package:bb_mobile/_pkg/recoverbull/_interface.dart'; import 'package:bb_mobile/locator.dart'; +import 'package:recoverbull/recoverbull.dart'; class FileSystemBackupManager extends IRecoverbullManager { final FileStorage fileStorage = locator(); @@ -27,32 +27,17 @@ class FileSystemBackupManager extends IRecoverbullManager { } } - /// Reads the encrypted backup from the specified file. - /// Returns a map containing the backup data and the backup ID, or an error message. - @override - Future<(Map?, Err?)> loadEncryptedBackup({ - required String backup, - }) async { - try { - final decodeEncryptedFile = jsonDecode(backup) as Map; - return (decodeEncryptedFile, null); - } catch (e) { - return (null, Err('Failed to read encrypted backup: $e')); - } - } - /// Writes the encrypted backup with backupId, to a storage medium. /// Returns the path to the written backup or an error message. @override Future<(String?, Err?)> saveEncryptedBackup({ - required String backup, + required BullBackup backup, String backupFolder = defaultBackupPath, }) async { try { - final backupId = jsonDecode(backup)['id'] as String; final now = DateTime.now(); final formattedDate = now.millisecondsSinceEpoch; - final filename = '${formattedDate}_$backupId.json'; + final filename = '${formattedDate}_${backup.id}.json'; final (appDir, errDir) = await fileStorage.getAppDirectory(); if (errDir != null) { @@ -62,7 +47,7 @@ class FileSystemBackupManager extends IRecoverbullManager { final backupDir = await Directory(backupFolder).create(recursive: true); final file = File('${backupDir.path}/$filename'); - final (f, errSave) = await fileStorage.saveToFile(file, backup); + final (f, errSave) = await fileStorage.saveToFile(file, backup.toJson()); if (errSave != null) return (null, Err(errSave.message)); return (file.path, null); diff --git a/lib/recoverbull/bloc/backup_settings_cubit.dart b/lib/recoverbull/bloc/backup_settings_cubit.dart index 6250f10cc..374e6cf0e 100644 --- a/lib/recoverbull/bloc/backup_settings_cubit.dart +++ b/lib/recoverbull/bloc/backup_settings_cubit.dart @@ -23,6 +23,7 @@ import 'package:bb_mobile/recoverbull/bloc/backup_settings_state.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:hex/hex.dart'; +import 'package:recoverbull/recoverbull.dart'; BackupSettingsCubit createBackupSettingsCubit({String? walletId}) { final appWalletsRepo = locator(); @@ -205,19 +206,18 @@ class BackupSettingsCubit extends Cubit { return; } final (loadedBackup, err) = - await _fileSystemBackupManager.loadEncryptedBackup(backup: fileContent); + _fileSystemBackupManager.loadEncryptedBackup(file: fileContent); if (loadedBackup != null) { - loadedBackup.addAll({'source': 'fs'}); emit( state.copyWith( loadingBackups: false, - latestRecoveredBackup: loadedBackup, + latestRecoveredBackup: loadedBackup.toMap(), lastBackupAttempt: DateTime.now(), ), ); return; - } else if ((err != null) || loadedBackup?["id"] == null) { - debugPrint('Error loading backups: ${err?.message}'); + } else if (err != null) { + debugPrint('Error loading backups: ${err.message}'); emit( state.copyWith( loadingBackups: false, @@ -278,12 +278,12 @@ class BackupSettingsCubit extends Cubit { return; } - final (loadedBackup, err) = - await _googleDriveBackupManager.loadEncryptedBackup( - backup: utf8.decode(loadedBackupMetaData), + final (backup, err) = _googleDriveBackupManager.loadEncryptedBackup( + file: utf8.decode(loadedBackupMetaData), ); - if (loadedBackup != null) { - loadedBackup.addAll({ + if (backup != null) { + final backupMap = backup.toMap(); + backupMap.addAll({ 'source': 'drive', 'filename': latestBackup.name, }); @@ -291,13 +291,13 @@ class BackupSettingsCubit extends Cubit { emit( state.copyWith( loadingBackups: false, - latestRecoveredBackup: loadedBackup, + latestRecoveredBackup: backupMap, lastBackupAttempt: DateTime.now(), ), ); return; - } else if ((err != null) || loadedBackup?["id"] == null) { - debugPrint('Error loading backups: ${err?.message}'); + } else if (err != null) { + debugPrint('Error loading backups: ${err.message}'); _handleLoadError("Corrupted backup file"); return; } @@ -345,20 +345,20 @@ class BackupSettingsCubit extends Cubit { ); if (backupKey.isEmpty) { - _handleLoadError('WalletSensitiveData key is missing'); + _handleLoadError('Backup key is missing'); return; } - final backupId = jsonDecode(encrypted)['id'] as String?; - - if (backupId == null) { + if (!BullBackup.isValid(encrypted)) { _handleLoadError('Invalid backup format'); return; } + final backup = BullBackup.fromJson(encrypted); + final (backups, decryptErr) = await _fileSystemBackupManager.restoreEncryptedBackup( - backup: encrypted, + backup: backup, backupKey: HEX.decode(backupKey), ); @@ -449,12 +449,15 @@ class BackupSettingsCubit extends Cubit { return; } - final (backup, err) = await _createBackup(backups); - if (err != null || backup == null) { + final (result, err) = await _createBackup(backups); + if (err != null || result == null) { _handleSaveError(err?.message ?? 'Encryption failed'); return; } + final backup = result.backup; + final backupKey = result.key; + final (savePath, pickErr) = await _filePicker?.getDirectoryPath() ?? (null, Err('File picker not initialized')); if (pickErr != null) { @@ -469,7 +472,7 @@ class BackupSettingsCubit extends Cubit { final (filePath, saveErr) = await _fileSystemBackupManager.saveEncryptedBackup( - backup: backup.file, + backup: backup, backupFolder: savePath, ); @@ -478,20 +481,12 @@ class BackupSettingsCubit extends Cubit { return; } - final theBackup = json.decode(backup.file); - final backupId = theBackup['id'] as String?; - final backupSalt = theBackup['salt'] as String?; - if (backupId == null || backupSalt == null) { - _handleSaveError('Failed to extract backup metadata'); - return; - } - _emitSafe( state.copyWith( - backupId: backupId, - backupKey: backup.key, + backupId: backup.id, + backupKey: backupKey, backupFolderPath: filePath ?? '', - backupSalt: backupSalt, + backupSalt: backup.salt, savingBackups: false, lastBackupAttempt: DateTime.now(), ), @@ -523,35 +518,35 @@ class BackupSettingsCubit extends Cubit { return; } - final (backup, encryptErr) = await _createBackup(backups); - if (encryptErr != null || backup == null) { + final (result, encryptErr) = await _createBackup(backups); + if (encryptErr != null || result == null) { _handleSaveError(encryptErr?.message ?? 'Encryption failed'); return; } - final (filePath, saveErr) = await _googleDriveBackupManager - .saveEncryptedBackup(backup: backup.file); + final backupKey = result.key; + final backup = result.backup; + + final (filePath, saveErr) = + await _googleDriveBackupManager.saveEncryptedBackup(backup: backup); if (saveErr != null) { _handleSaveError('Failed to save to Google Drive: ${saveErr.message}'); return; } - final theBackup = json.decode(backup.file); - final backupId = theBackup['id'] as String?; - final backupSalt = theBackup['salt'] as String?; final filename = filePath?.split('/').last; - if (backupId == null || filename == null || backupSalt == null) { - _handleSaveError('Failed to extract backup information'); + if (filename == null) { + _handleSaveError('filename is null'); return; } _emitSafe( state.copyWith( - backupId: backupId, - backupKey: backup.key, + backupId: backup.id, + backupKey: backupKey, backupFolderPath: filename, - backupSalt: backupSalt, + backupSalt: backup.salt, savingBackups: false, lastBackupAttempt: DateTime.now(), ), @@ -848,7 +843,7 @@ class BackupSettingsCubit extends Cubit { if (!isClosed) emit(newState); } - Future<(({String key, String file})?, Err?)> _createBackup( + Future<(({String key, BullBackup backup})?, Err?)> _createBackup( List wallets, ) async { try { diff --git a/lib/recoverbull/encrypted_vault_backup.dart b/lib/recoverbull/encrypted_vault_backup.dart index fc9905c22..0dc68f5ae 100644 --- a/lib/recoverbull/encrypted_vault_backup.dart +++ b/lib/recoverbull/encrypted_vault_backup.dart @@ -572,7 +572,7 @@ class _RecoveredBackupInfoPageState extends State { ), TextSpan( text: - ' ${DateFormat('MMM dd, yyyy HH:mm:ss').format(DateTime.fromMillisecondsSinceEpoch(recoveryFile['createdAt'] as int).toLocal())}', + ' ${DateFormat('MMM dd, yyyy HH:mm:ss').format(DateTime.fromMillisecondsSinceEpoch(recoveryFile['created_at'] as int).toLocal())}', style: context.font.bodyMedium! .copyWith(fontWeight: FontWeight.bold), ), From bccb65646240f2b9a3a980d3777fb64bc79567c5 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 24 Feb 2025 10:51:28 -0500 Subject: [PATCH 290/401] refactor: reorganize imports --- lib/routes.dart | 4 ++-- lib/wallet_settings/wallet_settings_page.dart | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/routes.dart b/lib/routes.dart index fa548fdd9..e522bc93d 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -15,6 +15,8 @@ import 'package:bb_mobile/receive/receive_page.dart'; import 'package:bb_mobile/recoverbull/backup_settings.dart'; import 'package:bb_mobile/recoverbull/encrypted_vault_backup.dart'; import 'package:bb_mobile/recoverbull/keychain_page.dart'; +import 'package:bb_mobile/recoverbull/physical_backup.dart'; +import 'package:bb_mobile/recoverbull/test_backup.dart'; import 'package:bb_mobile/send/bloc/send_cubit.dart'; import 'package:bb_mobile/send/send_page.dart'; import 'package:bb_mobile/settings/application_settings_page.dart'; @@ -35,8 +37,6 @@ import 'package:bb_mobile/wallet/details.dart'; import 'package:bb_mobile/wallet/information_page.dart'; import 'package:bb_mobile/wallet/wallet_page.dart'; import 'package:bb_mobile/wallet_settings/accounting.dart'; -import 'package:bb_mobile/wallet_settings/physical_backup.dart'; -import 'package:bb_mobile/wallet_settings/test_backup.dart'; import 'package:bb_mobile/wallet_settings/wallet_settings_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; diff --git a/lib/wallet_settings/wallet_settings_page.dart b/lib/wallet_settings/wallet_settings_page.dart index ceebfdd2f..5eb18b495 100644 --- a/lib/wallet_settings/wallet_settings_page.dart +++ b/lib/wallet_settings/wallet_settings_page.dart @@ -8,11 +8,11 @@ import 'package:bb_mobile/_ui/components/text.dart'; import 'package:bb_mobile/_ui/components/text_input.dart'; import 'package:bb_mobile/_ui/headers.dart'; import 'package:bb_mobile/currency/bloc/currency_cubit.dart'; +import 'package:bb_mobile/wallet_settings/listeners.dart'; import 'package:bb_mobile/wallet/bloc/wallet_bloc.dart'; import 'package:bb_mobile/wallet_settings/addresses.dart'; import 'package:bb_mobile/wallet_settings/bloc/wallet_settings_cubit.dart'; import 'package:bb_mobile/wallet_settings/bloc/wallet_settings_state.dart'; -import 'package:bb_mobile/wallet_settings/listeners.dart'; import 'package:extra_alignments/extra_alignments.dart'; import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; From 22694f0f530b52bec4f65c7dcdd7a0ba71bc104a Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 24 Feb 2025 10:51:48 -0500 Subject: [PATCH 291/401] refactor: move TestBackupListener to recoverbull --- lib/recoverbull/listeners.dart | 43 +++++++++++++++++++++++++ lib/wallet_settings/listeners.dart | 51 ------------------------------ 2 files changed, 43 insertions(+), 51 deletions(-) create mode 100644 lib/recoverbull/listeners.dart diff --git a/lib/recoverbull/listeners.dart b/lib/recoverbull/listeners.dart new file mode 100644 index 000000000..db4144215 --- /dev/null +++ b/lib/recoverbull/listeners.dart @@ -0,0 +1,43 @@ +import 'package:bb_mobile/_repository/app_wallets_repository.dart'; +import 'package:bb_mobile/_repository/wallet_service.dart'; +import 'package:bb_mobile/recoverbull/bloc/backup_settings_cubit.dart'; +import 'package:bb_mobile/recoverbull/bloc/backup_settings_state.dart'; +import 'package:bb_mobile/wallet/bloc/wallet_bloc.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class TestBackupListener extends StatelessWidget { + const TestBackupListener({super.key, required this.child}); + + final Widget child; + @override + Widget build(BuildContext context) { + return BlocListener( + listenWhen: (previous, current) => + previous.backupTested != current.backupTested, + listener: (context, state) { + if (!state.backupTested) return; + + final wallet = context.read().state.wallet; + + final walletService = context + .read() + .findWalletServiceWithSameFngr(wallet); + if (walletService == null) return; + + final w = walletService.wallet.copyWith( + physicalBackupTested: true, + lastPhysicalBackupTested: DateTime.now(), + ); + + walletService.updateWallet( + w, + updateTypes: [ + UpdateWalletTypes.settings, + ], + ); + }, + child: child, + ); + } +} diff --git a/lib/wallet_settings/listeners.dart b/lib/wallet_settings/listeners.dart index 9d392f1fc..e248f4309 100644 --- a/lib/wallet_settings/listeners.dart +++ b/lib/wallet_settings/listeners.dart @@ -1,10 +1,5 @@ -import 'package:bb_mobile/_repository/app_wallets_repository.dart'; -import 'package:bb_mobile/_repository/wallet_service.dart'; import 'package:bb_mobile/home/bloc/home_bloc.dart'; import 'package:bb_mobile/home/bloc/home_event.dart'; -import 'package:bb_mobile/recoverbull/bloc/backup_settings_cubit.dart'; -import 'package:bb_mobile/recoverbull/bloc/backup_settings_state.dart'; -import 'package:bb_mobile/wallet/bloc/wallet_bloc.dart'; import 'package:bb_mobile/wallet_settings/bloc/wallet_settings_cubit.dart'; import 'package:bb_mobile/wallet_settings/bloc/wallet_settings_state.dart'; import 'package:flutter/material.dart'; @@ -48,49 +43,3 @@ class WalletSettingsListeners extends StatelessWidget { ); } } - -class TestBackupListener extends StatelessWidget { - const TestBackupListener({super.key, required this.child}); - - final Widget child; - @override - Widget build(BuildContext context) { - return BlocListener( - listenWhen: (previous, current) => - previous.backupTested != current.backupTested, - listener: (context, state) { - if (!state.backupTested) return; - - final wallet = context.read().state.wallet; - - final walletService = context - .read() - .findWalletServiceWithSameFngr(wallet); - // .findWalletBlocWithSameFngr(state.wallet); - if (walletService == null) return; - - final w = walletService.wallet.copyWith( - physicalBackupTested: true, - lastPhysicalBackupTested: DateTime.now(), - ); - - walletService.updateWallet( - w, - updateTypes: [ - UpdateWalletTypes.settings, - ], - ); - - // walletService.add( - // UpdateWallet( - // w, - // updateTypes: [ - // UpdateWalletTypes.settings, - // ], - // ), - // ); - }, - child: child, - ); - } -} From 9fff7f95a9aab2f3f7e7ee769416395dc4f420f6 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 24 Feb 2025 10:52:35 -0500 Subject: [PATCH 292/401] refactor: reorganize imports --- lib/wallet_settings/wallet_settings_page.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/wallet_settings/wallet_settings_page.dart b/lib/wallet_settings/wallet_settings_page.dart index 5eb18b495..ceebfdd2f 100644 --- a/lib/wallet_settings/wallet_settings_page.dart +++ b/lib/wallet_settings/wallet_settings_page.dart @@ -8,11 +8,11 @@ import 'package:bb_mobile/_ui/components/text.dart'; import 'package:bb_mobile/_ui/components/text_input.dart'; import 'package:bb_mobile/_ui/headers.dart'; import 'package:bb_mobile/currency/bloc/currency_cubit.dart'; -import 'package:bb_mobile/wallet_settings/listeners.dart'; import 'package:bb_mobile/wallet/bloc/wallet_bloc.dart'; import 'package:bb_mobile/wallet_settings/addresses.dart'; import 'package:bb_mobile/wallet_settings/bloc/wallet_settings_cubit.dart'; import 'package:bb_mobile/wallet_settings/bloc/wallet_settings_state.dart'; +import 'package:bb_mobile/wallet_settings/listeners.dart'; import 'package:extra_alignments/extra_alignments.dart'; import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; From f87ae87ca87017248d63d993e1579cedfb7685d5 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 24 Feb 2025 10:54:33 -0500 Subject: [PATCH 293/401] refactor: moved backup classes --- lib/{wallet_settings => recoverbull}/physical_backup.dart | 0 lib/{wallet_settings => recoverbull}/test_backup.dart | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename lib/{wallet_settings => recoverbull}/physical_backup.dart (100%) rename lib/{wallet_settings => recoverbull}/test_backup.dart (99%) diff --git a/lib/wallet_settings/physical_backup.dart b/lib/recoverbull/physical_backup.dart similarity index 100% rename from lib/wallet_settings/physical_backup.dart rename to lib/recoverbull/physical_backup.dart diff --git a/lib/wallet_settings/test_backup.dart b/lib/recoverbull/test_backup.dart similarity index 99% rename from lib/wallet_settings/test_backup.dart rename to lib/recoverbull/test_backup.dart index 72e73e4ce..7d26188ee 100644 --- a/lib/wallet_settings/test_backup.dart +++ b/lib/recoverbull/test_backup.dart @@ -3,9 +3,9 @@ import 'package:bb_mobile/_ui/components/button.dart'; import 'package:bb_mobile/_ui/components/text.dart'; import 'package:bb_mobile/_ui/components/text_input.dart'; import 'package:bb_mobile/recoverbull/bloc/backup_settings_cubit.dart'; +import 'package:bb_mobile/recoverbull/listeners.dart'; import 'package:bb_mobile/styles.dart'; import 'package:bb_mobile/wallet/bloc/wallet_bloc.dart'; -import 'package:bb_mobile/wallet_settings/listeners.dart'; import 'package:extra_alignments/extra_alignments.dart'; import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; From 56da4ce330a27e5c566af1a8e1d5dab5ebd7f503 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Tue, 25 Feb 2025 16:06:13 -0500 Subject: [PATCH 294/401] refactor: add KeychainCubit to main app initialization --- lib/main.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/main.dart b/lib/main.dart index cbcaefa2d..9cbb9ab8b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -14,6 +14,7 @@ import 'package:bb_mobile/locator.dart'; import 'package:bb_mobile/network/bloc/network_bloc.dart'; import 'package:bb_mobile/network/listeners.dart'; import 'package:bb_mobile/network_fees/bloc/networkfees_cubit.dart'; +import 'package:bb_mobile/recoverbull/bloc/keychain_cubit.dart'; import 'package:bb_mobile/routes.dart'; import 'package:bb_mobile/settings/bloc/lighting_cubit.dart'; import 'package:bb_mobile/settings/bloc/settings_cubit.dart'; @@ -64,6 +65,7 @@ class BullBitcoinWalletApp extends StatelessWidget { BlocProvider.value(value: locator()), BlocProvider.value(value: locator()), BlocProvider.value(value: locator()), + BlocProvider.value(value: KeychainCubit()), BlocProvider.value(value: locator()), BlocProvider.value(value: locator()), BlocProvider.value(value: locator()), From 4ec38dfb04c20cf8bfd57405defbb35ebd2868cf Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Tue, 25 Feb 2025 16:06:25 -0500 Subject: [PATCH 295/401] refactor: integrate KeychainCubit for key server status in home page --- lib/home/home_page.dart | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/lib/home/home_page.dart b/lib/home/home_page.dart index d26d4007f..f91646cc4 100644 --- a/lib/home/home_page.dart +++ b/lib/home/home_page.dart @@ -22,6 +22,7 @@ import 'package:bb_mobile/home/bloc/home_event.dart'; import 'package:bb_mobile/home/transactions.dart'; import 'package:bb_mobile/locator.dart'; import 'package:bb_mobile/network/bloc/network_bloc.dart'; +import 'package:bb_mobile/recoverbull/bloc/keychain_cubit.dart'; import 'package:bb_mobile/settings/bloc/lighting_cubit.dart'; import 'package:bb_mobile/styles.dart'; import 'package:bb_mobile/wallet/bloc/wallet_bloc.dart'; @@ -132,9 +133,11 @@ class _ScreenState extends State<_Screen> { return widget; } - final warningsSize = - context.select((HomeBloc x) => x.state.homeWarnings(network)).length * - 40.0; + final keyServerUp = context.watch().state.keyServerUp; + final warningBannerCount = + context.select((HomeBloc x) => x.state.homeWarnings(network)).length + + (!keyServerUp ? 1 : 0); + final warningsSize = warningBannerCount * 40.0; final h = _calculateHeight(wallets.length); @@ -1149,6 +1152,7 @@ class HomeWarnings extends StatelessWidget { final network = context.select((NetworkBloc _) => _.state.getBBNetwork()); final warnings = context.select((HomeBloc _) => _.state.homeWarnings(network)); + final keyServerUp = context.watch().state.keyServerUp; return Column( children: [ for (final w in warnings) @@ -1161,6 +1165,11 @@ class HomeWarnings extends StatelessWidget { }, info: w.info, ), + if (!keyServerUp) + WarningBanner( + onTap: () {}, + info: 'Key server is down. Backup key is unavailable.', + ), ], ); } From 17b341b7836dfca4d1b7e135fa845e60fba5b251 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Tue, 25 Feb 2025 16:09:24 -0500 Subject: [PATCH 296/401] refactor: integrate KeychainCubit for key server status in backup flow --- lib/recoverbull/encrypted_vault_backup.dart | 40 +++++++++++++++------ 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/lib/recoverbull/encrypted_vault_backup.dart b/lib/recoverbull/encrypted_vault_backup.dart index 0dc68f5ae..4eb965f76 100644 --- a/lib/recoverbull/encrypted_vault_backup.dart +++ b/lib/recoverbull/encrypted_vault_backup.dart @@ -5,6 +5,7 @@ import 'package:bb_mobile/_ui/components/text.dart'; import 'package:bb_mobile/_ui/toast.dart'; import 'package:bb_mobile/recoverbull/bloc/backup_settings_cubit.dart'; import 'package:bb_mobile/recoverbull/bloc/backup_settings_state.dart'; +import 'package:bb_mobile/recoverbull/bloc/keychain_cubit.dart'; import 'package:bb_mobile/recoverbull/bloc/keychain_state.dart'; import 'package:bb_mobile/styles.dart'; import 'package:flutter/cupertino.dart'; @@ -41,16 +42,19 @@ class EncryptedVaultBackupPage extends StatefulWidget { class _EncryptedVaultBackupPageState extends State { late final BackupSettingsCubit _cubit; + late final KeychainCubit _keychainCubit; @override void initState() { super.initState(); _cubit = createBackupSettingsCubit(walletId: widget.wallet); + _keychainCubit = KeychainCubit(); } @override void dispose() { _cubit.close(); + _keychainCubit.close(); super.dispose(); } @@ -58,20 +62,35 @@ class _EncryptedVaultBackupPageState extends State { BuildContext context, BackupProvider provider, ) async { - switch (provider) { - case BackupProvider.googleDrive: - await _cubit.saveGoogleDriveBackup(); - case BackupProvider.iCloud: - debugPrint('iCloud backup'); - case BackupProvider.custom: - _cubit.saveFileSystemBackup(); + await _keychainCubit.keyServerStatus(); + + final keyServerUp = _keychainCubit.state.keyServerUp; + + if (keyServerUp) { + switch (provider) { + case BackupProvider.googleDrive: + await _cubit.saveGoogleDriveBackup(); + case BackupProvider.iCloud: + debugPrint('iCloud backup'); + case BackupProvider.custom: + _cubit.saveFileSystemBackup(); + } + } else { + ScaffoldMessenger.of(context).showSnackBar( + context.showToast( + 'Key server is down. Please try backing up again later', + ), + ); } } @override Widget build(BuildContext context) { - return BlocProvider.value( - value: _cubit, + return MultiBlocProvider( + providers: [ + BlocProvider.value(value: _cubit), + BlocProvider.value(value: _keychainCubit), + ], child: BlocConsumer( listenWhen: (previous, current) => previous.errorSavingBackups != current.errorSavingBackups || @@ -135,7 +154,8 @@ class _EncryptedVaultBackupPageState extends State { title: provider.title, description: provider.description, icon: Icon(provider.icon, size: 40), - onTap: () => _handleBackup(context, provider), + onTap: () async => + await _handleBackup(context, provider), ), ), ), From 2ea086f33b2496c1761121c4d0a5a63adfc88b84 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Tue, 25 Feb 2025 16:10:17 -0500 Subject: [PATCH 297/401] refactor(EncryptedVaultRecoverPage): rename BackupSettingsCubit variable --- lib/recoverbull/encrypted_vault_backup.dart | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/recoverbull/encrypted_vault_backup.dart b/lib/recoverbull/encrypted_vault_backup.dart index 4eb965f76..35ad28ebb 100644 --- a/lib/recoverbull/encrypted_vault_backup.dart +++ b/lib/recoverbull/encrypted_vault_backup.dart @@ -299,17 +299,17 @@ class EncryptedVaultRecoverPage extends StatefulWidget { } class _EncryptedVaultRecoverPageState extends State { - late final BackupSettingsCubit _cubit; + late final BackupSettingsCubit _backupSettingsCubit; @override void initState() { super.initState(); - _cubit = createBackupSettingsCubit(walletId: widget.wallet); + _backupSettingsCubit = createBackupSettingsCubit(walletId: widget.wallet); } @override void dispose() { - _cubit.close(); + _backupSettingsCubit.close(); super.dispose(); } @@ -319,11 +319,11 @@ class _EncryptedVaultRecoverPageState extends State { ) async { switch (provider) { case BackupProvider.googleDrive: - await _cubit.fetchGoogleDriveBackup(); + await _backupSettingsCubit.fetchGoogleDriveBackup(); case BackupProvider.iCloud: debugPrint('iCloud backup'); case BackupProvider.custom: - _cubit.fetchFsBackup(); + _backupSettingsCubit.fetchFsBackup(); } } @@ -353,7 +353,7 @@ class _EncryptedVaultRecoverPageState extends State { @override Widget build(BuildContext context) { return BlocProvider.value( - value: _cubit, + value: _backupSettingsCubit, child: BlocConsumer( listenWhen: (previous, current) => previous.errorLoadingBackups != current.errorLoadingBackups || @@ -363,7 +363,7 @@ class _EncryptedVaultRecoverPageState extends State { ScaffoldMessenger.of(context).showSnackBar( context.showToast(state.errorLoadingBackups), ); - _cubit.clearError(); + _backupSettingsCubit.clearError(); return; } if (state.latestRecoveredBackup.isNotEmpty) { @@ -371,7 +371,7 @@ class _EncryptedVaultRecoverPageState extends State { '/wallet-settings/backup-settings/recover-options/encrypted/info', extra: state.latestRecoveredBackup, ); - _cubit.clearError(); + _backupSettingsCubit.clearError(); } }, builder: (context, state) { From 9041887d53608878ed6f4aaa7a09affa8c8f6dce Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Tue, 25 Feb 2025 16:12:12 -0500 Subject: [PATCH 298/401] refactor(KeychainState): add keyServerUp state and update validation logic for password and backup key --- lib/recoverbull/bloc/keychain_state.dart | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/lib/recoverbull/bloc/keychain_state.dart b/lib/recoverbull/bloc/keychain_state.dart index 7ba471fa5..29a4e839a 100644 --- a/lib/recoverbull/bloc/keychain_state.dart +++ b/lib/recoverbull/bloc/keychain_state.dart @@ -26,6 +26,7 @@ enum KeySecretState { none, saved, recovered, deleted } class KeychainState with _$KeychainState { const factory KeychainState({ @Default(false) bool loading, + @Default(true) bool keyServerUp, @Default(KeyChainPageState.enter) KeyChainPageState pageState, @Default(KeyChainInputType.pin) KeyChainInputType inputType, @Default(KeySecretState.none) KeySecretState keySecretState, @@ -45,8 +46,6 @@ class KeychainState with _$KeychainState { String displayPin() => 'x' * secret.length; String? getValidationError() { - if (secret.isEmpty) return null; - if (inputType == KeyChainInputType.pin) { const pinMin = KeychainCubit.pinMin; const pinMax = KeychainCubit.pinMax; @@ -58,16 +57,22 @@ class KeychainState with _$KeychainState { } } - return validateSecret(secret) - ? 'The password is among the top 1000 most common' - : null; + if (validateSecret(secret) && + (inputType == KeyChainInputType.password || + inputType == KeyChainInputType.backupKey)) { + return 'The password is among the top 1000 most common'; + } + + return null; } bool get isValid => getValidationError() == null; - bool get showButton => isValid; + bool get hasError => error.isNotEmpty; bool get isRecovering => pageState == KeyChainPageState.recovery; - bool get canRecoverKey => backupId.isNotEmpty && isValid && !loading; - + bool get canStoreKey => isValid && keyServerUp && !loading; + bool get canRecoverKey => backupId.isNotEmpty && keyServerUp && !loading; + bool get canRecoverWithBckupKey => backupId.isNotEmpty && !loading; + bool get canDeleteKey => backupId.isNotEmpty && keyServerUp && !loading; bool validateSecret(String secret) => commonPasswordsTop1000.contains(secret); } From 6c88330b9b718f62f8d38f11aed3916e0b3d7cc8 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Tue, 25 Feb 2025 16:12:55 -0500 Subject: [PATCH 299/401] refactor(KeychainCubit): initialize key service and add key server status check --- lib/recoverbull/bloc/keychain_cubit.dart | 40 ++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/lib/recoverbull/bloc/keychain_cubit.dart b/lib/recoverbull/bloc/keychain_cubit.dart index 11c9363cc..44d5b7bf7 100644 --- a/lib/recoverbull/bloc/keychain_cubit.dart +++ b/lib/recoverbull/bloc/keychain_cubit.dart @@ -10,18 +10,54 @@ class KeychainCubit extends Cubit { static const pinMax = 8; KeychainCubit() : super(const KeychainState()) { + _initialize(); + } + + late final KeyService _keyService; + + void _initialize() { shuffleAndEmit(); if (keyServerUrl.isEmpty) { - emit(state.copyWith(error: 'keychain api is not set')); + emit( + state.copyWith( + error: 'keychain api is not set', + keyServerUp: false, + ), + ); return; } + _keyService = KeyService( keyServer: Uri.parse(keyServerUrl), keyServerPublicKey: keyServerPublicKey, ); + + // Initial status check + keyServerStatus(); } - late final KeyService _keyService; + Future keyServerStatus() async { + if (!isClosed) { + try { + final info = await _keyService.serverInfo(); + final isUp = info.cooldown <= 1; + + if (isUp != state.keyServerUp) { + emit(state.copyWith(keyServerUp: isUp)); + } + } catch (e) { + debugPrint('Server status check failed: $e'); + if (state.keyServerUp) { + emit(state.copyWith(keyServerUp: false)); + } + } + } + } + + Future _ensureServerStatus() async { + await keyServerStatus(); + return state.keyServerUp; + } void backspacePressed() { if (state.secret.isEmpty) return; From e63bf482b6693e57b063912361e4ccfa442ece22 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Tue, 25 Feb 2025 16:13:43 -0500 Subject: [PATCH 300/401] refactor(KeychainCubit): streamline server status checks and enhance key recovery conditions --- lib/recoverbull/bloc/keychain_cubit.dart | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/recoverbull/bloc/keychain_cubit.dart b/lib/recoverbull/bloc/keychain_cubit.dart index 44d5b7bf7..59474ef13 100644 --- a/lib/recoverbull/bloc/keychain_cubit.dart +++ b/lib/recoverbull/bloc/keychain_cubit.dart @@ -134,9 +134,9 @@ class KeychainCubit extends Cubit { } } - void confirmPressed() { - if (!state.showButton) return; - + Future confirmPressed() async { + if (!await _ensureServerStatus()) return; + if (!state.canStoreKey) return; if (state.pageState == KeyChainPageState.enter) { emit( state.copyWith( @@ -164,6 +164,8 @@ class KeychainCubit extends Cubit { } Future deleteBackupKey() async { + if (!await _ensureServerStatus()) return; + if (!state.canDeleteKey) return; try { emit(state.copyWith(loading: true, error: '')); final isServerReady = await serverInfo(); @@ -197,6 +199,7 @@ class KeychainCubit extends Cubit { } Future secureKey() async { + if (!await _ensureServerStatus()) return; try { final isServerReady = await serverInfo(); if (!isServerReady) return; From 391b6391ee2fa8e7d9ceb1b2723221d3635ee929 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Tue, 25 Feb 2025 16:13:51 -0500 Subject: [PATCH 301/401] refactor(KeychainCubit): prevent duplicate backupId state updates --- lib/recoverbull/bloc/keychain_cubit.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/recoverbull/bloc/keychain_cubit.dart b/lib/recoverbull/bloc/keychain_cubit.dart index 59474ef13..bd828dd53 100644 --- a/lib/recoverbull/bloc/keychain_cubit.dart +++ b/lib/recoverbull/bloc/keychain_cubit.dart @@ -225,6 +225,7 @@ class KeychainCubit extends Cubit { } void setBackupId(String id) { + if (id == state.backupId) return; // Avoid duplicate state emit(state.copyWith(backupId: id)); } From 0ae1a8e7b34ee38872cf2b9b1165298422999e31 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Tue, 25 Feb 2025 16:14:22 -0500 Subject: [PATCH 302/401] refactor: code cleanup --- lib/recoverbull/bloc/keychain_cubit.dart | 5 ----- 1 file changed, 5 deletions(-) diff --git a/lib/recoverbull/bloc/keychain_cubit.dart b/lib/recoverbull/bloc/keychain_cubit.dart index bd828dd53..8683769a7 100644 --- a/lib/recoverbull/bloc/keychain_cubit.dart +++ b/lib/recoverbull/bloc/keychain_cubit.dart @@ -168,8 +168,6 @@ class KeychainCubit extends Cubit { if (!state.canDeleteKey) return; try { emit(state.copyWith(loading: true, error: '')); - final isServerReady = await serverInfo(); - if (!isServerReady) return; await _keyService.trashBackupKey( backupId: state.backupId, @@ -201,9 +199,6 @@ class KeychainCubit extends Cubit { Future secureKey() async { if (!await _ensureServerStatus()) return; try { - final isServerReady = await serverInfo(); - if (!isServerReady) return; - await _keyService.storeBackupKey( backupId: state.backupId, password: state.tempSecret, From be50fe7e4106b97f7af91d0a04bfbe43b65d8f0a Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Tue, 25 Feb 2025 16:15:14 -0500 Subject: [PATCH 303/401] refactor(KeychainCubit): prevent duplicate state updates for backupKey and secret --- lib/recoverbull/bloc/keychain_cubit.dart | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/lib/recoverbull/bloc/keychain_cubit.dart b/lib/recoverbull/bloc/keychain_cubit.dart index 8683769a7..7db26c1e9 100644 --- a/lib/recoverbull/bloc/keychain_cubit.dart +++ b/lib/recoverbull/bloc/keychain_cubit.dart @@ -246,16 +246,14 @@ class KeychainCubit extends Cubit { } void updateBackupKey(String value) { - emit( - state.copyWith( - backupKey: value, - error: '', - ), - ); + if (value == state.backupKey) return; // Avoid duplicate state + emit(state.copyWith(backupKey: value, error: '')); } void updateInput(String value) { if (state.inputType == KeyChainInputType.pin && value.length > 6) return; + if (value == state.secret) return; // Avoid duplicate state + emit(state.copyWith(secret: value, error: '')); } From 3f591351e5c11cb0efe05be747d46c3f18999849 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Tue, 25 Feb 2025 16:15:27 -0500 Subject: [PATCH 304/401] refactor(KeychainCubit): remove unused serverInfo method --- lib/recoverbull/bloc/keychain_cubit.dart | 28 ++---------------------- 1 file changed, 2 insertions(+), 26 deletions(-) diff --git a/lib/recoverbull/bloc/keychain_cubit.dart b/lib/recoverbull/bloc/keychain_cubit.dart index 7db26c1e9..2751b6474 100644 --- a/lib/recoverbull/bloc/keychain_cubit.dart +++ b/lib/recoverbull/bloc/keychain_cubit.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:bb_mobile/_pkg/consts/configs.dart'; import 'package:bb_mobile/recoverbull/bloc/keychain_state.dart'; import 'package:flutter/material.dart'; @@ -272,30 +274,4 @@ class KeychainCubit extends Cubit { ), ); } - - Future serverInfo() async { - emit(state.copyWith(loading: true)); - try { - final info = await _keyService.serverInfo(); - if (info.cooldown > 1) { - emit(state.copyWith(loading: false, error: 'Server is on cooldown')); - return false; - } - if (state.tempSecret.length > info.secretMaxLength || - state.secret.length > info.secretMaxLength) { - emit(state.copyWith(loading: false, error: 'Secret is too long')); - return false; - } - return true; - } catch (e) { - debugPrint('Failed to get server info: $e'); - emit( - state.copyWith( - loading: false, - error: 'Key server is not reachable! Please try again later', - ), - ); - return false; - } - } } From 97a6298f6ea6d0486699ff0861eb10464ae74d4a Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Tue, 25 Feb 2025 16:16:03 -0500 Subject: [PATCH 305/401] fix(KeychainBackupPage):remove redundant backup deletion logic --- lib/recoverbull/keychain_page.dart | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/lib/recoverbull/keychain_page.dart b/lib/recoverbull/keychain_page.dart index a2d412f09..515284322 100644 --- a/lib/recoverbull/keychain_page.dart +++ b/lib/recoverbull/keychain_page.dart @@ -40,8 +40,8 @@ class KeychainBackupPage extends StatelessWidget { BlocProvider( create: (context) => KeychainCubit() ..setChainState( - _pState, // Use the provided state directly instead of determining it - backupId ?? '', + _pState, + backupData.$1 ?? '', backupKey, backupSalt ?? '', ), @@ -118,18 +118,7 @@ class _Screen extends StatelessWidget { !state.loading && !state.hasError) { context.read().clearSensitive(); - final source = backup['source'] as String?; - final fileName = backup['filename'] as String?; - if (source != null) { - if (source == 'drive' && fileName != null) { - context - .read() - .deleteGoogleDriveBackup(fileName); - } - if (source == 'fs') { - context.read().deleteFsBackup(); - } - } + showDialog( context: context, barrierDismissible: false, From 7a2a43e3b49f3df60a9194e0aa7c7c14a8520c91 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Tue, 25 Feb 2025 16:16:28 -0500 Subject: [PATCH 306/401] refactor(RecoveryPage): simplify input type text handling --- lib/recoverbull/keychain_page.dart | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/lib/recoverbull/keychain_page.dart b/lib/recoverbull/keychain_page.dart index 515284322..23a73ee17 100644 --- a/lib/recoverbull/keychain_page.dart +++ b/lib/recoverbull/keychain_page.dart @@ -310,13 +310,13 @@ class _RecoveryPage extends StatelessWidget { children: [ const Gap(50), BBText.titleLarge( - 'Enter Recovery ${inputType == KeyChainInputType.pin ? 'PIN' : inputType == KeyChainInputType.password ? 'Password' : 'Key'}', + 'Enter Recovery ${_getInputTypeText(inputType)}', textAlign: TextAlign.center, isBold: true, ), const Gap(8), BBText.bodySmall( - 'Enter the ${inputType == KeyChainInputType.pin ? 'PIN' : inputType == KeyChainInputType.password ? 'password' : 'backup key'} you used to backup your keychain', + 'Enter the ${_getInputTypeText(inputType).toLowerCase()} you used to backup your keychain', textAlign: TextAlign.center, ), const Gap(50), @@ -331,6 +331,17 @@ class _RecoveryPage extends StatelessWidget { ), ); } + + String _getInputTypeText(KeyChainInputType type) { + switch (type) { + case KeyChainInputType.pin: + return 'PIN'; + case KeyChainInputType.password: + return 'Password'; + case KeyChainInputType.backupKey: + return 'Key'; + } + } } class _DeletePage extends StatelessWidget { From bf2d273d0916a0bf89a635c9ad7fc611f618e51f Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Tue, 25 Feb 2025 16:16:48 -0500 Subject: [PATCH 307/401] refactor(KeychainPage): optimize state management in PasswordField widget --- lib/recoverbull/keychain_page.dart | 77 ++++++++++++++++-------------- 1 file changed, 42 insertions(+), 35 deletions(-) diff --git a/lib/recoverbull/keychain_page.dart b/lib/recoverbull/keychain_page.dart index 23a73ee17..b9db08ad4 100644 --- a/lib/recoverbull/keychain_page.dart +++ b/lib/recoverbull/keychain_page.dart @@ -435,41 +435,48 @@ class _PinField extends StatelessWidget { class _PasswordField extends StatelessWidget { @override Widget build(BuildContext context) { - final state = context.select( - (KeychainCubit x) => ( - x.state.secret, - x.state.obscure, - x.state.inputType, - x.state.backupKey, - x.state.getValidationError() - ), - ); - final (secret, obscure, inputType, backupKey, error) = state; - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - BBTextInput.bigWithIcon( - value: inputType == KeyChainInputType.backupKey ? backupKey : secret, - onChanged: (value) => inputType == KeyChainInputType.backupKey - ? context.read().updateBackupKey(value) - : context.read().updateInput(value), - obscure: obscure, - hint: inputType == KeyChainInputType.backupKey - ? 'Enter your backup key' - : 'Enter your password', - rightIcon: Icon( - obscure ? Icons.visibility_off : Icons.visibility, - color: context.colour.onPrimaryContainer, - ), - onRightTap: () => context.read().clickObscure(), - ), - if (error != null) - Padding( - padding: const EdgeInsets.only(top: 8, left: 8), - child: BBText.errorSmall(error), - ), - ], + return BlocBuilder( + buildWhen: (previous, current) => + previous.secret != current.secret || + previous.obscure != current.obscure || + previous.inputType != current.inputType || + previous.backupKey != current.backupKey || + previous.error != current.error, + builder: (context, state) { + final isBackupKeyMode = state.inputType == KeyChainInputType.backupKey; + final value = isBackupKeyMode ? state.backupKey : state.secret; + final error = state.getValidationError(); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + BBTextInput.bigWithIcon( + value: value, + onChanged: (value) => isBackupKeyMode + ? context.read().updateBackupKey(value) + : context.read().updateInput(value), + obscure: !isBackupKeyMode && state.obscure, + hint: isBackupKeyMode + ? 'Enter your backup key' + : 'Enter your password', + rightIcon: isBackupKeyMode + ? null + : Icon( + state.obscure ? Icons.visibility_off : Icons.visibility, + color: context.colour.onPrimaryContainer, + ), + onRightTap: isBackupKeyMode + ? null + : () => context.read().clickObscure(), + ), + if (error != null) + Padding( + padding: const EdgeInsets.only(top: 8, left: 8), + child: BBText.errorSmall(error), + ), + ], + ); + }, ); } } From 38d727b7d6351e9b04cd02046cd0a1ff79fe8951 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Tue, 25 Feb 2025 16:17:22 -0500 Subject: [PATCH 308/401] refactor(KeychainPage): update button visibility logic --- lib/recoverbull/keychain_page.dart | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/lib/recoverbull/keychain_page.dart b/lib/recoverbull/keychain_page.dart index b9db08ad4..e75d74b57 100644 --- a/lib/recoverbull/keychain_page.dart +++ b/lib/recoverbull/keychain_page.dart @@ -567,7 +567,8 @@ class _SetButton extends StatelessWidget { const _SetButton({required this.inputType}); @override Widget build(BuildContext context) { - final showButton = context.select((KeychainCubit x) => x.state.showButton); + final canStoreKey = + context.select((KeychainCubit x) => x.state.canStoreKey); return AnimatedSwitcher( duration: const Duration(milliseconds: 300), child: Column( @@ -597,11 +598,13 @@ class _SetButton extends StatelessWidget { const Gap(5), FilledButton( onPressed: () { - if (showButton) context.read().confirmPressed(); + context.read().keyServerStatus(); + if (canStoreKey) context.read().confirmPressed(); }, style: FilledButton.styleFrom( - backgroundColor: - showButton ? context.colour.shadow : context.colour.surface, + backgroundColor: canStoreKey + ? context.colour.shadow + : context.colour.surfaceBright, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), @@ -632,7 +635,8 @@ class _ConfirmButton extends StatelessWidget { final KeyChainInputType inputType; @override Widget build(BuildContext context) { - final showButton = context.select((KeychainCubit x) => x.state.showButton); + final canStoreKey = + context.select((KeychainCubit x) => x.state.canStoreKey); final err = context.select((KeychainCubit x) => x.state.error); if (err.isNotEmpty && inputType == KeyChainInputType.password) { @@ -640,11 +644,12 @@ class _ConfirmButton extends StatelessWidget { } return FilledButton( onPressed: () { - if (showButton) context.read().confirmPressed(); + context.read().keyServerStatus(); + if (canStoreKey) context.read().confirmPressed(); }, style: FilledButton.styleFrom( backgroundColor: - showButton ? context.colour.shadow : context.colour.surface, + canStoreKey ? context.colour.shadow : context.colour.surface, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), From 2352e8e685475820a994ea75a8bfa512da4fb985 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Tue, 25 Feb 2025 16:17:57 -0500 Subject: [PATCH 309/401] refactor(KeychainPage): improve recover button logic and state management --- lib/recoverbull/keychain_page.dart | 90 ++++++++++++++++++------------ 1 file changed, 54 insertions(+), 36 deletions(-) diff --git a/lib/recoverbull/keychain_page.dart b/lib/recoverbull/keychain_page.dart index e75d74b57..3be0d5cbd 100644 --- a/lib/recoverbull/keychain_page.dart +++ b/lib/recoverbull/keychain_page.dart @@ -678,18 +678,41 @@ class _RecoverButton extends StatelessWidget { @override Widget build(BuildContext context) { - return BlocSelector( - selector: (state) => state.canRecoverKey, - builder: (context, canRecoverKey) { - return AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - child: Column( - children: [ - _buildInputTypeSwitch(context), - const Gap(8), - _buildRecoverButton(context, canRecoverKey), - ], - ), + return BlocBuilder( + buildWhen: (previous, current) => + previous.canRecoverKey != current.canRecoverKey || + previous.loading != current.loading, + builder: (context, state) { + final canRecover = inputType == KeyChainInputType.backupKey + ? state.canRecoverWithBckupKey + : state.canRecoverKey; + + return Column( + children: [ + _buildInputTypeSwitch(context), + const Gap(8), + FilledButton( + onPressed: state.loading + ? null + : () => context.read().clickRecover(), + style: FilledButton.styleFrom( + backgroundColor: _getButtonColor(context, canRecover), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: state.loading + ? const SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(Colors.white), + ), + ) + : _buildButtonContent(context), + ), + ], ); }, ); @@ -716,29 +739,24 @@ class _RecoverButton extends StatelessWidget { ); } - Widget _buildRecoverButton(BuildContext context, bool canRecoverKey) { - return FilledButton( - onPressed: canRecoverKey - ? () => context.read().clickRecover() - : () => context.read().clickRecover(), - style: FilledButton.styleFrom( - backgroundColor: _getButtonColor(context, canRecoverKey), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - 'Recover with ${_getInputTypeText()}', - style: context.font.bodyMedium!.copyWith( - color: Colors.white, - fontWeight: FontWeight.w900, - ), + Widget _buildButtonContent(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Recover with ${_getInputTypeText()}', + style: context.font.bodyMedium!.copyWith( + color: Colors.white, + fontWeight: FontWeight.w900, ), - const SizedBox(width: 8), - const Icon(Icons.arrow_forward, color: Colors.white, size: 16), - ], - ), + ), + const SizedBox(width: 8), + const Icon( + Icons.arrow_forward, + color: Colors.white, + size: 16, + ), + ], ); } @@ -757,8 +775,8 @@ class _RecoverButton extends StatelessWidget { ); } - Color _getButtonColor(BuildContext context, bool canRecoverKey) { - if (inputType == KeyChainInputType.backupKey || canRecoverKey) { + Color _getButtonColor(BuildContext context, bool canRecover) { + if (inputType == KeyChainInputType.backupKey || canRecover) { return context.colour.shadow; } return context.colour.surface; From 69be749f906f32d3386d1131ba9ba67012b44b75 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Tue, 25 Feb 2025 16:18:13 -0500 Subject: [PATCH 310/401] refactor(KeychainPage): update delete button logic to use canDeleteKey state --- lib/recoverbull/keychain_page.dart | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/recoverbull/keychain_page.dart b/lib/recoverbull/keychain_page.dart index 3be0d5cbd..886fb963d 100644 --- a/lib/recoverbull/keychain_page.dart +++ b/lib/recoverbull/keychain_page.dart @@ -811,11 +811,10 @@ class _DeleteButton extends StatelessWidget { @override Widget build(BuildContext context) { - final state = context.select((KeychainCubit x) => x.state); - final showButton = state.showButton; - + final canDeleteKey = + context.select((KeychainCubit x) => x.state.canDeleteKey); return FilledButton( - onPressed: showButton ? () => _showDeleteConfirmation(context) : null, + onPressed: () => canDeleteKey ? _showDeleteConfirmation(context) : null, style: FilledButton.styleFrom( backgroundColor: context.colour.shadow, shape: RoundedRectangleBorder( From 012e82d1f4217661e54e05b3eab31e0e4230a71a Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Tue, 25 Feb 2025 16:44:10 -0500 Subject: [PATCH 311/401] chore: update resolved-ref in pubspec.lock to latest commit --- pubspec.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.lock b/pubspec.lock index 948842359..15970a487 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1364,7 +1364,7 @@ packages: description: path: "." ref: main - resolved-ref: "1502b11eeef3a139d28a64d9f138d1ca51554d73" + resolved-ref: "8f8efb3f9b2392deb16344bd226952ed044137f1" url: "https://github.com/SatoshiPortal/recoverbull-client-dart.git" source: git version: "1.0.0" From f972b9415de21918664a5273373ba749fe9c94bc Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Tue, 25 Feb 2025 17:30:37 -0500 Subject: [PATCH 312/401] chore: update dependencies in pubspec.lock to latest versions --- pubspec.lock | 101 ++++++++++++++++++++++----------------------------- 1 file changed, 44 insertions(+), 57 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 15970a487..fdc5a819c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -13,23 +13,18 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab" + sha256: dc27559385e905ad30838356c5f5d574014ba39872d732111cd07ac0beff4c57 url: "https://pub.dev" source: hosted - version: "76.0.0" - _macros: - dependency: transitive - description: dart - source: sdk - version: "0.3.3" + version: "80.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e" + sha256: "192d1c5b944e7e53b24b5586db760db934b177d4147c42fbca8c8c5f1eb8d11e" url: "https://pub.dev" source: hosted - version: "6.11.0" + version: "7.3.0" ansicolor: dependency: transitive description: @@ -42,10 +37,10 @@ packages: dependency: transitive description: name: archive - sha256: "6199c74e3db4fbfbd04f66d739e72fe11c8a8957d5f219f1f4482dbde6420b5a" + sha256: "528579c7e4579719f04b21eeeeddfd73a18b31dabc22766893b7d1be7f49b967" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.0.3" args: dependency: transitive description: @@ -363,10 +358,10 @@ packages: dependency: transitive description: name: dart_style - sha256: "7306ab8a2359a48d22310ad823521d723acfed60ee1f7e37388e8986853b6820" + sha256: "27eb0ae77836989a3bc541ce55595e8ceee0992807f14511552a898ddd0d88ac" url: "https://pub.dev" source: hosted - version: "2.3.8" + version: "3.0.1" diff_match_patch: dependency: transitive description: @@ -387,10 +382,10 @@ packages: dependency: transitive description: name: dio_web_adapter - sha256: e485c7a39ff2b384fa1d7e09b4e25f755804de8384358049124830b04fc4f93a + sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" document_file_save_plus: dependency: "direct main" description: @@ -435,10 +430,10 @@ packages: dependency: transitive description: name: ffi - sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" file: dependency: transitive description: @@ -597,10 +592,10 @@ packages: dependency: "direct main" description: name: flutter_native_splash - sha256: "7062602e0dbd29141fb8eb19220b5871ca650be5197ab9c1f193a28b17537bc7" + sha256: edb09c35ee9230c4b03f13dd45bb3a276d0801865f0a4650b7e2a3bba61a803a url: "https://pub.dev" source: hosted - version: "2.4.4" + version: "2.4.5" flutter_plugin_android_lifecycle: dependency: transitive description: @@ -711,10 +706,10 @@ packages: dependency: "direct main" description: name: freezed - sha256: "44c19278dd9d89292cf46e97dc0c1e52ce03275f40a97c5a348e802a924bf40e" + sha256: "59a584c24b3acdc5250bb856d0d3e9c0b798ed14a4af1ddb7dc1c7b41df91c9c" url: "https://pub.dev" source: hosted - version: "2.5.7" + version: "2.5.8" freezed_annotation: dependency: "direct main" description: @@ -796,18 +791,18 @@ packages: dependency: transitive description: name: google_sign_in_android - sha256: "3b96f9b6cf61915f73cbe1218a192623e296a9b8b31965702503649477761e36" + sha256: "7af72e5502c313865c729223b60e8ae7bce0a1011b250c24edcf30d3d7032748" url: "https://pub.dev" source: hosted - version: "6.1.34" + version: "6.1.35" google_sign_in_ios: dependency: transitive description: name: google_sign_in_ios - sha256: "83f015169102df1ab2905cf8abd8934e28f87db9ace7a5fa676998842fed228a" + sha256: "8468465516a6fdc283ffbbb06ec03a860ee34e9ff84b0454074978705b42379b" url: "https://pub.dev" source: hosted - version: "5.7.8" + version: "5.8.0" google_sign_in_platform_interface: dependency: transitive description: @@ -900,10 +895,10 @@ packages: dependency: transitive description: name: image - sha256: "8346ad4b5173924b5ddddab782fc7d8a6300178c8b1dc427775405a01701c4a6" + sha256: "13d3349ace88f12f4a0d175eb5c12dcdd39d35c4c109a8a13dfeb6d0bd9e31c3" url: "https://pub.dev" source: hosted - version: "4.5.2" + version: "4.5.3" integration_test: dependency: "direct dev" description: flutter @@ -945,10 +940,10 @@ packages: dependency: "direct dev" description: name: json_serializable - sha256: c2fcb3920cf2b6ae6845954186420fca40bc0a8abcc84903b7801f17d7050d7c + sha256: "81f04dee10969f89f604e1249382d46b97a1ccad53872875369622b5bfc9e58a" url: "https://pub.dev" source: hosted - version: "6.9.0" + version: "6.9.4" leak_tracker: dependency: transitive description: @@ -977,10 +972,10 @@ packages: dependency: "direct dev" description: name: lint - sha256: d758a5211fce7fd3f5e316f804daefecdc34c7e53559716125e6da7388ae8565 + sha256: "3cd03646de313481336500ba02eb34d07c590535525f154aae7fda7362aa07a9" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.8.0" lints: dependency: transitive description: @@ -1021,14 +1016,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.1.6" - macros: - dependency: transitive - description: - name: macros - sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656" - url: "https://pub.dev" - source: hosted - version: "0.1.3-main.0" matcher: dependency: transitive description: @@ -1219,26 +1206,26 @@ packages: dependency: "direct main" description: name: permission_handler - sha256: "18bf33f7fefbd812f37e72091a15575e72d5318854877e0e4035a24ac1113ecb" + sha256: "59adad729136f01ea9e35a48f5d1395e25cba6cea552249ddbe9cf950f5d7849" url: "https://pub.dev" source: hosted - version: "11.3.1" + version: "11.4.0" permission_handler_android: dependency: transitive description: name: permission_handler_android - sha256: "71bbecfee799e65aff7c744761a57e817e73b738fedf62ab7afd5593da21f9f1" + sha256: d3971dcdd76182a0c198c096b5db2f0884b0d4196723d21a866fc4cdea057ebc url: "https://pub.dev" source: hosted - version: "12.0.13" + version: "12.1.0" permission_handler_apple: dependency: transitive description: name: permission_handler_apple - sha256: e6f6d73b12438ef13e648c4ae56bd106ec60d17e90a59c4545db6781229082a0 + sha256: f84a188e79a35c687c132a0a0556c254747a08561e99ab933f12f6ca71ef3c98 url: "https://pub.dev" source: hosted - version: "9.4.5" + version: "9.4.6" permission_handler_html: dependency: transitive description: @@ -1251,10 +1238,10 @@ packages: dependency: transitive description: name: permission_handler_platform_interface - sha256: e9c8eadee926c4532d0305dff94b85bf961f16759c3af791486613152af4b4f9 + sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878 url: "https://pub.dev" source: hosted - version: "4.2.3" + version: "4.3.0" permission_handler_windows: dependency: transitive description: @@ -1267,10 +1254,10 @@ packages: dependency: transitive description: name: petitparser - sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 + sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646" url: "https://pub.dev" source: hosted - version: "6.0.2" + version: "6.1.0" platform: dependency: transitive description: @@ -1364,7 +1351,7 @@ packages: description: path: "." ref: main - resolved-ref: "8f8efb3f9b2392deb16344bd226952ed044137f1" + resolved-ref: f4592df56331a2473754da28ef329b1197a57b8c url: "https://github.com/SatoshiPortal/recoverbull-client-dart.git" source: git version: "1.0.0" @@ -1433,10 +1420,10 @@ packages: dependency: transitive description: name: source_gen - sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" + sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b" url: "https://pub.dev" source: hosted - version: "1.5.0" + version: "2.0.0" source_helper: dependency: transitive description: @@ -1521,10 +1508,10 @@ packages: dependency: "direct main" description: name: synchronized - sha256: "69fe30f3a8b04a0be0c15ae6490fc859a78ef4c43ae2dd5e8a623d45bfcf9225" + sha256: "0669c70faae6270521ee4f05bffd2919892d42d1276e6c495be80174b6bc0ef6" url: "https://pub.dev" source: hosted - version: "3.3.0+3" + version: "3.3.1" term_glyph: dependency: transitive description: @@ -1761,10 +1748,10 @@ packages: dependency: transitive description: name: win32 - sha256: daf97c9d80197ed7b619040e86c8ab9a9dad285e7671ee7390f9180cc828a51e + sha256: b89e6e24d1454e149ab20fbb225af58660f0c0bf4475544650700d8e2da54aef url: "https://pub.dev" source: hosted - version: "5.10.1" + version: "5.11.0" xdg_directories: dependency: transitive description: @@ -1790,5 +1777,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.7.0-0 <4.0.0" + dart: ">=3.7.0 <4.0.0" flutter: ">=3.27.0" From dc8d18383ac33dac7ab880e8fc88ff0bbf5899ed Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Tue, 25 Feb 2025 17:30:56 -0500 Subject: [PATCH 313/401] fix(KeychainBackupPage): backupId error --- lib/recoverbull/keychain_page.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/recoverbull/keychain_page.dart b/lib/recoverbull/keychain_page.dart index 886fb963d..f4b2b0d64 100644 --- a/lib/recoverbull/keychain_page.dart +++ b/lib/recoverbull/keychain_page.dart @@ -41,7 +41,7 @@ class KeychainBackupPage extends StatelessWidget { create: (context) => KeychainCubit() ..setChainState( _pState, - backupData.$1 ?? '', + backupId ?? '', backupKey, backupSalt ?? '', ), From 6e517457bbdd2fd4aa88df6916ca5f6af2058861 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Tue, 25 Feb 2025 17:33:05 -0500 Subject: [PATCH 314/401] fix(KeychainCubit): renamed invalid function call --- lib/recoverbull/bloc/keychain_cubit.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/recoverbull/bloc/keychain_cubit.dart b/lib/recoverbull/bloc/keychain_cubit.dart index 2751b6474..c6ebda747 100644 --- a/lib/recoverbull/bloc/keychain_cubit.dart +++ b/lib/recoverbull/bloc/keychain_cubit.dart @@ -95,8 +95,7 @@ class KeychainCubit extends Cubit { return; } - final isServerReady = await serverInfo(); - if (!isServerReady) return; + if (!await _ensureServerStatus()) return; if (state.secret.length < pinMin) { state.inputType == KeyChainInputType.pin ? emit( From 3dd0be01f15489ae787938928bc1c62f28ca760c Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Wed, 26 Feb 2025 05:48:00 -0500 Subject: [PATCH 315/401] chore: update flutter_native_splash and google_sign_in_ios versions in Podfile.lock --- ios/Podfile.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index ea32cee0e..affaea50a 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -192,9 +192,9 @@ SPEC CHECKSUMS: flutter_env_native: 52530b3bced65bd04f849b447c298e75d12565e6 flutter_file_dialog: 4c014a45b105709a27391e266c277d7e588e9299 flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069 - flutter_native_splash: f71420956eb811e6d310720fee915f1d42852e7a + flutter_native_splash: df59bb2e1421aa0282cb2e95618af4dcb0c56c29 flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12 - google_sign_in_ios: 07375bfbf2620bc93a602c0e27160d6afc6ead38 + google_sign_in_ios: 4111e87aa5e24a4404f00ea13479f35e571969cc GoogleSignIn: d4281ab6cf21542b1cfaff85c191f230b399d2db GTMAppAuth: f69bd07d68cd3b766125f7e072c45d7340dea0de GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6 From bef0bf3ab12f6955b8830be6df1e757c05087fc0 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Wed, 26 Feb 2025 05:48:09 -0500 Subject: [PATCH 316/401] feat: enable GPU validation mode in Runner scheme --- ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme | 1 + 1 file changed, 1 insertion(+) diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 0533c716f..c469fe173 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -77,6 +77,7 @@ ignoresPersistentStateOnLaunch = "NO" debugDocumentVersioning = "YES" debugServiceExtension = "internal" + enableGPUValidationMode = "1" allowLocationSimulation = "YES"> From 78792495b5b8d3091ab74381eca18632a6eeb77d Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Wed, 26 Feb 2025 07:53:49 -0500 Subject: [PATCH 317/401] feat(KeychainCubit): store original page state and add new download state --- lib/recoverbull/bloc/keychain_cubit.dart | 1 + lib/recoverbull/bloc/keychain_state.dart | 14 +++++++------- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/lib/recoverbull/bloc/keychain_cubit.dart b/lib/recoverbull/bloc/keychain_cubit.dart index c6ebda747..067bfc2e0 100644 --- a/lib/recoverbull/bloc/keychain_cubit.dart +++ b/lib/recoverbull/bloc/keychain_cubit.dart @@ -234,6 +234,7 @@ class KeychainCubit extends Cubit { emit( state.copyWith( pageState: keyChainPageState, + originalPageState: keyChainPageState, // Store original state backupKey: backupKey ?? '', backupId: backupId, backupSalt: HEX.decode(backupSalt), diff --git a/lib/recoverbull/bloc/keychain_state.dart b/lib/recoverbull/bloc/keychain_state.dart index 29a4e839a..afba52b51 100644 --- a/lib/recoverbull/bloc/keychain_state.dart +++ b/lib/recoverbull/bloc/keychain_state.dart @@ -8,6 +8,7 @@ enum KeyChainPageState { enter, confirm, recovery, + download, delete; static KeyChainPageState fromString(String value) { @@ -39,6 +40,7 @@ class KeychainState with _$KeychainState { @Default(false) bool isSecretConfirmed, @Default([]) List shuffledNumbers, @Default('') String error, + @Default(KeyChainPageState.enter) KeyChainPageState originalPageState, }) = _KeychainState; const KeychainState._(); @@ -46,6 +48,8 @@ class KeychainState with _$KeychainState { String displayPin() => 'x' * secret.length; String? getValidationError() { + if (secret.isEmpty) return null; + if (inputType == KeyChainInputType.pin) { const pinMin = KeychainCubit.pinMin; const pinMax = KeychainCubit.pinMax; @@ -57,13 +61,9 @@ class KeychainState with _$KeychainState { } } - if (validateSecret(secret) && - (inputType == KeyChainInputType.password || - inputType == KeyChainInputType.backupKey)) { - return 'The password is among the top 1000 most common'; - } - - return null; + return validateSecret(secret) + ? 'The password is among the top 1000 most common' + : null; } bool get isValid => getValidationError() == null; From 51fb305a886a96533e9d759f21e52b9c49e4e62f Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Wed, 26 Feb 2025 07:54:07 -0500 Subject: [PATCH 318/401] feat(BackupSettings): update button labels and add view/delete backup key option --- lib/recoverbull/backup_settings.dart | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/lib/recoverbull/backup_settings.dart b/lib/recoverbull/backup_settings.dart index 68329d535..a7e18b35f 100644 --- a/lib/recoverbull/backup_settings.dart +++ b/lib/recoverbull/backup_settings.dart @@ -104,7 +104,7 @@ class _Screen extends StatelessWidget { isRed: !isVaultBackupTested, ), BBButton.withColour( - label: "Start backup", + label: "Start Backup", onPressed: () => { context.push( '/wallet-settings/backup-settings/backup-options', @@ -116,7 +116,7 @@ class _Screen extends StatelessWidget { ), const Gap(20), BBButton.withColour( - label: "Recover or test backup", + label: "Recover/Test Backup", onPressed: () { context.push( '/wallet-settings/backup-settings/recover-options', @@ -126,6 +126,18 @@ class _Screen extends StatelessWidget { fillWidth: true, center: true, ), + const Gap(20), + BBButton.withColour( + label: "View/Delete Backup Key", + onPressed: () => { + context.push( + '/wallet-settings/backup-settings/backup-key', + extra: context.read().state.wallet.id, + ), + }, + fillWidth: true, + center: true, + ), ], ), ); @@ -200,7 +212,7 @@ class BackupOptionsScreen extends StatelessWidget { title: 'Physical backup (take your time)', description: 'You have to write down 12 words on a piece of paper or engrave it in metal. Make sure not to lose it. If anybody ever finds those 12 words, they can steal your Bitcoin.', - onTap: () async => context.push( + onTap: () => context.push( '/wallet-settings/backup-settings/backup-options/physical', extra: wallet, ), @@ -316,7 +328,7 @@ class RecoverOptionsScreen extends StatelessWidget { title: 'Physical backup', description: "Restore your wallet by entering the 12 words from your physical backup.", - onTap: () async => context.push( + onTap: () => context.push( '/wallet-settings/backup-settings/recover-options/physical', ), ), From 39cbd5829a356510c2c3012a94730338dc81ae33 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Wed, 26 Feb 2025 07:54:38 -0500 Subject: [PATCH 319/401] refactor: rename _StorageOptionCard to StorageOptionCard and clean up unused code --- lib/recoverbull/encrypted_vault_backup.dart | 69 ++------------------- 1 file changed, 5 insertions(+), 64 deletions(-) diff --git a/lib/recoverbull/encrypted_vault_backup.dart b/lib/recoverbull/encrypted_vault_backup.dart index 35ad28ebb..6c58057ca 100644 --- a/lib/recoverbull/encrypted_vault_backup.dart +++ b/lib/recoverbull/encrypted_vault_backup.dart @@ -11,7 +11,6 @@ import 'package:bb_mobile/styles.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:gap/gap.dart'; import 'package:go_router/go_router.dart'; @@ -150,7 +149,7 @@ class _EncryptedVaultBackupPageState extends State { ...BackupProvider.values.map( (provider) => Padding( padding: const EdgeInsets.only(bottom: 10), - child: _StorageOptionCard( + child: StorageOptionCard( title: provider.title, description: provider.description, icon: Icon(provider.icon, size: 40), @@ -219,13 +218,13 @@ class _InfoSection extends StatelessWidget { } } -class _StorageOptionCard extends StatelessWidget { +class StorageOptionCard extends StatelessWidget { final String title; final String description; final Widget icon; final VoidCallback onTap; - const _StorageOptionCard({ + const StorageOptionCard({ required this.title, required this.description, required this.icon, @@ -337,7 +336,7 @@ class _EncryptedVaultRecoverPageState extends State { ...BackupProvider.values.map( (provider) => Padding( padding: const EdgeInsets.only(bottom: 10), - child: _StorageOptionCard( + child: StorageOptionCard( title: provider.title, description: provider.description, icon: Icon(provider.icon, size: 40), @@ -499,8 +498,7 @@ class _RecoveredBackupInfoPageState extends State { listenWhen: (previous, current) => previous.errorLoadingBackups != current.errorLoadingBackups || previous.loadingBackups != current.loadingBackups || - previous.loadedBackups != current.loadedBackups || - previous.backupKey != current.backupKey, + previous.loadedBackups != current.loadedBackups, listener: (context, state) { if (state.errorLoadingBackups.isNotEmpty) { ScaffoldMessenger.of(context).showSnackBar( @@ -509,39 +507,6 @@ class _RecoveredBackupInfoPageState extends State { _cubit.clearError(); return; } - if (!state.errorLoadingBackups.isNotEmpty && - !state.loadingBackups && - state.backupKey.isNotEmpty) { - showDialog( - context: context, - builder: (context) => AlertDialog( - title: const BBText.titleLarge('Secret key', isBold: true), - content: Row( - children: [ - Expanded( - child: Text( - state.backupKey, - style: context.font.bodySmall! - .copyWith(fontWeight: FontWeight.bold), - ), - ), - IconButton( - onPressed: () { - Clipboard.setData(ClipboardData(text: state.backupKey)); - Navigator.of(context).pop(); - ScaffoldMessenger.of(context).showSnackBar( - context.showToast('Copied to clipboard'), - ); - }, - icon: const Icon(Icons.copy, color: Colors.black), - ), - ], - ), - ), - ); - _cubit.clearError(); - return; - } }, builder: (context, state) { return Scaffold( @@ -649,30 +614,6 @@ class _RecoveredBackupInfoPageState extends State { ], ), ), - const Gap(10), - InkWell( - onTap: () => _cubit.recoverBackupKeyFromMnemonic( - widget.recoveredBackup['path'] as String?, - ), - child: const BBText.bodySmall( - 'Forgot your secret? Click to recover', - textAlign: TextAlign.center, - ), - ), - const Gap(10), - IconButton( - onPressed: () { - context.push( - '/wallet-settings/backup-settings/keychain', - extra: ( - '', - widget.recoveredBackup, - KeyChainPageState.delete.name.toLowerCase() - ), - ); - }, - icon: const Icon(Icons.delete, color: Colors.black), - ), ], ), ), From 045b64ea20f6c22b7c143b250d85320079f36e07 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Wed, 26 Feb 2025 07:56:52 -0500 Subject: [PATCH 320/401] feat: add view/delete backup key page & route --- lib/recoverbull/backup_key.dart | 436 ++++++++++++++++++++++++++++++++ lib/routes.dart | 20 ++ 2 files changed, 456 insertions(+) create mode 100644 lib/recoverbull/backup_key.dart diff --git a/lib/recoverbull/backup_key.dart b/lib/recoverbull/backup_key.dart new file mode 100644 index 000000000..0e5f7da05 --- /dev/null +++ b/lib/recoverbull/backup_key.dart @@ -0,0 +1,436 @@ +import 'package:bb_mobile/_ui/app_bar.dart'; +import 'package:bb_mobile/_ui/components/text.dart'; +import 'package:bb_mobile/_ui/toast.dart'; +import 'package:bb_mobile/recoverbull/bloc/backup_settings_cubit.dart'; +import 'package:bb_mobile/recoverbull/bloc/backup_settings_state.dart'; +import 'package:bb_mobile/recoverbull/bloc/keychain_cubit.dart'; +import 'package:bb_mobile/recoverbull/bloc/keychain_state.dart'; +import 'package:bb_mobile/recoverbull/encrypted_vault_backup.dart'; +import 'package:bb_mobile/styles.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:gap/gap.dart'; +import 'package:go_router/go_router.dart'; +import 'package:intl/intl.dart'; + +class BackupKeyPage extends StatefulWidget { + final String wallet; + const BackupKeyPage({super.key, required this.wallet}); + + @override + State createState() => _BackupKeyPageState(); +} + +class _BackupKeyPageState extends State { + late final BackupSettingsCubit _backupSettingsCubit; + + @override + void initState() { + super.initState(); + _backupSettingsCubit = createBackupSettingsCubit(walletId: widget.wallet); + } + + @override + void dispose() { + _backupSettingsCubit.close(); + super.dispose(); + } + + Future _handleRecover( + BuildContext context, + BackupProvider provider, + ) async { + switch (provider) { + case BackupProvider.googleDrive: + await _backupSettingsCubit.fetchGoogleDriveBackup(); + case BackupProvider.iCloud: + debugPrint('iCloud backup'); + case BackupProvider.custom: + _backupSettingsCubit.fetchFsBackup(); + } + } + + Widget _buildContent(BuildContext context, BackupSettingsState state) { + return SingleChildScrollView( + padding: const EdgeInsets.all(20), + child: Column( + children: [ + const BBText.titleLarge('Where is your latest backup?', isBold: true), + const Gap(20), + ...BackupProvider.values.map( + (provider) => Padding( + padding: const EdgeInsets.only(bottom: 10), + child: StorageOptionCard( + title: provider.title, + description: provider.description, + icon: Icon(provider.icon, size: 40), + onTap: () => _handleRecover(context, provider), + ), + ), + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + return BlocProvider.value( + value: _backupSettingsCubit, + child: BlocConsumer( + listenWhen: (previous, current) => + previous.errorLoadingBackups != current.errorLoadingBackups || + previous.latestRecoveredBackup != current.latestRecoveredBackup, + listener: (context, state) { + if (state.errorLoadingBackups.isNotEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + context.showToast(state.errorLoadingBackups), + ); + _backupSettingsCubit.clearError(); + return; + } + if (state.latestRecoveredBackup.isNotEmpty) { + context.push( + '/wallet-settings/backup-settings/backup-key/options', + extra: ('', state.latestRecoveredBackup), + ); + _backupSettingsCubit.clearError(); + } + }, + builder: (context, state) { + return Scaffold( + appBar: AppBar( + elevation: 0, + automaticallyImplyLeading: false, + centerTitle: true, + flexibleSpace: BBAppBar( + text: '', + onBack: () => context.pop(), + ), + ), + body: state.loadingBackups + ? const Center(child: CircularProgressIndicator()) + : _buildContent(context, state), + ); + }, + ), + ); + } +} + +class BackupKeyOptionsPage extends StatefulWidget { + const BackupKeyOptionsPage({ + super.key, + required this.recoveredBackup, + required this.backupKey, + }); + + final Map recoveredBackup; + final String backupKey; + + @override + State createState() => _BackupKeyInfoPage(); +} + +class _BackupKeyInfoPage extends State { + late final BackupSettingsCubit _backupSettingsCubit; + + @override + void initState() { + super.initState(); + _backupSettingsCubit = createBackupSettingsCubit(); + context.read().keyServerStatus(); + } + + @override + void dispose() { + _backupSettingsCubit.close(); + super.dispose(); + } + + ButtonStyle _getButtonStyle(BuildContext context, bool isEnabled) { + return FilledButton.styleFrom( + backgroundColor: + isEnabled ? context.colour.shadow : context.colour.surface, + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + ); + } + + Widget _buildActionButton({ + required BuildContext context, + required bool isEnabled, + required String text, + required IconData icon, + required VoidCallback? onPressed, + }) { + return FilledButton( + onPressed: isEnabled ? onPressed : null, + style: _getButtonStyle(context, isEnabled), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + text, + style: context.font.bodyMedium!.copyWith( + color: isEnabled ? Colors.white : context.colour.onSurface, + fontWeight: FontWeight.w900, + ), + ), + const Gap(8), + Icon( + icon, + color: isEnabled ? Colors.white : context.colour.onSurface, + size: 20, + ), + ], + ), + ); + } + + void _handleBackupAction(BuildContext context) { + if (widget.backupKey.isNotEmpty) { + _showBackupKeyDialog(context, widget.backupKey); + } else { + context.push( + '/wallet-settings/backup-settings/keychain', + extra: ( + '', + widget.recoveredBackup, + KeyChainPageState.download.name.toLowerCase() + ), + ); + } + } + + void _showBackupKeyDialog(BuildContext context, String backupKey) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const BBText.titleLarge('Backup Key', isBold: true), + content: Row( + children: [ + Expanded( + child: Text( + backupKey, + style: context.font.bodySmall! + .copyWith(fontWeight: FontWeight.bold), + ), + ), + IconButton( + onPressed: () { + Clipboard.setData(ClipboardData(text: backupKey)); + Navigator.of(context).pop(); + ScaffoldMessenger.of(context).showSnackBar( + context.showToast('Copied to clipboard'), + ); + }, + icon: const Icon(Icons.copy, color: Colors.black), + ), + ], + ), + ), + ); + } + + Widget _buildErrorView(BuildContext context) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'ERROR', + style: context.font.titleLarge!.copyWith( + fontWeight: FontWeight.w900, + ), + ), + const Gap(16), + const BBText.title('This is not a backup file', isBold: true), + const Gap(24), + FilledButton( + onPressed: () => context.pop(), + style: FilledButton.styleFrom( + backgroundColor: context.colour.shadow, + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Try again', + style: context.font.bodyMedium!.copyWith( + color: Colors.white, + fontWeight: FontWeight.w900, + ), + ), + const Gap(8), + const Icon(Icons.arrow_forward, color: Colors.white, size: 20), + ], + ), + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + final recoveryFile = widget.recoveredBackup; + if (recoveryFile.isEmpty || + recoveryFile['id'] == null || + recoveryFile['ciphertext'] == null || + recoveryFile['salt'] == null) { + return Scaffold( + appBar: AppBar( + elevation: 0, + automaticallyImplyLeading: false, + centerTitle: true, + flexibleSpace: BBAppBar(text: '', onBack: () => context.go('/home')), + ), + body: _buildErrorView(context), + ); + } + + return MultiBlocProvider( + providers: [BlocProvider.value(value: _backupSettingsCubit)], + child: Builder( + builder: (context) { + return BlocBuilder( + builder: (context, keyState) { + return BlocConsumer( + listenWhen: (previous, current) => + previous.backupKey != current.backupKey, + listener: (context, state) { + if (state.errorLoadingBackups.isNotEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + context.showToast(state.errorLoadingBackups), + ); + context.read().clearError(); + return; + } + if (!state.errorLoadingBackups.isNotEmpty && + !state.loadingBackups && + state.backupKey.isNotEmpty) { + _showBackupKeyDialog(context, state.backupKey); + context.read().clearError(); + } + }, + builder: (context, state) { + return Scaffold( + appBar: AppBar( + elevation: 0, + automaticallyImplyLeading: false, + centerTitle: true, + flexibleSpace: BBAppBar( + text: '', + onBack: () => context.go('/home'), + ), + ), + body: keyState.loading + ? const Center(child: CircularProgressIndicator()) + : Padding( + padding: const EdgeInsets.symmetric(horizontal: 30), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Latest Available Backup', + style: context.font.titleLarge! + .copyWith(fontWeight: FontWeight.w900), + ), + const Gap(20), + _buildInfoText( + context, + 'Backup ID:', + '${recoveryFile['id']}', + ), + const Gap(8), + _buildInfoText( + context, + 'Created at:', + DateFormat('MMM dd, yyyy HH:mm:ss').format( + DateTime.fromMillisecondsSinceEpoch( + recoveryFile['created_at'] as int, + ).toLocal(), + ), + ), + const Gap(20), + _buildActionButton( + context: context, + isEnabled: keyState.keyServerUp, + text: widget.backupKey.isNotEmpty + ? 'View Backup Key' + : 'Download Backup Key', + icon: widget.backupKey.isNotEmpty + ? CupertinoIcons.eye_fill + : CupertinoIcons.cloud_download_fill, + onPressed: () => _handleBackupAction(context), + ), + const Gap(10), + _buildActionButton( + context: context, + isEnabled: keyState.keyServerUp, + text: 'Delete Backup Key', + icon: CupertinoIcons.delete_right, + onPressed: () => context.push( + '/wallet-settings/backup-settings/keychain', + extra: ( + '', + widget.recoveredBackup, + KeyChainPageState.delete.name + .toLowerCase() + ), + ), + ), + const Gap(10), + InkWell( + onTap: () => context + .read() + .recoverBackupKeyFromMnemonic( + widget.recoveredBackup['path'] + as String?, + ), + child: const BBText.bodySmall( + 'Forgot your secret? Click to recover', + textAlign: TextAlign.center, + ), + ), + const Gap(10), + ], + ), + ), + ); + }, + ); + }, + ); + }, + ), + ); + } + + Widget _buildInfoText(BuildContext context, String label, String value) { + return RichText( + textAlign: TextAlign.center, + text: TextSpan( + children: [ + TextSpan( + text: label, + style: + context.font.bodyMedium!.copyWith(fontWeight: FontWeight.bold), + ), + TextSpan( + text: value, + style: + context.font.bodyMedium!.copyWith(fontWeight: FontWeight.bold), + ), + ], + ), + ); + } +} diff --git a/lib/routes.dart b/lib/routes.dart index e522bc93d..ad7eeb851 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -12,6 +12,7 @@ import 'package:bb_mobile/import/hardware_page.dart'; import 'package:bb_mobile/import/page.dart'; import 'package:bb_mobile/locator.dart'; import 'package:bb_mobile/receive/receive_page.dart'; +import 'package:bb_mobile/recoverbull/backup_key.dart'; import 'package:bb_mobile/recoverbull/backup_settings.dart'; import 'package:bb_mobile/recoverbull/encrypted_vault_backup.dart'; import 'package:bb_mobile/recoverbull/keychain_page.dart'; @@ -276,6 +277,25 @@ GoRouter setupRouter() => GoRouter( ); }, ), + GoRoute( + path: 'backup-key', + builder: (context, state) { + return BackupKeyPage( + wallet: state.extra! as String, + ); + }, + routes: [ + GoRoute( + path: 'options', + builder: (context, state) { + final (backupKey, recoveredBackup) = + state.extra! as (String, Map); + return BackupKeyOptionsPage( + recoveredBackup: recoveredBackup, + backupKey: backupKey, + ); + }) + ]), ], ), ], From 186ece90968176ab246bc388bfa27ca70f9fdebb Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Wed, 26 Feb 2025 07:57:41 -0500 Subject: [PATCH 321/401] feat(KeychainBackupPage): pass page state to success dialog and handle download state --- lib/recoverbull/keychain_page.dart | 37 +++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/lib/recoverbull/keychain_page.dart b/lib/recoverbull/keychain_page.dart index f4b2b0d64..4070defa7 100644 --- a/lib/recoverbull/keychain_page.dart +++ b/lib/recoverbull/keychain_page.dart @@ -48,16 +48,21 @@ class KeychainBackupPage extends StatelessWidget { ), BlocProvider.value(value: createBackupSettingsCubit()), ], - child: _Screen(backupKey: backupKey, backup: backup), + child: _Screen( + backupKey: backupKey, + backup: backup, + pState: _pState, + ), ); } } class _Screen extends StatelessWidget { - const _Screen({this.backupKey, required this.backup}); + const _Screen({this.backupKey, required this.backup, required this.pState}); final String? backupKey; final Map backup; + final KeyChainPageState pState; @override Widget build(BuildContext context) { return MultiBlocListener( @@ -101,7 +106,9 @@ class _Screen extends StatelessWidget { showDialog( context: context, barrierDismissible: false, - builder: (context) => const _SuccessDialog(isRecovery: true), + builder: (context) => _SuccessDialog( + pageState: pState, + ), ); } }, @@ -122,9 +129,8 @@ class _Screen extends StatelessWidget { showDialog( context: context, barrierDismissible: false, - builder: (context) => const _SuccessDialog( - isRecovery: false, - isDelete: true, + builder: (context) => _SuccessDialog( + pageState: pState, ), ); } @@ -143,7 +149,9 @@ class _Screen extends StatelessWidget { showDialog( context: context, barrierDismissible: false, - builder: (context) => const _SuccessDialog(isRecovery: false), + builder: (context) => _SuccessDialog( + pageState: pState, + ), ); } @@ -152,10 +160,17 @@ class _Screen extends StatelessWidget { !state.hasError && backup.isNotEmpty && state.backupKey.isNotEmpty) { - context.read().recoverBackup( - jsonEncode(backup), - state.backupKey, - ); + if (state.pageState == KeyChainPageState.download) { + context.push( + '/wallet-settings/backup-settings/backup-key/options', + extra: (state.backupKey, backup), + ); + } else { + context.read().recoverBackup( + jsonEncode(backup), + state.backupKey, + ); + } } if (state.hasError) { From 81dd38a46da96e413547cfb0f805a5a79cdf10e8 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Wed, 26 Feb 2025 07:58:47 -0500 Subject: [PATCH 322/401] feat(KeychainPage): add download state handling and update success dialog messages --- lib/recoverbull/keychain_page.dart | 117 +++++++++++++++++------------ 1 file changed, 68 insertions(+), 49 deletions(-) diff --git a/lib/recoverbull/keychain_page.dart b/lib/recoverbull/keychain_page.dart index 4070defa7..e5caba0c8 100644 --- a/lib/recoverbull/keychain_page.dart +++ b/lib/recoverbull/keychain_page.dart @@ -119,7 +119,6 @@ class _Screen extends StatelessWidget { previous.keySecretState != current.keySecretState || previous.error != current.error, listener: (context, state) { - // Handle delete state if (state.pageState == KeyChainPageState.delete && state.keySecretState == KeySecretState.deleted && !state.loading && @@ -154,7 +153,6 @@ class _Screen extends StatelessWidget { ), ); } - if (state.keySecretState == KeySecretState.recovered && !state.loading && !state.hasError && @@ -235,6 +233,11 @@ class _Screen extends StatelessWidget { key: const ValueKey('delete'), inputType: state.inputType, ); + case KeyChainPageState.download: + return _RecoveryPage( + key: const ValueKey('view'), + inputType: state.inputType, + ); } } } @@ -375,13 +378,13 @@ class _DeletePage extends StatelessWidget { children: [ const Gap(50), const BBText.titleLarge( - 'Delete Backup', + 'Delete Backup Key', textAlign: TextAlign.center, isBold: true, ), const Gap(8), BBText.bodySmall( - 'Enter your ${inputType == KeyChainInputType.pin ? 'PIN' : 'password'} to delete this backup', + 'Enter your ${inputType == KeyChainInputType.pin ? 'PIN' : 'password'} to delete this backup key', textAlign: TextAlign.center, ), const Gap(50), @@ -696,15 +699,36 @@ class _RecoverButton extends StatelessWidget { return BlocBuilder( buildWhen: (previous, current) => previous.canRecoverKey != current.canRecoverKey || - previous.loading != current.loading, + previous.loading != current.loading || + previous.pageState != current.pageState, builder: (context, state) { final canRecover = inputType == KeyChainInputType.backupKey ? state.canRecoverWithBckupKey : state.canRecoverKey; + // Check if we're in the download flow by checking original state + final isDownloadFlow = state.pageState == KeyChainPageState.download || + state.originalPageState == KeyChainPageState.download; + return Column( children: [ - _buildInputTypeSwitch(context), + // Always show PIN/password switch + InkWell( + onTap: () => _switchInputType(context), + child: BBText.bodySmall(_getSwitchButtonText(), isBold: true), + ), + // Only show backup key option if not in download flow and not in backup key mode + if (!isDownloadFlow && + inputType != KeyChainInputType.backupKey) ...[ + const Gap(8), + InkWell( + onTap: () => _switchToBackupKey(context), + child: const BBText.bodySmall( + 'Recover with backup key', + isBold: true, + ), + ), + ], const Gap(8), FilledButton( onPressed: state.loading @@ -733,27 +757,6 @@ class _RecoverButton extends StatelessWidget { ); } - Widget _buildInputTypeSwitch(BuildContext context) { - return Column( - children: [ - // Switch between PIN and Password - InkWell( - onTap: () => _switchInputType(context), - child: BBText.bodySmall(_getSwitchButtonText(), isBold: true), - ), - // Show backup key option only when not in backup key mode - if (inputType != KeyChainInputType.backupKey) ...[ - const Gap(8), - InkWell( - onTap: () => _switchToBackupKey(context), - child: - const BBText.bodySmall('Recover with backup key', isBold: true), - ), - ], - ], - ); - } - Widget _buildButtonContent(BuildContext context) { return Row( mainAxisAlignment: MainAxisAlignment.center, @@ -840,7 +843,7 @@ class _DeleteButton extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ Text( - 'Delete Backup', + 'Delete Backup Key', style: context.font.bodyMedium!.copyWith( color: Colors.white, fontWeight: FontWeight.w900, @@ -857,25 +860,36 @@ class _DeleteButton extends StatelessWidget { showDialog( context: context, builder: (dialogContext) => AlertDialog( - title: const BBText.title('Delete Backup?', isBold: true), + title: const BBText.title('Delete Backup Key?', isBold: true), content: const BBText.bodySmall( - 'This action cannot be undone. Are you sure you want to delete this backup?', + 'This action cannot be undone. Are you sure you want to delete this backup key?', ), actions: [ TextButton( onPressed: () => Navigator.of(dialogContext).pop(), - child: const Text('Cancel'), + child: Text( + 'Cancel', + style: context.font.bodyMedium, + ), ), FilledButton( - onPressed: () async { + onPressed: () { // First close the dialog Navigator.of(dialogContext).pop(); // Then trigger the delete action using the original context context.read().deleteBackupKey(); }, - style: - FilledButton.styleFrom(backgroundColor: context.colour.error), - child: const Text('Delete'), + style: FilledButton.styleFrom( + backgroundColor: context.colour.shadow, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: Text('Delete', + style: context.font.bodyMedium!.copyWith( + color: Colors.white, + fontWeight: FontWeight.w900, + )), ), ], ), @@ -898,33 +912,32 @@ class _LoadingView extends StatelessWidget { } class _SuccessDialog extends StatelessWidget { - const _SuccessDialog({ - required this.isRecovery, - this.isDelete = false, - }); + const _SuccessDialog({required this.pageState}); - final bool isRecovery; - final bool isDelete; + final KeyChainPageState pageState; @override Widget build(BuildContext context) { String title; String message; String route; - - if (isDelete) { - title = 'Backup Deleted'; - message = 'Your backup has been permanently deleted'; - route = '/home'; - } else if (isRecovery) { + if (pageState == KeyChainPageState.recovery) { title = 'Recovery Successful'; message = 'Your wallet has been recovered successfully'; route = '/home'; - } else { + } else if (pageState == KeyChainPageState.enter) { title = 'Backup Successful'; message = 'Your wallet has been backed up successfully \n Please test your backup'; route = '/wallet-settings/backup-settings/recover-options/encrypted'; + } else if (pageState == KeyChainPageState.delete) { + title = 'Backup Key Deleted'; + message = 'Your backup key has been permanently deleted'; + route = '/home'; + } else { + title = 'Backup Downloaded'; + message = 'Your backup has been downloaded successfully'; + route = '/home'; } return Dialog( @@ -955,7 +968,13 @@ class _SuccessDialog extends StatelessWidget { borderRadius: BorderRadius.circular(8), ), ), - child: const Text('Continue'), + child: Text( + 'Continue', + style: context.font.bodyMedium?.copyWith( + color: Colors.white, + fontWeight: FontWeight.w900, + ), + ), ), ], ), From 26ec87ce694b6f06fd169aefbb66feb5821e2a3b Mon Sep 17 00:00:00 2001 From: ethicnology Date: Wed, 26 Feb 2025 13:30:18 -0500 Subject: [PATCH 323/401] fix: Recover from Physical Backup OR Encrypted Vault --- lib/home/home_page.dart | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/lib/home/home_page.dart b/lib/home/home_page.dart index f91646cc4..19f1f904b 100644 --- a/lib/home/home_page.dart +++ b/lib/home/home_page.dart @@ -1029,8 +1029,19 @@ class HomeNoWalletsWithCreation extends StatelessWidget { ), const Gap(16), BBButton.text( - label: 'Recover wallet backup', + label: 'Recover wallet from Physical Backup', + centered: true, + onSurface: true, + isBlue: false, + fontSize: 11, + onPressed: () => context1.push('/import-main'), + ), + BBButton.text( + label: 'Recover wallet from Encrypted Vault', centered: true, + onSurface: true, + isBlue: false, + fontSize: 11, onPressed: () { context1.push( '/wallet-settings/backup-settings/recover-options/encrypted', @@ -1098,10 +1109,7 @@ class HomeNoWalletsView extends StatelessWidget { ), Text( 'OWN YOUR MONEY', - style: font.copyWith( - fontSize: 59, - height: 0.8, - ), + style: font.copyWith(fontSize: 59, height: 0.8), ), const Gap(8), SizedBox( @@ -1122,8 +1130,17 @@ class HomeNoWalletsView extends StatelessWidget { }, ), ), + const Gap(20), + BBButton.text( + label: 'Recover wallet from Physical Backup', + centered: true, + onSurface: true, + isBlue: false, + fontSize: 11, + onPressed: () => context.push('/import-main'), + ), BBButton.text( - label: 'Recover wallet backup', + label: 'Recover wallet from Encrypted Vault', centered: true, onSurface: true, isBlue: false, From 099bc62763c697ea28e490073e144ebfe59e040f Mon Sep 17 00:00:00 2001 From: ethicnology Date: Wed, 26 Feb 2025 14:56:41 -0500 Subject: [PATCH 324/401] fix: remove re-added enableGPUValidationMode = "1" --- ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme | 1 - 1 file changed, 1 deletion(-) diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index c469fe173..0533c716f 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -77,7 +77,6 @@ ignoresPersistentStateOnLaunch = "NO" debugDocumentVersioning = "YES" debugServiceExtension = "internal" - enableGPUValidationMode = "1" allowLocationSimulation = "YES"> From ffd01fbd05c695ec83278b6683c32c1a636e5eeb Mon Sep 17 00:00:00 2001 From: ethicnology Date: Wed, 26 Feb 2025 14:59:10 -0500 Subject: [PATCH 325/401] refactor: rename WarningBanner `Key server is down` --- lib/home/home_page.dart | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/home/home_page.dart b/lib/home/home_page.dart index 19f1f904b..2e7038e9e 100644 --- a/lib/home/home_page.dart +++ b/lib/home/home_page.dart @@ -1183,10 +1183,7 @@ class HomeWarnings extends StatelessWidget { info: w.info, ), if (!keyServerUp) - WarningBanner( - onTap: () {}, - info: 'Key server is down. Backup key is unavailable.', - ), + WarningBanner(onTap: () {}, info: 'Key server is down'), ], ); } From 079fff67f7deb1dbb68600fa78619fd23a871612 Mon Sep 17 00:00:00 2001 From: ethicnology Date: Wed, 26 Feb 2025 15:01:33 -0500 Subject: [PATCH 326/401] refactor: remove cooldown check from keyServerStatus --- lib/recoverbull/bloc/keychain_cubit.dart | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/lib/recoverbull/bloc/keychain_cubit.dart b/lib/recoverbull/bloc/keychain_cubit.dart index 067bfc2e0..71e0b734f 100644 --- a/lib/recoverbull/bloc/keychain_cubit.dart +++ b/lib/recoverbull/bloc/keychain_cubit.dart @@ -41,17 +41,11 @@ class KeychainCubit extends Cubit { Future keyServerStatus() async { if (!isClosed) { try { - final info = await _keyService.serverInfo(); - final isUp = info.cooldown <= 1; - - if (isUp != state.keyServerUp) { - emit(state.copyWith(keyServerUp: isUp)); - } + await _keyService.serverInfo(); + emit(state.copyWith(keyServerUp: true)); } catch (e) { debugPrint('Server status check failed: $e'); - if (state.keyServerUp) { - emit(state.copyWith(keyServerUp: false)); - } + emit(state.copyWith(keyServerUp: false)); } } } From 3e30a524c67a1a019115cbb04010c9003fb37883 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Wed, 26 Feb 2025 18:03:42 -0500 Subject: [PATCH 327/401] feat(Button): add border color to BBButton for improved visibility --- lib/_ui/components/button.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/_ui/components/button.dart b/lib/_ui/components/button.dart index 5048c58a5..89e123101 100644 --- a/lib/_ui/components/button.dart +++ b/lib/_ui/components/button.dart @@ -453,6 +453,7 @@ class BBButton extends StatelessWidget { surfaceTintColor: darkMode ? context.colour.primaryContainer : NewColours.lightGray, // shadowColor: Colors.transparent, + side: const BorderSide(color: Colors.white), elevation: 0, padding: const EdgeInsets.symmetric(vertical: 8), From 7e3ba1181eee95ed68f7b7aeb1d2c43ab8f74561 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Wed, 26 Feb 2025 18:05:09 -0500 Subject: [PATCH 328/401] refactor(KeychainCubit): remove input validation for secret length for recovery, download & delete --- lib/recoverbull/bloc/keychain_state.dart | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/recoverbull/bloc/keychain_state.dart b/lib/recoverbull/bloc/keychain_state.dart index afba52b51..db2f379ee 100644 --- a/lib/recoverbull/bloc/keychain_state.dart +++ b/lib/recoverbull/bloc/keychain_state.dart @@ -48,6 +48,13 @@ class KeychainState with _$KeychainState { String displayPin() => 'x' * secret.length; String? getValidationError() { + // Skip validation during recovery, delete or download + if (pageState == KeyChainPageState.recovery || + pageState == KeyChainPageState.download || + pageState == KeyChainPageState.delete) { + return null; + } + if (secret.isEmpty) return null; if (inputType == KeyChainInputType.pin) { From 7e4a194b98f544b9607483442f30727ba37f938c Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Wed, 26 Feb 2025 18:05:15 -0500 Subject: [PATCH 329/401] refactor(KeychainCubit): remove input validation for secret length --- lib/recoverbull/bloc/keychain_cubit.dart | 29 ++++++++++++------------ 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/lib/recoverbull/bloc/keychain_cubit.dart b/lib/recoverbull/bloc/keychain_cubit.dart index 067bfc2e0..c37f16761 100644 --- a/lib/recoverbull/bloc/keychain_cubit.dart +++ b/lib/recoverbull/bloc/keychain_cubit.dart @@ -94,22 +94,21 @@ class KeychainCubit extends Cubit { ); return; } - if (!await _ensureServerStatus()) return; - if (state.secret.length < pinMin) { - state.inputType == KeyChainInputType.pin - ? emit( - state.copyWith( - error: 'pin should be at least $pinMin digits long', - ), - ) - : emit( - state.copyWith( - error: 'password should be at least $pinMin characters long', - ), - ); - return; - } + // if (state.secret.length < pinMin) { + // state.inputType == KeyChainInputType.pin + // ? emit( + // state.copyWith( + // error: 'pin should be at least $pinMin digits long', + // ), + // ) + // : emit( + // state.copyWith( + // error: 'password should be at least $pinMin characters long', + // ), + // ); + // return; + // } try { emit(state.copyWith(loading: true, error: '')); From 1641fe72049b089163129664355c6d33b1411664 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Wed, 26 Feb 2025 18:05:57 -0500 Subject: [PATCH 330/401] feat(KeychainPage): replace FilledButton with BBButton to handle dark mode UI --- lib/recoverbull/keychain_page.dart | 84 +++++++----------------------- 1 file changed, 20 insertions(+), 64 deletions(-) diff --git a/lib/recoverbull/keychain_page.dart b/lib/recoverbull/keychain_page.dart index e5caba0c8..c6be67d7f 100644 --- a/lib/recoverbull/keychain_page.dart +++ b/lib/recoverbull/keychain_page.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:bb_mobile/_ui/app_bar.dart'; +import 'package:bb_mobile/_ui/components/button.dart'; import 'package:bb_mobile/_ui/components/text.dart'; import 'package:bb_mobile/_ui/components/text_input.dart'; import 'package:bb_mobile/_ui/page_template.dart'; @@ -319,7 +320,7 @@ class _RecoveryPage extends StatelessWidget { @override Widget build(BuildContext context) { return StackedPage( - bottomChildHeight: MediaQuery.of(context).size.height * 0.15, + bottomChildHeight: MediaQuery.of(context).size.height * 0.18, bottomChild: _RecoverButton(inputType: inputType), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 32), @@ -614,33 +615,15 @@ class _SetButton extends StatelessWidget { ), ), const Gap(5), - FilledButton( + BBButton.withColour( + fillWidth: true, + label: + 'Set ${inputType == KeyChainInputType.pin ? 'PIN' : 'password'}', + disabled: !canStoreKey, onPressed: () { context.read().keyServerStatus(); if (canStoreKey) context.read().confirmPressed(); }, - style: FilledButton.styleFrom( - backgroundColor: canStoreKey - ? context.colour.shadow - : context.colour.surfaceBright, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - 'Set ${inputType == KeyChainInputType.pin ? 'PIN' : 'password'}', - style: context.font.bodyMedium!.copyWith( - color: Colors.white, - fontWeight: FontWeight.w900, - ), - ), - const SizedBox(width: 8), - const Icon(Icons.arrow_forward, color: Colors.white, size: 16), - ], - ), ), ], ), @@ -660,32 +643,16 @@ class _ConfirmButton extends StatelessWidget { if (err.isNotEmpty && inputType == KeyChainInputType.password) { return Center(child: BBText.errorSmall(err)); } - return FilledButton( + return BBButton.withColour( + fillWidth: true, onPressed: () { context.read().keyServerStatus(); if (canStoreKey) context.read().confirmPressed(); }, - style: FilledButton.styleFrom( - backgroundColor: - canStoreKey ? context.colour.shadow : context.colour.surface, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - 'Confirm ${inputType == KeyChainInputType.pin ? 'PIN' : 'password'}', - style: context.font.bodyMedium!.copyWith( - color: Colors.white, - fontWeight: FontWeight.w900, - ), - ), - const SizedBox(width: 8), - const Icon(Icons.arrow_forward, color: Colors.white, size: 16), - ], - ), + leftIcon: Icons.arrow_forward, + label: + 'Confirm ${inputType == KeyChainInputType.pin ? 'PIN' : 'password'}', + disabled: !canStoreKey, ); } } @@ -712,15 +679,15 @@ class _RecoverButton extends StatelessWidget { return Column( children: [ + const Gap(10), // Always show PIN/password switch InkWell( onTap: () => _switchInputType(context), child: BBText.bodySmall(_getSwitchButtonText(), isBold: true), ), - // Only show backup key option if not in download flow and not in backup key mode if (!isDownloadFlow && inputType != KeyChainInputType.backupKey) ...[ - const Gap(8), + const Gap(10), InkWell( onTap: () => _switchToBackupKey(context), child: const BBText.bodySmall( @@ -941,6 +908,7 @@ class _SuccessDialog extends StatelessWidget { } return Dialog( + backgroundColor: context.colour.primaryContainer, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), child: Padding( padding: const EdgeInsets.all(24), @@ -949,7 +917,7 @@ class _SuccessDialog extends StatelessWidget { children: [ Icon( Icons.check_circle_outline, - color: context.colour.shadow, + color: context.colour.primary, size: 48, ), const Gap(16), @@ -957,25 +925,13 @@ class _SuccessDialog extends StatelessWidget { const Gap(8), BBText.bodySmall(message, textAlign: TextAlign.center), const Gap(24), - FilledButton( + BBButton.big( + label: 'Continue', onPressed: () { Navigator.of(context).pop(); context.go(route); }, - style: FilledButton.styleFrom( - backgroundColor: context.colour.shadow, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - child: Text( - 'Continue', - style: context.font.bodyMedium?.copyWith( - color: Colors.white, - fontWeight: FontWeight.w900, - ), - ), - ), + ) ], ), ), From 2ef7cb4478db8a8e52c01622e50db69d92f3594f Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Wed, 26 Feb 2025 18:06:06 -0500 Subject: [PATCH 331/401] code cleanup --- lib/recoverbull/backup_settings.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/recoverbull/backup_settings.dart b/lib/recoverbull/backup_settings.dart index a7e18b35f..554ee510f 100644 --- a/lib/recoverbull/backup_settings.dart +++ b/lib/recoverbull/backup_settings.dart @@ -252,7 +252,10 @@ class BackupOptionsScreen extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - BBText.title(title, isBold: true), + BBText.title( + title, + isBold: true, + ), const Gap(4), BBText.bodySmall(description, removeColourOpacity: true), ], From 80d18b97ddad3ff8f206303a95cf88b3b2cfc5b8 Mon Sep 17 00:00:00 2001 From: ethicnology Date: Wed, 26 Feb 2025 18:10:06 -0500 Subject: [PATCH 332/401] refactor: remove scrambled PIN --- lib/recoverbull/bloc/keychain_cubit.dart | 11 +---------- lib/recoverbull/bloc/keychain_state.dart | 1 - lib/recoverbull/keychain_page.dart | 9 +-------- 3 files changed, 2 insertions(+), 19 deletions(-) diff --git a/lib/recoverbull/bloc/keychain_cubit.dart b/lib/recoverbull/bloc/keychain_cubit.dart index 71e0b734f..11a9189bf 100644 --- a/lib/recoverbull/bloc/keychain_cubit.dart +++ b/lib/recoverbull/bloc/keychain_cubit.dart @@ -18,13 +18,9 @@ class KeychainCubit extends Cubit { late final KeyService _keyService; void _initialize() { - shuffleAndEmit(); if (keyServerUrl.isEmpty) { emit( - state.copyWith( - error: 'keychain api is not set', - keyServerUp: false, - ), + state.copyWith(error: 'keychain api is not set', keyServerUp: false), ); return; } @@ -236,11 +232,6 @@ class KeychainCubit extends Cubit { ); } - void shuffleAndEmit() { - final shuffledList = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]..shuffle(); - emit(state.copyWith(shuffledNumbers: shuffledList)); - } - void updateBackupKey(String value) { if (value == state.backupKey) return; // Avoid duplicate state emit(state.copyWith(backupKey: value, error: '')); diff --git a/lib/recoverbull/bloc/keychain_state.dart b/lib/recoverbull/bloc/keychain_state.dart index afba52b51..811a45cda 100644 --- a/lib/recoverbull/bloc/keychain_state.dart +++ b/lib/recoverbull/bloc/keychain_state.dart @@ -38,7 +38,6 @@ class KeychainState with _$KeychainState { @Default('') String backupKey, @Default([]) List backupSalt, @Default(false) bool isSecretConfirmed, - @Default([]) List shuffledNumbers, @Default('') String error, @Default(KeyChainPageState.enter) KeyChainPageState originalPageState, }) = _KeychainState; diff --git a/lib/recoverbull/keychain_page.dart b/lib/recoverbull/keychain_page.dart index e5caba0c8..74f27f557 100644 --- a/lib/recoverbull/keychain_page.dart +++ b/lib/recoverbull/keychain_page.dart @@ -504,11 +504,6 @@ class KeyPad extends StatelessWidget { @override Widget build(BuildContext context) { - final shuffledNumbers = - context.select((KeychainCubit x) => x.state.shuffledNumbers); - final shuffledNumberButtonList = [ - for (final i in shuffledNumbers) NumberButton(text: i.toString()), - ]; return Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: GridView( @@ -517,9 +512,7 @@ class KeyPad extends StatelessWidget { gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3), children: [ - for (var i = 0; i < 9; i = i + 1) shuffledNumberButtonList[i], - Container(), - shuffledNumberButtonList[9], + for (var i = 0; i < 9; i = i + 1) NumberButton(text: i.toString()), ], ), ); From 9535b566cc9d2e5b1914584aa16f4b9537b80b09 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Wed, 26 Feb 2025 19:39:41 -0500 Subject: [PATCH 333/401] refactor(HomeState): remove optional route parameter from homeWarnings method --- lib/home/bloc/home_state.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/home/bloc/home_state.dart b/lib/home/bloc/home_state.dart index e6c29bdff..219e0aaf0 100644 --- a/lib/home/bloc/home_state.dart +++ b/lib/home/bloc/home_state.dart @@ -402,7 +402,7 @@ class HomeState with _$HomeState { : walletsWithEnoughBalance; } - Set<({String info, Wallet walletBloc, String? route})> homeWarnings( + Set<({String info, Wallet walletBloc})> homeWarnings( BBNetwork network, ) { bool instantBalWarning(Wallet wb) { From bef92814cede11bacaaf1f5437f4c79ca0aed320 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Wed, 26 Feb 2025 19:43:24 -0500 Subject: [PATCH 334/401] feat(HomeState): update homeWarnings to show common warning --- lib/home/bloc/home_state.dart | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/lib/home/bloc/home_state.dart b/lib/home/bloc/home_state.dart index 219e0aaf0..c537be0d4 100644 --- a/lib/home/bloc/home_state.dart +++ b/lib/home/bloc/home_state.dart @@ -413,10 +413,23 @@ class HomeState with _$HomeState { bool needsBackupWarning(Wallet wb) => !wb.physicalBackupTested || !wb.vaultBackupTested; - final warnings = <({String info, Wallet walletBloc, String? route})>{}; - final List backupWalletFngrForBackupWarning = []; + final warnings = <({String info, Wallet walletBloc})>{}; + final networkWallets = walletsFromNetwork(network); + + // Check for any wallet needing backup + final walletNeedingBackup = + networkWallets.where(needsBackupWarning).firstOrNull; + if (walletNeedingBackup != null) { + warnings.add( + ( + info: 'Backup needed to be tested! Tap to test.', + walletBloc: walletNeedingBackup, + ), + ); + } - for (final walletBloc in walletsFromNetwork(network)) { + // Check for instant wallets with high balance + for (final walletBloc in networkWallets) { if (instantBalWarning(walletBloc)) { warnings.add( ( From b14f7d612f7e1875d15ffd5b4472bef95c0029ac Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Wed, 26 Feb 2025 19:46:17 -0500 Subject: [PATCH 335/401] code cleanup --- lib/home/bloc/home_state.dart | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/lib/home/bloc/home_state.dart b/lib/home/bloc/home_state.dart index c537be0d4..1dafe664e 100644 --- a/lib/home/bloc/home_state.dart +++ b/lib/home/bloc/home_state.dart @@ -435,36 +435,9 @@ class HomeState with _$HomeState { ( info: 'Instant wallet balance is high', walletBloc: walletBloc, - route: null ), ); } - - if (needsBackupWarning(walletBloc)) { - final fngr = walletBloc.sourceFingerprint; - if (backupWalletFngrForBackupWarning.contains(fngr)) continue; - - if (!walletBloc.physicalBackupTested) { - warnings.add( - ( - info: 'Physical backup needed! Tap to test backup.', - walletBloc: walletBloc, - route: '/wallet-settings/backup-settings/backup-options/physical' - ), - ); - } - if (!walletBloc.vaultBackupTested) { - warnings.add( - ( - info: 'Encrypted backup needed! Tap to test backup.', - walletBloc: walletBloc, - route: '/wallet-settings/backup-settings/backup-options/encrypted' - ), - ); - } - - backupWalletFngrForBackupWarning.add(fngr); - } } return warnings; From d19b8217fe908a1afecebf0464cff5cdff0ecc08 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Wed, 26 Feb 2025 19:48:25 -0500 Subject: [PATCH 336/401] refactor(CardItem): show warning on each wallet that need to be tested --- lib/home/home_page.dart | 154 ++++++++++++++++++++-------------------- 1 file changed, 77 insertions(+), 77 deletions(-) diff --git a/lib/home/home_page.dart b/lib/home/home_page.dart index f91646cc4..a58d71df0 100644 --- a/lib/home/home_page.dart +++ b/lib/home/home_page.dart @@ -422,90 +422,95 @@ class CardItem extends StatelessWidget { ), child: Stack( children: [ - /* - // Uncomment to get settings button (3 dots) on top right - TopRight( - child: Padding( - padding: const EdgeInsets.only(top: 4), - child: IconButton( - onPressed: () { - final walletBloc = context.read(); - context.push('/wallet-settings', extra: walletBloc); - }, - color: context.colour.onPrimary, - icon: const FaIcon( - FontAwesomeIcons.ellipsis, - ), - ), - ), - ), - */ - Column( - crossAxisAlignment: CrossAxisAlignment.stretch, + Row( children: [ - const Gap(8), - BBText.titleLarge( - name ?? fingerprint, - onSurface: true, - fontSize: 20, - compact: true, - ), - const Gap(4), - Opacity( - opacity: 0.7, - child: BBText.bodySmall( - walletStr, - onSurface: true, - isBold: true, - fontSize: 12, - ), - ), - const Spacer(), - Row( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - BBText.titleLarge( - balance, - onSurface: true, - isBold: true, - fontSize: 24, - compact: true, - ), - const Gap(4), - Padding( - padding: const EdgeInsets.only(bottom: 1), - child: BBText.title( - unit, - onSurface: true, - isBold: true, - fontSize: 12, - ), - ), - ], - ), - if (fiatCurrency != null) ...[ - Row( + Expanded( + flex: 9, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - BBText.bodySmall( - '~$fiatAmt', + const Gap(8), + BBText.titleLarge( + name ?? fingerprint, onSurface: true, - fontSize: 12, + fontSize: 20, + compact: true, ), const Gap(4), - Padding( - padding: const EdgeInsets.only(bottom: 1), + Opacity( + opacity: 0.7, child: BBText.bodySmall( - fiatCurrency.shortName.toUpperCase(), + walletStr, onSurface: true, + isBold: true, fontSize: 12, ), ), + const Spacer(), + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + BBText.titleLarge( + balance, + onSurface: true, + isBold: true, + fontSize: 24, + compact: true, + ), + const Gap(4), + Padding( + padding: const EdgeInsets.only(bottom: 1), + child: BBText.title( + unit, + onSurface: true, + isBold: true, + fontSize: 12, + ), + ), + ], + ), + if (fiatCurrency != null) ...[ + Row( + children: [ + BBText.bodySmall( + '~$fiatAmt', + onSurface: true, + fontSize: 12, + ), + const Gap(4), + Padding( + padding: const EdgeInsets.only(bottom: 1), + child: BBText.bodySmall( + fiatCurrency.shortName.toUpperCase(), + onSurface: true, + fontSize: 12, + ), + ), + ], + ), + ], + const Gap(4), ], ), - ], - const Gap(4), + ), + Expanded( + flex: 1, + child: !wallet.vaultBackupTested || + !wallet.physicalBackupTested + ? IconButton( + onPressed: () => context.push( + '/wallet-settings/backup-settings', + extra: wallet.id, + ), + icon: Icon( + Icons.warning_rounded, + color: context.colour.error, + size: 20, + )) + : const SizedBox.shrink(), + ), ], - ), + ) ], ), ), @@ -1157,12 +1162,7 @@ class HomeWarnings extends StatelessWidget { children: [ for (final w in warnings) WarningBanner( - onTap: () { - context.push( - w.route ?? '/home', - extra: w.walletBloc.id, - ); - }, + onTap: () {}, info: w.info, ), if (!keyServerUp) From 64ecc190cb6078ccbdab9b76c7e9d5e080a02784 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Wed, 26 Feb 2025 19:51:22 -0500 Subject: [PATCH 337/401] refactor(HomeState): simplify backup warning message --- lib/home/bloc/home_state.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/home/bloc/home_state.dart b/lib/home/bloc/home_state.dart index 1dafe664e..a6fbc955b 100644 --- a/lib/home/bloc/home_state.dart +++ b/lib/home/bloc/home_state.dart @@ -422,7 +422,7 @@ class HomeState with _$HomeState { if (walletNeedingBackup != null) { warnings.add( ( - info: 'Backup needed to be tested! Tap to test.', + info: 'Backup needed to be tested!', walletBloc: walletNeedingBackup, ), ); From c84bf70ee54acd41acfff614b0a28e4894cf5aab Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Thu, 27 Feb 2025 09:07:07 -0500 Subject: [PATCH 338/401] refactor(routes): update backup key route path for consistency --- lib/recoverbull/backup_settings.dart | 2 +- lib/routes.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/recoverbull/backup_settings.dart b/lib/recoverbull/backup_settings.dart index 554ee510f..8b4871332 100644 --- a/lib/recoverbull/backup_settings.dart +++ b/lib/recoverbull/backup_settings.dart @@ -131,7 +131,7 @@ class _Screen extends StatelessWidget { label: "View/Delete Backup Key", onPressed: () => { context.push( - '/wallet-settings/backup-settings/backup-key', + '/wallet-settings/backup-settings/key', extra: context.read().state.wallet.id, ), }, diff --git a/lib/routes.dart b/lib/routes.dart index ad7eeb851..cf3906455 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -278,7 +278,7 @@ GoRouter setupRouter() => GoRouter( }, ), GoRoute( - path: 'backup-key', + path: 'key', builder: (context, state) { return BackupKeyPage( wallet: state.extra! as String, From 17eaec7c585916a4f3d58a1ea83e8d76d028e704 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Thu, 27 Feb 2025 09:07:19 -0500 Subject: [PATCH 339/401] feat(KeychainState): add cooldown logic for backup key requests --- lib/recoverbull/bloc/keychain_state.dart | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/lib/recoverbull/bloc/keychain_state.dart b/lib/recoverbull/bloc/keychain_state.dart index db2f379ee..28f9f2085 100644 --- a/lib/recoverbull/bloc/keychain_state.dart +++ b/lib/recoverbull/bloc/keychain_state.dart @@ -41,6 +41,8 @@ class KeychainState with _$KeychainState { @Default([]) List shuffledNumbers, @Default('') String error, @Default(KeyChainPageState.enter) KeyChainPageState originalPageState, + DateTime? lastRequestTime, + int? cooldownMinutes, }) = _KeychainState; const KeychainState._(); @@ -82,4 +84,18 @@ class KeychainState with _$KeychainState { bool get canRecoverWithBckupKey => backupId.isNotEmpty && !loading; bool get canDeleteKey => backupId.isNotEmpty && keyServerUp && !loading; bool validateSecret(String secret) => commonPasswordsTop1000.contains(secret); + + bool get isInCooldown { + if (lastRequestTime == null || cooldownMinutes == null) return false; + final cooldownEnd = + lastRequestTime!.add(Duration(minutes: cooldownMinutes!)); + return DateTime.now().isBefore(cooldownEnd); + } + + int? get remainingCooldownSeconds { + if (!isInCooldown) return null; + final cooldownEnd = + lastRequestTime!.add(Duration(minutes: cooldownMinutes!)); + return cooldownEnd.difference(DateTime.now()).inSeconds; + } } From 99c0cb3972f61b34d97f0cd0d8a4437677c6abeb Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Thu, 27 Feb 2025 09:07:28 -0500 Subject: [PATCH 340/401] feat(KeychainCubit): implement cooldown handling for key server status checks --- lib/recoverbull/bloc/keychain_cubit.dart | 68 +++++++++++++++++++++--- 1 file changed, 60 insertions(+), 8 deletions(-) diff --git a/lib/recoverbull/bloc/keychain_cubit.dart b/lib/recoverbull/bloc/keychain_cubit.dart index c37f16761..3c97f731f 100644 --- a/lib/recoverbull/bloc/keychain_cubit.dart +++ b/lib/recoverbull/bloc/keychain_cubit.dart @@ -39,18 +39,69 @@ class KeychainCubit extends Cubit { } Future keyServerStatus() async { + if (state.isInCooldown) { + emit(state.copyWith( + keyServerUp: false, + error: + 'Rate limited. Please wait ${state.remainingCooldownSeconds} seconds.', + loading: false, + )); + return; + } + + emit(state.copyWith(loading: true, error: '')); + if (!isClosed) { try { final info = await _keyService.serverInfo(); - final isUp = info.cooldown <= 1; - - if (isUp != state.keyServerUp) { - emit(state.copyWith(keyServerUp: isUp)); - } + debugPrint('Server info: $info'); + emit(state.copyWith( + keyServerUp: true, + loading: false, + lastRequestTime: null, + cooldownMinutes: null, + error: '', + )); } catch (e) { - debugPrint('Server status check failed: $e'); - if (state.keyServerUp) { - emit(state.copyWith(keyServerUp: false)); + final errorStr = e.toString(); + + if (errorStr.contains('Failed host lookup') || + errorStr.contains('SocketException') || + errorStr.contains('Connection refused')) { + debugPrint('Connection issue: $errorStr'); + emit(state.copyWith( + keyServerUp: false, + loading: false, + error: + 'Unable to reach key server. This could be due to network issues or the server may be temporarily unavailable.', + )); + } else if (errorStr.contains('429')) { + final cooldownMatch = RegExp(r'cooldown: (\d+)').firstMatch(errorStr); + final dateMatch = + RegExp(r'requestedAt: ([^,\)]+)').firstMatch(errorStr); + + final cooldownMinutes = cooldownMatch != null + ? int.tryParse(cooldownMatch.group(1) ?? '0') + : 1; + + final requestedAt = dateMatch != null + ? DateTime.tryParse(dateMatch.group(1) ?? '') + : DateTime.now(); + + emit(state.copyWith( + keyServerUp: true, // Server is reachable but rate-limited + loading: false, + lastRequestTime: requestedAt ?? DateTime.now(), + cooldownMinutes: cooldownMinutes, + error: 'Rate limited. Please wait $cooldownMinutes minutes.', + )); + } else { + debugPrint('Server status check failed: $e'); + emit(state.copyWith( + keyServerUp: false, + loading: false, + error: 'Key server is not responding. Please try again later.', + )); } } } @@ -95,6 +146,7 @@ class KeychainCubit extends Cubit { return; } if (!await _ensureServerStatus()) return; + print('_ensureServerStatus'); // if (state.secret.length < pinMin) { // state.inputType == KeyChainInputType.pin // ? emit( From 3d960d302e137cf778aa139a7f5aa562e6bc1701 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Thu, 27 Feb 2025 09:07:38 -0500 Subject: [PATCH 341/401] refactor(KeychainPage): update backup key route path for clarity --- lib/recoverbull/keychain_page.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/recoverbull/keychain_page.dart b/lib/recoverbull/keychain_page.dart index c6be67d7f..821c662a5 100644 --- a/lib/recoverbull/keychain_page.dart +++ b/lib/recoverbull/keychain_page.dart @@ -161,7 +161,7 @@ class _Screen extends StatelessWidget { state.backupKey.isNotEmpty) { if (state.pageState == KeyChainPageState.download) { context.push( - '/wallet-settings/backup-settings/backup-key/options', + '/wallet-settings/backup-settings/key/options', extra: (state.backupKey, backup), ); } else { From b312f440889ca6d9f0a5dff38085d47ab2ce5918 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Thu, 27 Feb 2025 09:10:08 -0500 Subject: [PATCH 342/401] refactor(KeychainPage): adjust bottom child height for improved layout --- lib/recoverbull/keychain_page.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/recoverbull/keychain_page.dart b/lib/recoverbull/keychain_page.dart index 821c662a5..dfa847bb4 100644 --- a/lib/recoverbull/keychain_page.dart +++ b/lib/recoverbull/keychain_page.dart @@ -251,7 +251,7 @@ class _EnterPage extends StatelessWidget { @override Widget build(BuildContext context) { return StackedPage( - bottomChildHeight: MediaQuery.of(context).size.height * 0.12, + bottomChildHeight: MediaQuery.of(context).size.height * 0.11, bottomChild: _SetButton(inputType: inputType), child: Padding( key: ValueKey('enter$inputType'), @@ -285,7 +285,7 @@ class _ConfirmPage extends StatelessWidget { Widget build(BuildContext context) { return StackedPage( bottomChild: _ConfirmButton(inputType: inputType), - bottomChildHeight: MediaQuery.of(context).size.height * 0.12, + bottomChildHeight: MediaQuery.of(context).size.height * 0.11, child: SingleChildScrollView( key: ValueKey('confirm$inputType'), child: Padding( @@ -320,7 +320,7 @@ class _RecoveryPage extends StatelessWidget { @override Widget build(BuildContext context) { return StackedPage( - bottomChildHeight: MediaQuery.of(context).size.height * 0.18, + bottomChildHeight: MediaQuery.of(context).size.height * 0.16, bottomChild: _RecoverButton(inputType: inputType), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 32), From 392d5817d08bf39bb819f2f4ffac1fa3cd7edb65 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Thu, 27 Feb 2025 09:10:29 -0500 Subject: [PATCH 343/401] refactor(KeychainPage): adjust bottom child height for better layout --- lib/recoverbull/keychain_page.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/recoverbull/keychain_page.dart b/lib/recoverbull/keychain_page.dart index dfa847bb4..b75e2d78f 100644 --- a/lib/recoverbull/keychain_page.dart +++ b/lib/recoverbull/keychain_page.dart @@ -370,7 +370,7 @@ class _DeletePage extends StatelessWidget { @override Widget build(BuildContext context) { return StackedPage( - bottomChildHeight: MediaQuery.of(context).size.height * 0.15, + bottomChildHeight: MediaQuery.of(context).size.height * 0.11, bottomChild: _DeleteButton(inputType: inputType), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 32), From 14c3802d88df7020f7288cab4eaedf78274e6ab2 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Thu, 27 Feb 2025 09:11:18 -0500 Subject: [PATCH 344/401] refactor(KeychainPage): replace FilledButton with BBButton --- lib/recoverbull/keychain_page.dart | 89 ++++++------------------------ 1 file changed, 18 insertions(+), 71 deletions(-) diff --git a/lib/recoverbull/keychain_page.dart b/lib/recoverbull/keychain_page.dart index b75e2d78f..b8bd6ea01 100644 --- a/lib/recoverbull/keychain_page.dart +++ b/lib/recoverbull/keychain_page.dart @@ -697,26 +697,13 @@ class _RecoverButton extends StatelessWidget { ), ], const Gap(8), - FilledButton( - onPressed: state.loading - ? null - : () => context.read().clickRecover(), - style: FilledButton.styleFrom( - backgroundColor: _getButtonColor(context, canRecover), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - child: state.loading - ? const SizedBox( - height: 20, - width: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - valueColor: AlwaysStoppedAnimation(Colors.white), - ), - ) - : _buildButtonContent(context), + BBButton.withColour( + fillWidth: true, + label: 'Recover with ${_getInputTypeText()}', + leftIcon: Icons.arrow_forward_rounded, + disabled: !canRecover, + loading: state.loading, + onPressed: () => context.read().clickRecover(), ), ], ); @@ -724,27 +711,6 @@ class _RecoverButton extends StatelessWidget { ); } - Widget _buildButtonContent(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - 'Recover with ${_getInputTypeText()}', - style: context.font.bodyMedium!.copyWith( - color: Colors.white, - fontWeight: FontWeight.w900, - ), - ), - const SizedBox(width: 8), - const Icon( - Icons.arrow_forward, - color: Colors.white, - size: 16, - ), - ], - ); - } - void _switchInputType(BuildContext context) { final cubit = context.read(); final newType = inputType == KeyChainInputType.pin @@ -760,13 +726,6 @@ class _RecoverButton extends StatelessWidget { ); } - Color _getButtonColor(BuildContext context, bool canRecover) { - if (inputType == KeyChainInputType.backupKey || canRecover) { - return context.colour.shadow; - } - return context.colour.surface; - } - String _getSwitchButtonText() { switch (inputType) { case KeyChainInputType.pin: @@ -925,7 +884,7 @@ class _SuccessDialog extends StatelessWidget { const Gap(8), BBText.bodySmall(message, textAlign: TextAlign.center), const Gap(24), - BBButton.big( + BBButton.withColour( label: 'Continue', onPressed: () { Navigator.of(context).pop(); @@ -946,6 +905,7 @@ class _ErrorDialog extends StatelessWidget { @override Widget build(BuildContext context) { return Dialog( + backgroundColor: context.colour.primaryContainer, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), child: Padding( padding: const EdgeInsets.all(24), @@ -954,34 +914,21 @@ class _ErrorDialog extends StatelessWidget { children: [ Icon( Icons.error_outline, - color: context.colour.error, + color: context.colour.primary, size: 48, ), const Gap(16), - BBText.title( - isRecovery ? 'Recovery Failed' : 'Backup Failed', - textAlign: TextAlign.center, - isBold: true, - ), + BBText.title(isRecovery ? 'Recovery failed' : 'Backup failed', + textAlign: TextAlign.center, isBold: true), const Gap(8), BBText.bodySmall(error, textAlign: TextAlign.center), const Gap(24), - FilledButton( - onPressed: () => Navigator.of(context).pop(), - style: FilledButton.styleFrom( - backgroundColor: context.colour.shadow, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - child: Text( - 'Close', - style: context.font.bodyMedium!.copyWith( - color: Colors.white, - fontWeight: FontWeight.w900, - ), - ), - ), + BBButton.withColour( + label: 'Continue', + onPressed: () { + Navigator.of(context).pop(); + }, + ) ], ), ), From 5b38eb3277ebfb589e8307890bc6bc5459df893d Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Thu, 27 Feb 2025 09:11:25 -0500 Subject: [PATCH 345/401] fix(KeychainPage): update error handling to check key server status --- lib/recoverbull/keychain_page.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/recoverbull/keychain_page.dart b/lib/recoverbull/keychain_page.dart index b8bd6ea01..dfbe5cac3 100644 --- a/lib/recoverbull/keychain_page.dart +++ b/lib/recoverbull/keychain_page.dart @@ -172,7 +172,7 @@ class _Screen extends StatelessWidget { } } - if (state.hasError) { + if (state.hasError && state.keyServerUp) { showDialog( context: context, barrierDismissible: false, From 158129b2e91bb0315b0fb3c4107db631d2d4b642 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Thu, 27 Feb 2025 09:12:53 -0500 Subject: [PATCH 346/401] refactor(EncryptedVaultBackupPage): remove unused KeychainCubit and related key server status checks --- lib/recoverbull/encrypted_vault_backup.dart | 31 +++++---------------- 1 file changed, 7 insertions(+), 24 deletions(-) diff --git a/lib/recoverbull/encrypted_vault_backup.dart b/lib/recoverbull/encrypted_vault_backup.dart index 6c58057ca..8d03a0152 100644 --- a/lib/recoverbull/encrypted_vault_backup.dart +++ b/lib/recoverbull/encrypted_vault_backup.dart @@ -41,19 +41,15 @@ class EncryptedVaultBackupPage extends StatefulWidget { class _EncryptedVaultBackupPageState extends State { late final BackupSettingsCubit _cubit; - late final KeychainCubit _keychainCubit; - @override void initState() { super.initState(); _cubit = createBackupSettingsCubit(walletId: widget.wallet); - _keychainCubit = KeychainCubit(); } @override void dispose() { _cubit.close(); - _keychainCubit.close(); super.dispose(); } @@ -61,25 +57,13 @@ class _EncryptedVaultBackupPageState extends State { BuildContext context, BackupProvider provider, ) async { - await _keychainCubit.keyServerStatus(); - - final keyServerUp = _keychainCubit.state.keyServerUp; - - if (keyServerUp) { - switch (provider) { - case BackupProvider.googleDrive: - await _cubit.saveGoogleDriveBackup(); - case BackupProvider.iCloud: - debugPrint('iCloud backup'); - case BackupProvider.custom: - _cubit.saveFileSystemBackup(); - } - } else { - ScaffoldMessenger.of(context).showSnackBar( - context.showToast( - 'Key server is down. Please try backing up again later', - ), - ); + switch (provider) { + case BackupProvider.googleDrive: + await _cubit.saveGoogleDriveBackup(); + case BackupProvider.iCloud: + debugPrint('iCloud backup'); + case BackupProvider.custom: + _cubit.saveFileSystemBackup(); } } @@ -88,7 +72,6 @@ class _EncryptedVaultBackupPageState extends State { return MultiBlocProvider( providers: [ BlocProvider.value(value: _cubit), - BlocProvider.value(value: _keychainCubit), ], child: BlocConsumer( listenWhen: (previous, current) => From ecea048ece5220b806f88787868909079879f847 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Thu, 27 Feb 2025 09:13:31 -0500 Subject: [PATCH 347/401] refactor(RecoveredBackupInfoPage): rename cubit variable and replace FilledButton with BBButton --- lib/recoverbull/encrypted_vault_backup.dart | 47 ++++++--------------- 1 file changed, 12 insertions(+), 35 deletions(-) diff --git a/lib/recoverbull/encrypted_vault_backup.dart b/lib/recoverbull/encrypted_vault_backup.dart index 8d03a0152..a5753a8d6 100644 --- a/lib/recoverbull/encrypted_vault_backup.dart +++ b/lib/recoverbull/encrypted_vault_backup.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:bb_mobile/_ui/app_bar.dart'; +import 'package:bb_mobile/_ui/components/button.dart'; import 'package:bb_mobile/_ui/components/text.dart'; import 'package:bb_mobile/_ui/toast.dart'; import 'package:bb_mobile/recoverbull/bloc/backup_settings_cubit.dart'; @@ -391,17 +392,18 @@ class RecoveredBackupInfoPage extends StatefulWidget { } class _RecoveredBackupInfoPageState extends State { - late final BackupSettingsCubit _cubit; + late final BackupSettingsCubit _backupSettingsCubit; @override void initState() { super.initState(); - _cubit = createBackupSettingsCubit(); + _backupSettingsCubit = createBackupSettingsCubit(); + context.read().keyServerStatus(); } @override void dispose() { - _cubit.close(); + _backupSettingsCubit.close(); super.dispose(); } @@ -476,7 +478,7 @@ class _RecoveredBackupInfoPageState extends State { } return BlocProvider.value( - value: _cubit, + value: _backupSettingsCubit, child: BlocConsumer( listenWhen: (previous, current) => previous.errorLoadingBackups != current.errorLoadingBackups || @@ -487,7 +489,7 @@ class _RecoveredBackupInfoPageState extends State { ScaffoldMessenger.of(context).showSnackBar( context.showToast(state.errorLoadingBackups), ); - _cubit.clearError(); + _backupSettingsCubit.clearError(); return; } }, @@ -557,7 +559,10 @@ class _RecoveredBackupInfoPageState extends State { ), ), const Gap(20), - FilledButton( + BBButton.withColour( + leftIcon: Icons.arrow_forward_rounded, + label: 'Decrypt Backup', + disabled: state.loadingBackups, onPressed: () => { context.push( '/wallet-settings/backup-settings/keychain', @@ -568,35 +573,7 @@ class _RecoveredBackupInfoPageState extends State { ), ), }, - style: FilledButton.styleFrom( - backgroundColor: context.colour.shadow, - padding: const EdgeInsets.symmetric( - horizontal: 24, - vertical: 16, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - 'Decrypt Backup', - style: context.font.bodyMedium!.copyWith( - color: Colors.white, - fontWeight: FontWeight.w900, - ), - ), - const Gap(8), - const Icon( - Icons.arrow_forward, - color: Colors.white, - size: 20, - ), - ], - ), - ), + ) ], ), ), From adca71dd891847de576affcbb6c1cbceaebd1eec Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Thu, 27 Feb 2025 09:16:38 -0500 Subject: [PATCH 348/401] reafactor: implemented server offline error messages for backup & recover --- lib/recoverbull/backup_settings.dart | 309 ++++++++++++++++----------- 1 file changed, 181 insertions(+), 128 deletions(-) diff --git a/lib/recoverbull/backup_settings.dart b/lib/recoverbull/backup_settings.dart index 8b4871332..82c192e0b 100644 --- a/lib/recoverbull/backup_settings.dart +++ b/lib/recoverbull/backup_settings.dart @@ -1,7 +1,10 @@ import 'package:bb_mobile/_ui/app_bar.dart'; import 'package:bb_mobile/_ui/components/button.dart'; import 'package:bb_mobile/_ui/components/text.dart'; +import 'package:bb_mobile/_ui/toast.dart'; import 'package:bb_mobile/recoverbull/bloc/backup_settings_cubit.dart'; +import 'package:bb_mobile/recoverbull/bloc/keychain_cubit.dart'; +import 'package:bb_mobile/recoverbull/bloc/keychain_state.dart'; import 'package:bb_mobile/styles.dart'; import 'package:bb_mobile/wallet/bloc/wallet_bloc.dart'; import 'package:flutter/material.dart'; @@ -31,6 +34,9 @@ class _BackupSettingsState extends State { create: (BuildContext context) => createBackupSettingsCubit(walletId: widget.wallet), ), + BlocProvider( + create: (context) => KeychainCubit(), + ), ], child: Scaffold( appBar: AppBar( @@ -144,82 +150,103 @@ class _Screen extends StatelessWidget { } } -class BackupOptionsScreen extends StatelessWidget { +class BackupOptionsScreen extends StatefulWidget { const BackupOptionsScreen({super.key, required this.wallet}); final String wallet; + @override + State createState() => _BackupOptionsScreenState(); +} + +class _BackupOptionsScreenState extends State { + @override + void initState() { + context.read().keyServerStatus(); + super.initState(); + } + @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - backgroundColor: Colors.transparent, - elevation: 0, - automaticallyImplyLeading: false, - flexibleSpace: BBAppBar( - onBack: () => context.pop(), - text: '', - ), - ), - body: Padding( - padding: const EdgeInsets.all(20.0), - child: Column( - children: [ - const BBText.titleLarge( - 'Backup your wallet', - isBold: true, - fontSize: 25, + return BlocBuilder( + builder: (context, state) { + return Scaffold( + appBar: AppBar( + backgroundColor: Colors.transparent, + elevation: 0, + automaticallyImplyLeading: false, + flexibleSpace: BBAppBar( + onBack: () => context.pop(), + text: '', ), - const Gap(10), - RichText( - textAlign: TextAlign.center, - text: TextSpan( - children: [ - TextSpan( - text: 'Without a backup, you', - style: context.font.bodySmall!.copyWith( - fontSize: 12, - ), - ), - TextSpan( - text: ' will ', - style: context.font.bodySmall!.copyWith( - fontWeight: FontWeight.w900, - fontSize: 12, - ), + ), + body: Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + children: [ + const BBText.titleLarge( + 'Backup your wallet', + isBold: true, + fontSize: 25, + ), + const Gap(10), + RichText( + textAlign: TextAlign.center, + text: TextSpan( + children: [ + TextSpan( + text: 'Without a backup, you', + style: context.font.bodySmall!.copyWith( + fontSize: 12, + ), + ), + TextSpan( + text: ' will ', + style: context.font.bodySmall!.copyWith( + fontWeight: FontWeight.w900, + fontSize: 12, + ), + ), + TextSpan( + text: + 'eventually lose access to your money. It is critically important to do a backup.', + style: context.font.bodySmall!.copyWith( + fontSize: 12, + ), + ), + ], ), - TextSpan( - text: - 'eventually lose access to your money. It is critically important to do a backup.', - style: context.font.bodySmall!.copyWith( - fontSize: 12, - ), + ), + const Gap(20), + _renderBackupSetting( + title: 'Encrypted vault (quick and easy)', + description: + 'Your backup is encrypted with a secure key that cannot be cracked, and uploaded to your cloud account. The key to unlock your vault is stored in an anonymous password manager and accessible with your PIN.', + onTap: () => state.keyServerUp + ? context.push( + '/wallet-settings/backup-settings/backup-options/encrypted', + extra: widget.wallet, + ) + : ScaffoldMessenger.of(context).showSnackBar( + context.showToast( + '${state.error} Please try backing up again later', + ), + ), + ), + const Gap(20), + _renderBackupSetting( + title: 'Physical backup (take your time)', + description: + 'You have to write down 12 words on a piece of paper or engrave it in metal. Make sure not to lose it. If anybody ever finds those 12 words, they can steal your Bitcoin.', + onTap: () => context.push( + '/wallet-settings/backup-settings/backup-options/physical', + extra: widget.wallet, ), - ], - ), - ), - const Gap(20), - _renderBackupSetting( - title: 'Encrypted vault (quick and easy)', - description: - 'Your backup is encrypted with a secure key that cannot be cracked, and uploaded to your cloud account. The key to unlock your vault is stored in an anonymous password manager and accessible with your PIN.', - onTap: () => context.push( - '/wallet-settings/backup-settings/backup-options/encrypted', - extra: wallet, - ), + ), + ], ), - const Gap(20), - _renderBackupSetting( - title: 'Physical backup (take your time)', - description: - 'You have to write down 12 words on a piece of paper or engrave it in metal. Make sure not to lose it. If anybody ever finds those 12 words, they can steal your Bitcoin.', - onTap: () => context.push( - '/wallet-settings/backup-settings/backup-options/physical', - extra: wallet, - ), - ), - ], - ), - ), + ), + ); + }, ); } @@ -269,75 +296,101 @@ class BackupOptionsScreen extends StatelessWidget { } } -class RecoverOptionsScreen extends StatelessWidget { +class RecoverOptionsScreen extends StatefulWidget { const RecoverOptionsScreen({super.key}); + @override + State createState() => _RecoverOptionsScreenState(); +} + +class _RecoverOptionsScreenState extends State { + @override + void initState() { + context.read().keyServerStatus(); + super.initState(); + } + @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - backgroundColor: Colors.transparent, - elevation: 0, - automaticallyImplyLeading: false, - flexibleSpace: BBAppBar( - onBack: () => context.pop(), - text: '', - ), - ), - body: Padding( - padding: const EdgeInsets.all(20.0), - child: Column( - children: [ - const BBText.titleLarge( - 'Recover or test your backup', - isBold: true, - fontSize: 25, + return BlocBuilder( + builder: (context, state) { + return Scaffold( + appBar: AppBar( + backgroundColor: Colors.transparent, + elevation: 0, + automaticallyImplyLeading: false, + flexibleSpace: BBAppBar( + onBack: () => context.pop(), + text: '', ), - const Gap(10), - RichText( - textAlign: TextAlign.center, - text: TextSpan( - children: [ - TextSpan( - text: 'Testing your backup is ', - style: context.font.bodySmall!.copyWith(fontSize: 12), - ), - TextSpan( - text: 'critically important ', - style: context.font.bodySmall!.copyWith( - fontWeight: FontWeight.w900, - fontSize: 12, - ), + ), + body: Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + children: [ + const BBText.titleLarge( + 'Recover or test your backup', + isBold: true, + fontSize: 25, + ), + const Gap(10), + RichText( + textAlign: TextAlign.center, + text: TextSpan( + children: [ + TextSpan( + text: 'Testing your backup is ', + style: context.font.bodySmall!.copyWith(fontSize: 12), + ), + TextSpan( + text: 'critically important ', + style: context.font.bodySmall!.copyWith( + fontWeight: FontWeight.w900, + fontSize: 12, + ), + ), + TextSpan( + text: + 'to ensure you can recover your wallet if needed. Choose your recovery method below.', + style: context.font.bodySmall!.copyWith(fontSize: 12), + ), + ], ), - TextSpan( - text: - 'to ensure you can recover your wallet if needed. Choose your recovery method below.', - style: context.font.bodySmall!.copyWith(fontSize: 12), + ), + const Gap(20), + _renderBackupSetting( + title: 'Encrypted vault', + description: + "Restore your wallet using the encrypted backup stored in your cloud account. You'll need your PIN to access the decryption key from the password manager.", + onTap: () => state.keyServerUp + ? context.push( + '/wallet-settings/backup-settings/recover-options/encrypted', + ) + : { + ScaffoldMessenger.of(context).showSnackBar( + context.showToast( + '${state.error} Please try again later!', + ), + ), + context.push( + '/wallet-settings/backup-settings/recover-options/encrypted', + ) + }, + ), + const Gap(20), + _renderBackupSetting( + title: 'Physical backup', + description: + "Restore your wallet by entering the 12 words from your physical backup.", + onTap: () => context.push( + '/wallet-settings/backup-settings/recover-options/physical', ), - ], - ), + ), + ], ), - const Gap(20), - _renderBackupSetting( - title: 'Encrypted vault', - description: - "Restore your wallet using the encrypted backup stored in your cloud account. You'll need your PIN to access the decryption key from the password manager.", - onTap: () => context.push( - '/wallet-settings/backup-settings/recover-options/encrypted', - ), - ), - const Gap(20), - _renderBackupSetting( - title: 'Physical backup', - description: - "Restore your wallet by entering the 12 words from your physical backup.", - onTap: () => context.push( - '/wallet-settings/backup-settings/recover-options/physical', - ), - ), - ], - ), - ), + ), + ); + }, ); } From 2660824f0be4bb19a157998e4c006f4dbd56a91c Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Thu, 27 Feb 2025 09:17:03 -0500 Subject: [PATCH 349/401] refactor(BackupKeyPage): implement key server status check and update navigation logic --- lib/recoverbull/backup_key.dart | 86 +++++++++++++++++++-------------- 1 file changed, 49 insertions(+), 37 deletions(-) diff --git a/lib/recoverbull/backup_key.dart b/lib/recoverbull/backup_key.dart index 0e5f7da05..a2db0658f 100644 --- a/lib/recoverbull/backup_key.dart +++ b/lib/recoverbull/backup_key.dart @@ -77,44 +77,56 @@ class _BackupKeyPageState extends State { @override Widget build(BuildContext context) { - return BlocProvider.value( - value: _backupSettingsCubit, - child: BlocConsumer( - listenWhen: (previous, current) => - previous.errorLoadingBackups != current.errorLoadingBackups || - previous.latestRecoveredBackup != current.latestRecoveredBackup, - listener: (context, state) { - if (state.errorLoadingBackups.isNotEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - context.showToast(state.errorLoadingBackups), - ); - _backupSettingsCubit.clearError(); - return; - } - if (state.latestRecoveredBackup.isNotEmpty) { - context.push( - '/wallet-settings/backup-settings/backup-key/options', - extra: ('', state.latestRecoveredBackup), - ); - _backupSettingsCubit.clearError(); - } - }, - builder: (context, state) { - return Scaffold( - appBar: AppBar( - elevation: 0, - automaticallyImplyLeading: false, - centerTitle: true, - flexibleSpace: BBAppBar( - text: '', - onBack: () => context.pop(), - ), - ), - body: state.loadingBackups - ? const Center(child: CircularProgressIndicator()) - : _buildContent(context, state), + return BlocListener( + listener: (context, state) { + if (!state.keyServerUp) { + ScaffoldMessenger.of(context).showSnackBar( + context.showToast(state.error), ); - }, + } + }, + listenWhen: (previous, current) => + previous.keyServerUp != current.keyServerUp || + current.loading != previous.loading, + child: BlocProvider.value( + value: _backupSettingsCubit, + child: BlocConsumer( + listenWhen: (previous, current) => + previous.errorLoadingBackups != current.errorLoadingBackups || + previous.latestRecoveredBackup != current.latestRecoveredBackup, + listener: (context, state) { + if (state.errorLoadingBackups.isNotEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + context.showToast(state.errorLoadingBackups), + ); + _backupSettingsCubit.clearError(); + return; + } + if (state.latestRecoveredBackup.isNotEmpty) { + context.push( + '/wallet-settings/backup-settings/key/options', + extra: ('', state.latestRecoveredBackup), + ); + _backupSettingsCubit.clearError(); + } + }, + builder: (context, state) { + return Scaffold( + appBar: AppBar( + elevation: 0, + automaticallyImplyLeading: false, + centerTitle: true, + flexibleSpace: BBAppBar( + text: '', + onBack: () => context.pop(), + ), + ), + body: state.loadingBackups + ? const Center(child: CircularProgressIndicator()) + : _buildContent(context, state), + ); + }, + ), ), ); } From 4ed6df506e97c763b4d21808273cdca7ee5be41a Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Thu, 27 Feb 2025 09:18:27 -0500 Subject: [PATCH 350/401] refactor(BackupKeyPage): replaced buttons with BBButton --- lib/recoverbull/backup_key.dart | 91 +++++++++++---------------------- 1 file changed, 30 insertions(+), 61 deletions(-) diff --git a/lib/recoverbull/backup_key.dart b/lib/recoverbull/backup_key.dart index a2db0658f..78a17e992 100644 --- a/lib/recoverbull/backup_key.dart +++ b/lib/recoverbull/backup_key.dart @@ -1,4 +1,5 @@ import 'package:bb_mobile/_ui/app_bar.dart'; +import 'package:bb_mobile/_ui/components/button.dart'; import 'package:bb_mobile/_ui/components/text.dart'; import 'package:bb_mobile/_ui/toast.dart'; import 'package:bb_mobile/recoverbull/bloc/backup_settings_cubit.dart'; @@ -30,6 +31,7 @@ class _BackupKeyPageState extends State { void initState() { super.initState(); _backupSettingsCubit = createBackupSettingsCubit(walletId: widget.wallet); + context.read().keyServerStatus(); } @override @@ -162,46 +164,6 @@ class _BackupKeyInfoPage extends State { super.dispose(); } - ButtonStyle _getButtonStyle(BuildContext context, bool isEnabled) { - return FilledButton.styleFrom( - backgroundColor: - isEnabled ? context.colour.shadow : context.colour.surface, - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), - ); - } - - Widget _buildActionButton({ - required BuildContext context, - required bool isEnabled, - required String text, - required IconData icon, - required VoidCallback? onPressed, - }) { - return FilledButton( - onPressed: isEnabled ? onPressed : null, - style: _getButtonStyle(context, isEnabled), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - text, - style: context.font.bodyMedium!.copyWith( - color: isEnabled ? Colors.white : context.colour.onSurface, - fontWeight: FontWeight.w900, - ), - ), - const Gap(8), - Icon( - icon, - color: isEnabled ? Colors.white : context.colour.onSurface, - size: 20, - ), - ], - ), - ); - } - void _handleBackupAction(BuildContext context) { if (widget.backupKey.isNotEmpty) { _showBackupKeyDialog(context, widget.backupKey); @@ -221,7 +183,8 @@ class _BackupKeyInfoPage extends State { showDialog( context: context, builder: (context) => AlertDialog( - title: const BBText.titleLarge('Backup Key', isBold: true), + backgroundColor: context.colour.primaryContainer, + title: const BBText.title('Backup key', isBold: true), content: Row( children: [ Expanded( @@ -239,7 +202,7 @@ class _BackupKeyInfoPage extends State { context.showToast('Copied to clipboard'), ); }, - icon: const Icon(Icons.copy, color: Colors.black), + icon: Icon(Icons.copy, color: context.colour.primary), ), ], ), @@ -316,7 +279,8 @@ class _BackupKeyInfoPage extends State { builder: (context, keyState) { return BlocConsumer( listenWhen: (previous, current) => - previous.backupKey != current.backupKey, + previous.backupKey != current.backupKey || + previous.loadingBackups != current.loadingBackups, listener: (context, state) { if (state.errorLoadingBackups.isNotEmpty) { ScaffoldMessenger.of(context).showSnackBar( @@ -351,7 +315,7 @@ class _BackupKeyInfoPage extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ Text( - 'Latest Available Backup', + 'Latest available backup', style: context.font.titleLarge! .copyWith(fontWeight: FontWeight.w900), ), @@ -372,23 +336,25 @@ class _BackupKeyInfoPage extends State { ), ), const Gap(20), - _buildActionButton( - context: context, - isEnabled: keyState.keyServerUp, - text: widget.backupKey.isNotEmpty + BBButton.withColour( + fillWidth: true, + label: widget.backupKey.isNotEmpty ? 'View Backup Key' : 'Download Backup Key', - icon: widget.backupKey.isNotEmpty + disabled: !keyState.keyServerUp, + leftIcon: widget.backupKey.isNotEmpty ? CupertinoIcons.eye_fill : CupertinoIcons.cloud_download_fill, - onPressed: () => _handleBackupAction(context), + onPressed: () => !keyState.keyServerUp + ? _handleBackupAction(context) + : () {}, ), const Gap(10), - _buildActionButton( - context: context, - isEnabled: keyState.keyServerUp, - text: 'Delete Backup Key', - icon: CupertinoIcons.delete_right, + BBButton.withColour( + fillWidth: true, + label: 'Delete Backup Key', + disabled: !keyState.keyServerUp, + leftIcon: CupertinoIcons.delete_right, onPressed: () => context.push( '/wallet-settings/backup-settings/keychain', extra: ( @@ -400,17 +366,20 @@ class _BackupKeyInfoPage extends State { ), ), const Gap(10), - InkWell( - onTap: () => context + BBButton.text( + center: true, + centered: true, + isBlue: false, + onPressed: () => context .read() .recoverBackupKeyFromMnemonic( widget.recoveredBackup['path'] as String?, ), - child: const BBText.bodySmall( - 'Forgot your secret? Click to recover', - textAlign: TextAlign.center, - ), + fontSize: 12, + label: keyState.keyServerUp + ? 'Forgot your secret? Click to recover.' + : 'Server unreachable? Click to recover.', ), const Gap(10), ], From 2f1e8fd0e742e3d927f6a2205be1fb877cd25cde Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Thu, 27 Feb 2025 09:24:31 -0500 Subject: [PATCH 351/401] refactor(HomeState): update backup warning logic to require both physical and vault backups to be untested --- lib/home/bloc/home_state.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/home/bloc/home_state.dart b/lib/home/bloc/home_state.dart index a6fbc955b..9f454fa49 100644 --- a/lib/home/bloc/home_state.dart +++ b/lib/home/bloc/home_state.dart @@ -411,7 +411,7 @@ class HomeState with _$HomeState { } bool needsBackupWarning(Wallet wb) => - !wb.physicalBackupTested || !wb.vaultBackupTested; + !wb.physicalBackupTested && !wb.vaultBackupTested; final warnings = <({String info, Wallet walletBloc})>{}; final networkWallets = walletsFromNetwork(network); From 66c6c341349b4dba7c6d65df3e3407dcf2e2e11a Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Thu, 27 Feb 2025 10:01:43 -0500 Subject: [PATCH 352/401] refactor(BackupSettingsCubit): update newWallet call to include mainWallet type --- lib/recoverbull/bloc/backup_settings_cubit.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/recoverbull/bloc/backup_settings_cubit.dart b/lib/recoverbull/bloc/backup_settings_cubit.dart index 374e6cf0e..73b91a3ea 100644 --- a/lib/recoverbull/bloc/backup_settings_cubit.dart +++ b/lib/recoverbull/bloc/backup_settings_cubit.dart @@ -682,9 +682,9 @@ class BackupSettingsCubit extends Cubit { if (wallet == null) { return (null, Err('Failed to create wallet')); } - - final walletRepoErr = await _walletsStorageRepository - .newWallet(wallet.copyWith(vaultBackupTested: true)); + final walletRepoErr = await _walletsStorageRepository.newWallet( + wallet.copyWith( + vaultBackupTested: true, mainWallet: type == BBWalletType.main)); if (walletRepoErr != null && !walletRepoErr.message.toLowerCase().contains('exists')) { return (null, Err(walletRepoErr.toString())); From 8e9e5ca28d0e780b750ccf2af14f238aa1f10295 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Thu, 27 Feb 2025 10:21:30 -0500 Subject: [PATCH 353/401] refactor(KeychainCubit): remove commented-out validation logic for pin and password length --- lib/recoverbull/bloc/keychain_cubit.dart | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/lib/recoverbull/bloc/keychain_cubit.dart b/lib/recoverbull/bloc/keychain_cubit.dart index 3c97f731f..786e6474c 100644 --- a/lib/recoverbull/bloc/keychain_cubit.dart +++ b/lib/recoverbull/bloc/keychain_cubit.dart @@ -146,21 +146,6 @@ class KeychainCubit extends Cubit { return; } if (!await _ensureServerStatus()) return; - print('_ensureServerStatus'); - // if (state.secret.length < pinMin) { - // state.inputType == KeyChainInputType.pin - // ? emit( - // state.copyWith( - // error: 'pin should be at least $pinMin digits long', - // ), - // ) - // : emit( - // state.copyWith( - // error: 'password should be at least $pinMin characters long', - // ), - // ); - // return; - // } try { emit(state.copyWith(loading: true, error: '')); From 606025d8d604caea7f64dedc9f7ef1a81b83bc78 Mon Sep 17 00:00:00 2001 From: ethicnology Date: Wed, 26 Feb 2025 13:30:18 -0500 Subject: [PATCH 354/401] fix: Recover from Physical Backup OR Encrypted Vault --- lib/home/home_page.dart | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/lib/home/home_page.dart b/lib/home/home_page.dart index a58d71df0..6424b05cd 100644 --- a/lib/home/home_page.dart +++ b/lib/home/home_page.dart @@ -1034,8 +1034,19 @@ class HomeNoWalletsWithCreation extends StatelessWidget { ), const Gap(16), BBButton.text( - label: 'Recover wallet backup', + label: 'Recover wallet from Physical Backup', + centered: true, + onSurface: true, + isBlue: false, + fontSize: 11, + onPressed: () => context1.push('/import-main'), + ), + BBButton.text( + label: 'Recover wallet from Encrypted Vault', centered: true, + onSurface: true, + isBlue: false, + fontSize: 11, onPressed: () { context1.push( '/wallet-settings/backup-settings/recover-options/encrypted', @@ -1103,10 +1114,7 @@ class HomeNoWalletsView extends StatelessWidget { ), Text( 'OWN YOUR MONEY', - style: font.copyWith( - fontSize: 59, - height: 0.8, - ), + style: font.copyWith(fontSize: 59, height: 0.8), ), const Gap(8), SizedBox( @@ -1127,8 +1135,17 @@ class HomeNoWalletsView extends StatelessWidget { }, ), ), + const Gap(20), + BBButton.text( + label: 'Recover wallet from Physical Backup', + centered: true, + onSurface: true, + isBlue: false, + fontSize: 11, + onPressed: () => context.push('/import-main'), + ), BBButton.text( - label: 'Recover wallet backup', + label: 'Recover wallet from Encrypted Vault', centered: true, onSurface: true, isBlue: false, From 7c44acc26f8f9b1f0e4afefd428da69661b45253 Mon Sep 17 00:00:00 2001 From: ethicnology Date: Wed, 26 Feb 2025 14:56:41 -0500 Subject: [PATCH 355/401] fix: remove re-added enableGPUValidationMode = "1" --- ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme | 1 - 1 file changed, 1 deletion(-) diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index c469fe173..0533c716f 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -77,7 +77,6 @@ ignoresPersistentStateOnLaunch = "NO" debugDocumentVersioning = "YES" debugServiceExtension = "internal" - enableGPUValidationMode = "1" allowLocationSimulation = "YES"> From 44ef373fe8c4635b5029f3009d4dae476a49d23a Mon Sep 17 00:00:00 2001 From: ethicnology Date: Wed, 26 Feb 2025 14:59:10 -0500 Subject: [PATCH 356/401] refactor: rename WarningBanner `Key server is down` --- lib/home/home_page.dart | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/home/home_page.dart b/lib/home/home_page.dart index 6424b05cd..c5a24772b 100644 --- a/lib/home/home_page.dart +++ b/lib/home/home_page.dart @@ -1183,10 +1183,7 @@ class HomeWarnings extends StatelessWidget { info: w.info, ), if (!keyServerUp) - WarningBanner( - onTap: () {}, - info: 'Key server is down. Backup key is unavailable.', - ), + WarningBanner(onTap: () {}, info: 'Key server is down'), ], ); } From 5915d6330a0bd61db5b7cd36e10a45569f699476 Mon Sep 17 00:00:00 2001 From: ethicnology Date: Wed, 26 Feb 2025 18:10:06 -0500 Subject: [PATCH 357/401] refactor: remove scrambled PIN --- lib/recoverbull/bloc/keychain_cubit.dart | 11 +---------- lib/recoverbull/bloc/keychain_state.dart | 1 - lib/recoverbull/keychain_page.dart | 9 +-------- 3 files changed, 2 insertions(+), 19 deletions(-) diff --git a/lib/recoverbull/bloc/keychain_cubit.dart b/lib/recoverbull/bloc/keychain_cubit.dart index 786e6474c..4fa25674b 100644 --- a/lib/recoverbull/bloc/keychain_cubit.dart +++ b/lib/recoverbull/bloc/keychain_cubit.dart @@ -18,13 +18,9 @@ class KeychainCubit extends Cubit { late final KeyService _keyService; void _initialize() { - shuffleAndEmit(); if (keyServerUrl.isEmpty) { emit( - state.copyWith( - error: 'keychain api is not set', - keyServerUp: false, - ), + state.copyWith(error: 'keychain api is not set', keyServerUp: false), ); return; } @@ -278,11 +274,6 @@ class KeychainCubit extends Cubit { ); } - void shuffleAndEmit() { - final shuffledList = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]..shuffle(); - emit(state.copyWith(shuffledNumbers: shuffledList)); - } - void updateBackupKey(String value) { if (value == state.backupKey) return; // Avoid duplicate state emit(state.copyWith(backupKey: value, error: '')); diff --git a/lib/recoverbull/bloc/keychain_state.dart b/lib/recoverbull/bloc/keychain_state.dart index 28f9f2085..1b45732d2 100644 --- a/lib/recoverbull/bloc/keychain_state.dart +++ b/lib/recoverbull/bloc/keychain_state.dart @@ -38,7 +38,6 @@ class KeychainState with _$KeychainState { @Default('') String backupKey, @Default([]) List backupSalt, @Default(false) bool isSecretConfirmed, - @Default([]) List shuffledNumbers, @Default('') String error, @Default(KeyChainPageState.enter) KeyChainPageState originalPageState, DateTime? lastRequestTime, diff --git a/lib/recoverbull/keychain_page.dart b/lib/recoverbull/keychain_page.dart index dfbe5cac3..289176997 100644 --- a/lib/recoverbull/keychain_page.dart +++ b/lib/recoverbull/keychain_page.dart @@ -505,11 +505,6 @@ class KeyPad extends StatelessWidget { @override Widget build(BuildContext context) { - final shuffledNumbers = - context.select((KeychainCubit x) => x.state.shuffledNumbers); - final shuffledNumberButtonList = [ - for (final i in shuffledNumbers) NumberButton(text: i.toString()), - ]; return Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: GridView( @@ -518,9 +513,7 @@ class KeyPad extends StatelessWidget { gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3), children: [ - for (var i = 0; i < 9; i = i + 1) shuffledNumberButtonList[i], - Container(), - shuffledNumberButtonList[9], + for (var i = 0; i < 9; i = i + 1) NumberButton(text: i.toString()), ], ), ); From 5233200296574f3d08113a7edf2b0e43db58d466 Mon Sep 17 00:00:00 2001 From: ethicnology Date: Wed, 26 Feb 2025 15:01:33 -0500 Subject: [PATCH 358/401] refactor: remove cooldown check from keyServerStatus --- lib/recoverbull/bloc/keychain_cubit.dart | 53 ++---------------------- 1 file changed, 4 insertions(+), 49 deletions(-) diff --git a/lib/recoverbull/bloc/keychain_cubit.dart b/lib/recoverbull/bloc/keychain_cubit.dart index 4fa25674b..cda4bf24d 100644 --- a/lib/recoverbull/bloc/keychain_cubit.dart +++ b/lib/recoverbull/bloc/keychain_cubit.dart @@ -49,56 +49,11 @@ class KeychainCubit extends Cubit { if (!isClosed) { try { - final info = await _keyService.serverInfo(); - debugPrint('Server info: $info'); - emit(state.copyWith( - keyServerUp: true, - loading: false, - lastRequestTime: null, - cooldownMinutes: null, - error: '', - )); + await _keyService.serverInfo(); + emit(state.copyWith(keyServerUp: true)); } catch (e) { - final errorStr = e.toString(); - - if (errorStr.contains('Failed host lookup') || - errorStr.contains('SocketException') || - errorStr.contains('Connection refused')) { - debugPrint('Connection issue: $errorStr'); - emit(state.copyWith( - keyServerUp: false, - loading: false, - error: - 'Unable to reach key server. This could be due to network issues or the server may be temporarily unavailable.', - )); - } else if (errorStr.contains('429')) { - final cooldownMatch = RegExp(r'cooldown: (\d+)').firstMatch(errorStr); - final dateMatch = - RegExp(r'requestedAt: ([^,\)]+)').firstMatch(errorStr); - - final cooldownMinutes = cooldownMatch != null - ? int.tryParse(cooldownMatch.group(1) ?? '0') - : 1; - - final requestedAt = dateMatch != null - ? DateTime.tryParse(dateMatch.group(1) ?? '') - : DateTime.now(); - - emit(state.copyWith( - keyServerUp: true, // Server is reachable but rate-limited - loading: false, - lastRequestTime: requestedAt ?? DateTime.now(), - cooldownMinutes: cooldownMinutes, - error: 'Rate limited. Please wait $cooldownMinutes minutes.', - )); - } else { - debugPrint('Server status check failed: $e'); - emit(state.copyWith( - keyServerUp: false, - loading: false, - error: 'Key server is not responding. Please try again later.', - )); - } + debugPrint('Server status check failed: $e'); + emit(state.copyWith(keyServerUp: false)); } } } From 0729072108978e09a460391253b188653284ba74 Mon Sep 17 00:00:00 2001 From: ethicnology Date: Thu, 27 Feb 2025 11:13:43 -0500 Subject: [PATCH 359/401] refactor: main wallet default name should not contains fingerprint --- lib/_model/wallet.dart | 28 +- lib/_pkg/migrations/migration0_1to0_2.dart | 6 +- lib/_pkg/wallet/bdk/create.dart | 12 +- lib/_pkg/wallet/bdk/sensitive_create.dart | 8 +- lib/_pkg/wallet/create.dart | 489 --------------------- lib/_pkg/wallet/lwk/sensitive_create.dart | 2 +- lib/create/bloc/create_cubit.dart | 4 +- lib/import/bloc/import_cubit.dart | 4 +- 8 files changed, 20 insertions(+), 533 deletions(-) diff --git a/lib/_model/wallet.dart b/lib/_model/wallet.dart index e07f6283c..be190654f 100644 --- a/lib/_model/wallet.dart +++ b/lib/_model/wallet.dart @@ -354,34 +354,10 @@ class Wallet with _$Wallet { return str; } - String defaultNameString() { + String defaultName() { String str = ''; switch (type) { - case BBWalletType.main: - if (baseWalletType == BaseWalletType.Bitcoin) { - str = 'Secure:${id.substring(0, 5)}'; - } else { - str = 'Instant:${id.substring(0, 5)}'; - } - case BBWalletType.xpub: - str = 'Xpub:${id.substring(0, 5)}'; - case BBWalletType.words: - str = 'Imported:${id.substring(0, 5)}'; - case BBWalletType.coldcard: - str = 'Coldcard:${id.substring(0, 5)}'; - case BBWalletType.descriptors: - str = 'Imported Descriptor:${id.substring(0, 5)}'; - } - - return str; - } - - String creationName() { - String str = ''; - - switch (type) { - case BBWalletType.words: case BBWalletType.main: if (baseWalletType == BaseWalletType.Bitcoin) { str = 'Secure Bitcoin Wallet'; @@ -390,6 +366,8 @@ class Wallet with _$Wallet { } case BBWalletType.xpub: str = 'Xpub:${id.substring(0, 5)}'; + case BBWalletType.words: + str = 'Imported:${id.substring(0, 5)}'; case BBWalletType.coldcard: str = 'Coldcard:${id.substring(0, 5)}'; case BBWalletType.descriptors: diff --git a/lib/_pkg/migrations/migration0_1to0_2.dart b/lib/_pkg/migrations/migration0_1to0_2.dart index eb902703e..c107aecdf 100644 --- a/lib/_pkg/migrations/migration0_1to0_2.dart +++ b/lib/_pkg/migrations/migration0_1to0_2.dart @@ -362,8 +362,7 @@ Future> createLiquidWallet( network: BBNetwork.Mainnet, walletCreate: walletCreate, ); - final liquidWallet = - lw?.copyWith(name: lw.creationName(), mainWallet: true); + final liquidWallet = lw?.copyWith(name: lw.defaultName(), mainWallet: true); wallets.add(liquidWallet!); } @@ -378,8 +377,7 @@ Future> createLiquidWallet( network: BBNetwork.Testnet, walletCreate: walletCreate, ); - final liquidWallet = - lw?.copyWith(name: lw.creationName(), mainWallet: true); + final liquidWallet = lw?.copyWith(name: lw.defaultName(), mainWallet: true); wallets.add(liquidWallet!); await walletsStorageRepository.newWallet(liquidWallet); diff --git a/lib/_pkg/wallet/bdk/create.dart b/lib/_pkg/wallet/bdk/create.dart index 244f2ed67..9ecbc959f 100644 --- a/lib/_pkg/wallet/bdk/create.dart +++ b/lib/_pkg/wallet/bdk/create.dart @@ -133,7 +133,7 @@ class BDKCreate { addressIndex: const bdk.AddressIndex.peek(index: 0), ); wallet44 = wallet44.copyWith( - name: wallet44.defaultNameString(), + name: wallet44.defaultName(), lastGeneratedAddress: Address( address: firstAddress44.address.asString(), index: 0, @@ -165,7 +165,7 @@ class BDKCreate { addressIndex: const bdk.AddressIndex.peek(index: 0), ); wallet49 = wallet49.copyWith( - name: wallet49.defaultNameString(), + name: wallet49.defaultName(), lastGeneratedAddress: Address( address: firstAddress49.address.asString(), index: 0, @@ -197,7 +197,7 @@ class BDKCreate { addressIndex: const bdk.AddressIndex.peek(index: 0), ); wallet84 = wallet84.copyWith( - name: wallet84.defaultNameString(), + name: wallet84.defaultName(), lastGeneratedAddress: Address( address: firstAddress84.address.asString(), index: 0, @@ -299,7 +299,7 @@ class BDKCreate { addressIndex: const bdk.AddressIndex.peek(index: 0), ); wallet = wallet.copyWith( - name: wallet.defaultNameString(), + name: wallet.defaultName(), lastGeneratedAddress: Address( address: firstAddress.address.asString(), index: 0, @@ -308,7 +308,7 @@ class BDKCreate { ), ); - wallet = wallet.copyWith(name: wallet.defaultNameString()); + wallet = wallet.copyWith(name: wallet.defaultName()); _walletsRepository.removeBdkWallet(wallet.id); @@ -490,7 +490,7 @@ class BDKCreate { addressIndex: const bdk.AddressIndex.peek(index: 0), ); wallet = wallet.copyWith( - name: wallet.defaultNameString(), + name: wallet.defaultName(), lastGeneratedAddress: Address( address: firstAddress.address.asString(), index: 0, diff --git a/lib/_pkg/wallet/bdk/sensitive_create.dart b/lib/_pkg/wallet/bdk/sensitive_create.dart index 5a7a52e96..6251a408b 100644 --- a/lib/_pkg/wallet/bdk/sensitive_create.dart +++ b/lib/_pkg/wallet/bdk/sensitive_create.dart @@ -175,7 +175,7 @@ class BDKSensitiveCreate { addressIndex: const bdk.AddressIndex.peek(index: 0), ); wallet44 = wallet44.copyWith( - name: wallet44.defaultNameString(), + name: wallet44.defaultName(), lastGeneratedAddress: Address( address: firstAddress44.address.asString(), index: 0, @@ -206,7 +206,7 @@ class BDKSensitiveCreate { addressIndex: const bdk.AddressIndex.peek(index: 0), ); wallet49 = wallet49.copyWith( - name: wallet49.defaultNameString(), + name: wallet49.defaultName(), lastGeneratedAddress: Address( address: firstAddress49.address.asString(), index: 0, @@ -237,7 +237,7 @@ class BDKSensitiveCreate { addressIndex: const bdk.AddressIndex.peek(index: 0), ); wallet84 = wallet84.copyWith( - name: wallet84.defaultNameString(), + name: wallet84.defaultName(), lastGeneratedAddress: Address( address: firstAddress84.address.asString(), index: 0, @@ -378,7 +378,7 @@ class BDKSensitiveCreate { addressIndex: const bdk.AddressIndex.peek(index: 0), ); wallet = wallet.copyWith( - name: wallet.defaultNameString(), + name: wallet.defaultName(), lastGeneratedAddress: Address( address: firstAddress.address.asString(), index: 0, diff --git a/lib/_pkg/wallet/create.dart b/lib/_pkg/wallet/create.dart index 1e5f89096..164597755 100644 --- a/lib/_pkg/wallet/create.dart +++ b/lib/_pkg/wallet/create.dart @@ -85,492 +85,3 @@ class WalletCreate implements IWalletCreate { } } } - -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// - - -// class WalletCreate { -// WalletCreate({required WalletsRepository walletsRepository}) -// : _walletsRepository = walletsRepository; - -// final WalletsRepository _walletsRepository; - -// Future<(List?, Err?)> allFromColdCard( -// ColdCard coldCard, -// BBNetwork network, -// ) async { -// // create all 3 coldcard wallets and return only the one requested -// final fingerprint = coldCard.xfp!; -// final bdkNetwork = network == BBNetwork.Mainnet ? bdk.Network.Bitcoin : bdk.Network.Testnet; -// final ColdWallet coldWallet44 = coldCard.bip44!; -// final xpub44 = coldWallet44.xpub; -// final ColdWallet coldWallet49 = coldCard.bip49!; -// final xpub49 = coldWallet49.xpub; -// final ColdWallet coldWallet84 = coldCard.bip84!; -// final xpub84 = coldWallet84.xpub; - -// final networkPath = network == BBNetwork.Mainnet ? '0h' : '1h'; -// final accountPath = coldCard.account.toString() + 'h'; - -// final coldWallet44ExtendedPublic = '[$fingerprint/44h/$networkPath/$accountPath]$xpub44'; -// final coldWallet49ExtendedPublic = '[$fingerprint/49h/$networkPath/$accountPath]$xpub49'; -// final coldWallet84ExtendedPublic = '[$fingerprint/84h/$networkPath/$accountPath]$xpub84'; - -// final bdkXpub44 = await bdk.DescriptorPublicKey.fromString(coldWallet44ExtendedPublic); -// final bdkXpub49 = await bdk.DescriptorPublicKey.fromString(coldWallet49ExtendedPublic); -// final bdkXpub84 = await bdk.DescriptorPublicKey.fromString(coldWallet84ExtendedPublic); - -// final bdkDescriptor44External = await bdk.Descriptor.newBip44Public( -// publicKey: bdkXpub44, -// fingerPrint: fingerprint, -// network: bdkNetwork, -// keychain: bdk.KeychainKind.External, -// ); -// final bdkDescriptor44Internal = await bdk.Descriptor.newBip44Public( -// publicKey: bdkXpub44, -// fingerPrint: fingerprint, -// network: bdkNetwork, -// keychain: bdk.KeychainKind.Internal, -// ); -// final bdkDescriptor49External = await bdk.Descriptor.newBip49Public( -// publicKey: bdkXpub49, -// fingerPrint: fingerprint, -// network: bdkNetwork, -// keychain: bdk.KeychainKind.External, -// ); -// final bdkDescriptor49Internal = await bdk.Descriptor.newBip49Public( -// publicKey: bdkXpub49, -// fingerPrint: fingerprint, -// network: bdkNetwork, -// keychain: bdk.KeychainKind.Internal, -// ); -// final bdkDescriptor84External = await bdk.Descriptor.newBip84Public( -// publicKey: bdkXpub84, -// fingerPrint: fingerprint, -// network: bdkNetwork, -// keychain: bdk.KeychainKind.External, -// ); -// final bdkDescriptor84Internal = await bdk.Descriptor.newBip84Public( -// publicKey: bdkXpub84, -// fingerPrint: fingerprint, -// network: bdkNetwork, -// keychain: bdk.KeychainKind.Internal, -// ); - -// final wallet44HashId = -// createDescriptorHashId(await bdkDescriptor44External.asString()).substring(0, 12); -// var wallet44 = Wallet( -// id: wallet44HashId, -// externalPublicDescriptor: await bdkDescriptor44External.asString(), -// internalPublicDescriptor: await bdkDescriptor44Internal.asString(), -// mnemonicFingerprint: fingerprint, -// sourceFingerprint: fingerprint, -// network: network, -// type: BBWalletType.coldcard, -// scriptType: ScriptType.bip44, -// physicalBackupTested: true, -// baseWalletType: BaseWalletType.Bitcoin, -// ); -// final errBdk44 = await loadPublicBdkWallet(wallet44); -// if (errBdk44 != null) return (null, errBdk44); -// final (bdkWallet44, errLoading) = _walletsRepository.getBdkWallet(wallet44); -// if (errLoading != null) return (null, errLoading); -// final firstAddress44 = await bdkWallet44!.getAddress( -// addressIndex: const bdk.AddressIndex.peek(index: 0), -// ); -// wallet44 = wallet44.copyWith( -// name: wallet44.defaultNameString(), -// lastGeneratedAddress: Address( -// address: firstAddress44.address, -// index: 0, -// kind: AddressKind.deposit, -// state: AddressStatus.unused, -// ), -// ); - -// final wallet49HashId = -// createDescriptorHashId(await bdkDescriptor49External.asString()).substring(0, 12); -// var wallet49 = Wallet( -// id: wallet49HashId, -// externalPublicDescriptor: await bdkDescriptor49External.asString(), -// internalPublicDescriptor: await bdkDescriptor49Internal.asString(), -// mnemonicFingerprint: fingerprint, -// sourceFingerprint: fingerprint, -// network: network, -// type: BBWalletType.coldcard, -// scriptType: ScriptType.bip49, -// physicalBackupTested: true, -// baseWalletType: BaseWalletType.Bitcoin, -// ); -// final errBdk49 = await loadPublicBdkWallet(wallet49); -// if (errBdk49 != null) return (null, errBdk49); -// final (bdkWallet49, errLoading49) = _walletsRepository.getBdkWallet(wallet49); -// if (errLoading49 != null) return (null, errLoading49); -// final firstAddress49 = await bdkWallet49!.getAddress( -// addressIndex: const bdk.AddressIndex.peek(index: 0), -// ); -// wallet49 = wallet49.copyWith( -// name: wallet49.defaultNameString(), -// lastGeneratedAddress: Address( -// address: firstAddress49.address, -// index: 0, -// kind: AddressKind.deposit, -// state: AddressStatus.unused, -// ), -// ); - -// final wallet84HashId = -// createDescriptorHashId(await bdkDescriptor84External.asString()).substring(0, 12); -// var wallet84 = Wallet( -// id: wallet84HashId, -// externalPublicDescriptor: await bdkDescriptor84External.asString(), -// internalPublicDescriptor: await bdkDescriptor84Internal.asString(), -// mnemonicFingerprint: fingerprint, -// sourceFingerprint: fingerprint, -// network: network, -// type: BBWalletType.coldcard, -// scriptType: ScriptType.bip84, -// physicalBackupTested: true, -// baseWalletType: BaseWalletType.Bitcoin, -// ); -// final errBdk84 = await loadPublicBdkWallet(wallet84); -// if (errBdk84 != null) return (null, errBdk84); -// final (bdkWallet84, errLoading84) = _walletsRepository.getBdkWallet(wallet84); -// if (errLoading84 != null) return (null, errLoading84); -// final firstAddress84 = await bdkWallet84!.getAddress( -// addressIndex: const bdk.AddressIndex.peek(index: 0), -// ); -// wallet84 = wallet84.copyWith( -// name: wallet84.defaultNameString(), -// lastGeneratedAddress: Address( -// address: firstAddress84.address, -// index: 0, -// kind: AddressKind.deposit, -// state: AddressStatus.unused, -// ), -// ); - -// _walletsRepository.removeBdkWallet(wallet44); -// _walletsRepository.removeBdkWallet(wallet49); -// _walletsRepository.removeBdkWallet(wallet84); - -// if (firstAddress44.address == coldWallet44.first && -// firstAddress49.address == coldWallet49.first && -// firstAddress84.address == coldWallet84.first) -// return ([wallet44, wallet49, wallet84], null); -// else -// return ( -// null, -// Err('First Addresses Did Not Match!'), -// ); -// } - -// Future<(Wallet?, Err?)> oneFromSlip132Pub( -// String slip132Pub, -// ) async { -// try { -// final network = -// (slip132Pub.startsWith('t') || slip132Pub.startsWith('u') || slip132Pub.startsWith('v')) -// ? BBNetwork.Testnet -// : BBNetwork.Mainnet; -// final bdkNetwork = network == BBNetwork.Testnet ? bdk.Network.Testnet : bdk.Network.Bitcoin; -// final scriptType = slip132Pub.startsWith('x') || slip132Pub.startsWith('t') -// ? ScriptType.bip44 -// : slip132Pub.startsWith('y') || slip132Pub.startsWith('u') -// ? ScriptType.bip49 -// : ScriptType.bip84; -// final xPub = convertToXpubStr(slip132Pub); - -// bdk.Descriptor? internal; -// bdk.Descriptor? external; -// switch (scriptType) { -// case ScriptType.bip84: -// internal = await bdk.Descriptor.create( -// descriptor: 'wpkh($xPub/1/*)', -// network: bdkNetwork, -// ); -// external = await bdk.Descriptor.create( -// descriptor: 'wpkh($xPub/0/*)', -// network: bdkNetwork, -// ); -// case ScriptType.bip49: -// internal = await bdk.Descriptor.create( -// descriptor: 'sh(wpkh($xPub/1/*))', -// network: bdkNetwork, -// ); -// external = await bdk.Descriptor.create( -// descriptor: 'sh(wpkh($xPub/0/*))', -// network: bdkNetwork, -// ); -// case ScriptType.bip44: -// internal = await bdk.Descriptor.create( -// descriptor: 'pkh($xPub/1/*)', -// network: bdkNetwork, -// ); -// external = await bdk.Descriptor.create( -// descriptor: 'pkh($xPub/0/*)', -// network: bdkNetwork, -// ); -// } - -// final descHashId = createDescriptorHashId(await external.asString()).substring(0, 12); -// var wallet = Wallet( -// id: descHashId, -// externalPublicDescriptor: await external.asString(), -// internalPublicDescriptor: await internal.asString(), -// mnemonicFingerprint: 'Unknown', -// sourceFingerprint: 'Unknown', -// network: network, -// type: BBWalletType.xpub, -// scriptType: scriptType, -// physicalBackupTested: true, -// baseWalletType: BaseWalletType.Bitcoin, -// ); -// final errBdk = await loadPublicBdkWallet(wallet); -// if (errBdk != null) return (null, errBdk); -// final (bdkWallet, errLoading) = _walletsRepository.getBdkWallet(wallet); -// final firstAddress = await bdkWallet!.getAddress( -// addressIndex: const bdk.AddressIndex.peek(index: 0), -// ); -// wallet = wallet.copyWith( -// name: wallet.defaultNameString(), -// lastGeneratedAddress: Address( -// address: firstAddress.address, -// index: 0, -// kind: AddressKind.deposit, -// state: AddressStatus.unused, -// ), -// ); - -// wallet = wallet.copyWith(name: wallet.defaultNameString()); - -// _walletsRepository.removeBdkWallet(wallet); - -// return (wallet, null); -// } on Exception catch (e) { -// return ( -// null, -// Err( -// e.message, -// title: 'Error occurred while creating wallet', -// solution: 'Please try again.', -// ) -// ); -// } -// } - -// Future<(Wallet?, Err?)> oneFromXpubWithOrigin( -// String xpubWithOrigin, -// ) async { -// try { -// final network = (xpubWithOrigin.contains('tpub')) ? BBNetwork.Testnet : BBNetwork.Mainnet; -// final bdkNetwork = network == BBNetwork.Testnet ? bdk.Network.Testnet : bdk.Network.Bitcoin; -// final scriptType = xpubWithOrigin.contains('/44') -// ? ScriptType.bip44 -// : xpubWithOrigin.contains('/49') -// ? ScriptType.bip49 -// : ScriptType.bip84; -// bdk.Descriptor? internal; -// bdk.Descriptor? external; -// switch (scriptType) { -// case ScriptType.bip84: -// internal = await bdk.Descriptor.create( -// descriptor: 'wpkh($xpubWithOrigin/1/*)', -// network: bdkNetwork, -// ); -// external = await bdk.Descriptor.create( -// descriptor: 'wpkh($xpubWithOrigin/0/*)', -// network: bdkNetwork, -// ); -// case ScriptType.bip49: -// internal = await bdk.Descriptor.create( -// descriptor: 'sh(wpkh($xpubWithOrigin/1/*))', -// network: bdkNetwork, -// ); -// external = await bdk.Descriptor.create( -// descriptor: 'sh(wpkh($xpubWithOrigin/0/*))', -// network: bdkNetwork, -// ); -// case ScriptType.bip44: -// internal = await bdk.Descriptor.create( -// descriptor: 'pkh($xpubWithOrigin/1/*)', -// network: bdkNetwork, -// ); -// external = await bdk.Descriptor.create( -// descriptor: 'pkh($xpubWithOrigin/0/*)', -// network: bdkNetwork, -// ); -// } - -// final descHashId = createDescriptorHashId(await external.asString()).substring(0, 12); -// var wallet = Wallet( -// id: descHashId, -// externalPublicDescriptor: await external.asString(), -// internalPublicDescriptor: await internal.asString(), -// mnemonicFingerprint: fingerPrintFromXKeyDesc(xpubWithOrigin), -// sourceFingerprint: fingerPrintFromXKeyDesc(xpubWithOrigin), -// network: network, -// type: BBWalletType.xpub, -// scriptType: scriptType, -// physicalBackupTested: true, -// baseWalletType: BaseWalletType.Bitcoin, -// ); -// final errBdk = await loadPublicBdkWallet(wallet); -// if (errBdk != null) return (null, errBdk); -// final (bdkWallet, errLoading) = _walletsRepository.getBdkWallet(wallet); -// final firstAddress = await bdkWallet!.getAddress( -// addressIndex: const bdk.AddressIndex.peek(index: 0), -// ); -// wallet = wallet.copyWith( -// name: wallet.defaultNameString(), -// lastGeneratedAddress: Address( -// address: firstAddress.address, -// index: 0, -// kind: AddressKind.deposit, -// state: AddressStatus.unused, -// ), -// ); - -// return (wallet, null); -// } on Exception catch (e) { -// return ( -// null, -// Err( -// e.message, -// title: 'Error occurred while creating wallet', -// solution: 'Please try again.', -// ) -// ); -// } -// } - -// Future loadPublicBdkWallet( -// Wallet wallet, -// ) async { -// try { -// final network = -// wallet.network == BBNetwork.Testnet ? bdk.Network.Testnet : bdk.Network.Bitcoin; - -// final external = await bdk.Descriptor.create( -// descriptor: wallet.externalPublicDescriptor, -// network: network, -// ); -// final internal = await bdk.Descriptor.create( -// descriptor: wallet.internalPublicDescriptor, -// network: network, -// ); - -// final appDocDir = await getApplicationDocumentsDirectory(); -// final String dbDir = appDocDir.path + '/${wallet.getWalletStorageString()}'; - -// final dbConfig = bdk.DatabaseConfig.sqlite( -// config: bdk.SqliteDbConfiguration(path: dbDir), -// ); - -// final bdkWallet = await bdk.Wallet.create( -// descriptor: external, -// changeDescriptor: internal, -// network: network, -// databaseConfig: dbConfig, -// ); - -// final err = _walletsRepository.setBdkWallet(wallet, bdkWallet); -// if (err != null) return err; - -// return null; -// } on Exception catch (e) { -// return Err( -// e.message, -// title: 'Error occurred while creating wallet', -// solution: 'Please try again.', -// ); -// } -// } - -// Future loadPublicLwkWallet(Wallet wallet) async { -// try { -// final network = -// wallet.network == BBNetwork.LMainnet ? lwk.Network.Mainnet : lwk.Network.Testnet; - -// final appDocDir = await getApplicationDocumentsDirectory(); -// final String dbDir = '${appDocDir.path}/db'; - -// final w = await lwk.Wallet.create( -// network: network, -// dbPath: dbDir, -// descriptor: wallet.externalPublicDescriptor, -// ); - -// final err = _walletsRepository.setLwkWallet(wallet, w); -// if (err != null) return err; - -// return null; -// } on Exception catch (e) { -// return Err( -// e.message, -// title: 'Error occurred while creating wallet', -// solution: 'Please try again.', -// ); -// } -// } -// } - -// i "wpkh([2e9795fd/84'/1'/0']tpubDCHrHCLCpsLQLL7nbAaKM5WCZNy1uXwJoFin6xm2vHH8rgwUnErx7qQftrm2ayxALfLTgv8EvoJ2FL1pppBVyt1ciEtF8rbUngA…" - -// e "wpkh([2e9795fd/84'/1'/0']tpubDCHrHCLCpsLQLL7nbAaKM5WCZNy1uXwJoFin6xm2vHH8rgwUnErx7qQftrm2ayxALfLTgv8EvoJ2FL1pppBVyt1ciEtF8rbUngA…" diff --git a/lib/_pkg/wallet/lwk/sensitive_create.dart b/lib/_pkg/wallet/lwk/sensitive_create.dart index 2deb03f8b..deb646205 100644 --- a/lib/_pkg/wallet/lwk/sensitive_create.dart +++ b/lib/_pkg/wallet/lwk/sensitive_create.dart @@ -101,7 +101,7 @@ class LWKSensitiveCreate { // if (errLoading != null) return (null, errLoading); final firstAddress = await lwkWallet?.address(index: 0); wallet = wallet.copyWith( - name: wallet.defaultNameString(), + name: wallet.defaultName(), lastGeneratedAddress: Address( address: firstAddress?.confidential ?? '', standard: firstAddress?.standard ?? '', diff --git a/lib/create/bloc/create_cubit.dart b/lib/create/bloc/create_cubit.dart index 6e3d925fa..4df5f20c8 100644 --- a/lib/create/bloc/create_cubit.dart +++ b/lib/create/bloc/create_cubit.dart @@ -138,7 +138,7 @@ class CreateWalletCubit extends Cubit { if (state.mainWallet) wallet = wallet!.copyWith(mainWallet: true); var walletLabel = state.walletLabel ?? ''; - if (state.mainWallet) walletLabel = wallet!.creationName(); + if (state.mainWallet) walletLabel = wallet!.defaultName(); final updatedWallet = wallet!.copyWith(name: walletLabel); final ssErr = await _walletSensRepository.newSeed(seed: seed); @@ -219,7 +219,7 @@ class CreateWalletCubit extends Cubit { wallet = wallet!.copyWith(mainWallet: true); var walletLabel = state.walletLabel ?? ''; - if (state.mainWallet) walletLabel = wallet.creationName(); + if (state.mainWallet) walletLabel = wallet.defaultName(); final updatedWallet = wallet.copyWith(name: walletLabel); final wsErr = await _walletsStorageRepository.newWallet(updatedWallet); diff --git a/lib/import/bloc/import_cubit.dart b/lib/import/bloc/import_cubit.dart index 5a017c5b4..3b90af71b 100644 --- a/lib/import/bloc/import_cubit.dart +++ b/lib/import/bloc/import_cubit.dart @@ -639,7 +639,7 @@ class ImportWalletCubit extends Cubit { ); } var walletLabel = state.walletLabel ?? ''; - if (state.mainWallet) walletLabel = selectedWallet.creationName(); + if (state.mainWallet) walletLabel = selectedWallet.defaultName(); final secureWallet = selectedWallet.copyWith(name: walletLabel); final err = await _walletsStorageRepository.newWallet( @@ -700,7 +700,7 @@ class ImportWalletCubit extends Cubit { } var walletLabel = state.walletLabel ?? ''; - if (state.mainWallet) walletLabel = wallet.creationName(); + if (state.mainWallet) walletLabel = wallet.defaultName(); final updatedWallet = wallet.copyWith(name: walletLabel); final wsErr = await _walletsStorageRepository.newWallet(updatedWallet); From c572d7cf2472c4f3ad6e6fcdcf04eebddfa0132a Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Thu, 27 Feb 2025 11:40:00 -0500 Subject: [PATCH 360/401] fix: update keyServerUp stop loading status --- lib/recoverbull/bloc/keychain_cubit.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/recoverbull/bloc/keychain_cubit.dart b/lib/recoverbull/bloc/keychain_cubit.dart index cda4bf24d..1488cd20a 100644 --- a/lib/recoverbull/bloc/keychain_cubit.dart +++ b/lib/recoverbull/bloc/keychain_cubit.dart @@ -50,7 +50,7 @@ class KeychainCubit extends Cubit { if (!isClosed) { try { await _keyService.serverInfo(); - emit(state.copyWith(keyServerUp: true)); + emit(state.copyWith(keyServerUp: true, loading: false)); } catch (e) { debugPrint('Server status check failed: $e'); emit(state.copyWith(keyServerUp: false)); From f7bfae3e63c9de1575cf4dd6d0ea9376641ec827 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Thu, 27 Feb 2025 11:41:49 -0500 Subject: [PATCH 361/401] fix: update CardItem to check both vaultBackupTested and physicalBackupTested --- lib/home/home_page.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/home/home_page.dart b/lib/home/home_page.dart index c5a24772b..e2fcad9f9 100644 --- a/lib/home/home_page.dart +++ b/lib/home/home_page.dart @@ -495,7 +495,7 @@ class CardItem extends StatelessWidget { ), Expanded( flex: 1, - child: !wallet.vaultBackupTested || + child: !wallet.vaultBackupTested && !wallet.physicalBackupTested ? IconButton( onPressed: () => context.push( From a8e904a9db01b2918d8d897be7fe93074722391c Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Thu, 27 Feb 2025 13:59:34 -0500 Subject: [PATCH 362/401] fix: update BackupAlertBanner to show warning only if both backups are untested --- lib/wallet/wallet_page.dart | 2 +- lib/wallet/wallet_txs.dart | 20 +++----------------- 2 files changed, 4 insertions(+), 18 deletions(-) diff --git a/lib/wallet/wallet_page.dart b/lib/wallet/wallet_page.dart index 0f61a7756..053920b91 100644 --- a/lib/wallet/wallet_page.dart +++ b/lib/wallet/wallet_page.dart @@ -79,7 +79,7 @@ class _Screen extends StatelessWidget { children: [ const WalletHeader(), const ActionsRow(), - if (!physicalBackupTested || !vaultBackupTested) ...[ + if (!physicalBackupTested && !vaultBackupTested) ...[ const Gap(24), const BackupAlertBanner(), // const Gap(24), diff --git a/lib/wallet/wallet_txs.dart b/lib/wallet/wallet_txs.dart index 16b497339..5127a3104 100644 --- a/lib/wallet/wallet_txs.dart +++ b/lib/wallet/wallet_txs.dart @@ -235,31 +235,17 @@ class BackupAlertBanner extends StatelessWidget { final vaultBackupTested = context.select((WalletBloc x) => x.state.wallet.vaultBackupTested); - if (physicalBackupTested && vaultBackupTested) { - return const SizedBox.shrink(); - } - return Column( children: [ - if (!physicalBackupTested) - WarningBanner( - onTap: () { - context.push( - '/wallet-settings/backup-settings/backup-options/physical', - extra: wallet.id, - ); - }, - info: 'Physical backup not tested! Tap to test backup.', - ), - if (!vaultBackupTested) + if (!physicalBackupTested && !vaultBackupTested) WarningBanner( onTap: () { context.push( - '/wallet-settings/backup-settings/backup-options/encrypted', + '/wallet-settings/backup-settings/backup-options', extra: wallet.id, ); }, - info: 'Encrypted backup not tested! Tap to test backup.', + info: 'Backup needed to be tested! Tap to test backup.', ), ], ); From ced6390b186502a3ea8b397bda54baf1d9448ad0 Mon Sep 17 00:00:00 2001 From: ethicnology Date: Thu, 27 Feb 2025 14:25:08 -0500 Subject: [PATCH 363/401] refactor: rename warning --- lib/home/bloc/home_state.dart | 2 +- lib/wallet/wallet_txs.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/home/bloc/home_state.dart b/lib/home/bloc/home_state.dart index 9f454fa49..d7b03bbd9 100644 --- a/lib/home/bloc/home_state.dart +++ b/lib/home/bloc/home_state.dart @@ -422,7 +422,7 @@ class HomeState with _$HomeState { if (walletNeedingBackup != null) { warnings.add( ( - info: 'Backup needed to be tested!', + info: 'Backup needs to be tested!', walletBloc: walletNeedingBackup, ), ); diff --git a/lib/wallet/wallet_txs.dart b/lib/wallet/wallet_txs.dart index 5127a3104..d83580643 100644 --- a/lib/wallet/wallet_txs.dart +++ b/lib/wallet/wallet_txs.dart @@ -245,7 +245,7 @@ class BackupAlertBanner extends StatelessWidget { extra: wallet.id, ); }, - info: 'Backup needed to be tested! Tap to test backup.', + info: 'Backup needs to be tested! Tap to test', ), ], ); From d08fad734d0bcbccdd307518c3c37f3296494426 Mon Sep 17 00:00:00 2001 From: ethicnology Date: Thu, 27 Feb 2025 14:43:01 -0500 Subject: [PATCH 364/401] refactor: redirect properly to the physical backup test with words displayed at the beginning --- lib/recoverbull/backup_settings.dart | 7 +++++-- lib/routes.dart | 3 ++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/recoverbull/backup_settings.dart b/lib/recoverbull/backup_settings.dart index 82c192e0b..cc09abf43 100644 --- a/lib/recoverbull/backup_settings.dart +++ b/lib/recoverbull/backup_settings.dart @@ -297,7 +297,9 @@ class _BackupOptionsScreenState extends State { } class RecoverOptionsScreen extends StatefulWidget { - const RecoverOptionsScreen({super.key}); + const RecoverOptionsScreen({super.key, required this.wallet}); + + final String wallet; @override State createState() => _RecoverOptionsScreenState(); @@ -383,7 +385,8 @@ class _RecoverOptionsScreenState extends State { description: "Restore your wallet by entering the 12 words from your physical backup.", onTap: () => context.push( - '/wallet-settings/backup-settings/recover-options/physical', + '/wallet-settings/backup-settings/backup-options/physical', + extra: widget.wallet, ), ), ], diff --git a/lib/routes.dart b/lib/routes.dart index cf3906455..30b67479a 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -237,7 +237,8 @@ GoRouter setupRouter() => GoRouter( ), GoRoute( path: 'recover-options', - builder: (context, state) => const RecoverOptionsScreen(), + builder: (context, state) => + RecoverOptionsScreen(wallet: state.extra! as String), routes: [ GoRoute( path: 'physical', From 5166dd8a7011fd312fc446f505ddf1f394a58888 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Thu, 27 Feb 2025 14:43:52 -0500 Subject: [PATCH 365/401] fix: update wallet creation to set vaultBackupTested and lastVaultBackupTested --- .../bloc/backup_settings_cubit.dart | 41 +++++++++++++------ 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/lib/recoverbull/bloc/backup_settings_cubit.dart b/lib/recoverbull/bloc/backup_settings_cubit.dart index 73b91a3ea..bbf1fc2b8 100644 --- a/lib/recoverbull/bloc/backup_settings_cubit.dart +++ b/lib/recoverbull/bloc/backup_settings_cubit.dart @@ -678,18 +678,35 @@ class BackupSettingsCubit extends Cubit { type, publicDescriptors, ); - if (wallet == null) { + debugPrint('Failed to create wallet'); return (null, Err('Failed to create wallet')); } - final walletRepoErr = await _walletsStorageRepository.newWallet( - wallet.copyWith( - vaultBackupTested: true, mainWallet: type == BBWalletType.main)); - if (walletRepoErr != null && - !walletRepoErr.message.toLowerCase().contains('exists')) { - return (null, Err(walletRepoErr.toString())); + final updatedWallet = wallet.copyWith( + mainWallet: type == BBWalletType.main, + vaultBackupTested: true, + lastVaultBackupTested: DateTime.now(), + ); + final createWalletErr = + await _walletsStorageRepository.newWallet(updatedWallet); + + if (createWalletErr != null && + createWalletErr.message.toLowerCase().contains('exists')) { + final (existingWallet, readErr) = await _walletsStorageRepository + .readWallet(walletHashId: wallet.getWalletStorageString()); + + if (readErr != null) { + return (null, Err(readErr.toString())); + } + return ( + existingWallet?.copyWith( + vaultBackupTested: true, + lastVaultBackupTested: DateTime.now(), + ), + null + ); } - return (wallet, null); + return (updatedWallet, null); } catch (e) { return (null, Err(e.toString())); } @@ -971,12 +988,12 @@ class BackupSettingsCubit extends Cubit { backup.passphrase, backup.publicDescriptors, ); + if (err != null) { + return err; + } if (savedWallet != null) { await _updateWalletBackupStatus( - savedWallet.copyWith( - vaultBackupTested: true, - lastVaultBackupTested: DateTime.now(), - ), + savedWallet, ); } return err; From cdbd6011da09a87de2962cf5315f5f332eca0bba Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Thu, 27 Feb 2025 15:37:15 -0500 Subject: [PATCH 366/401] fix: improve error handling for Google Drive silent sign-in attempts --- lib/_pkg/recoverbull/google_drive.dart | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/_pkg/recoverbull/google_drive.dart b/lib/_pkg/recoverbull/google_drive.dart index c05b58f0b..9b4401e5b 100644 --- a/lib/_pkg/recoverbull/google_drive.dart +++ b/lib/_pkg/recoverbull/google_drive.dart @@ -29,12 +29,15 @@ class GoogleDriveBackupManager extends IRecoverbullManager { account = await _google.signInSilently(); } catch (e) { debugPrint('Silent sign-in failed, trying interactive sign-in: $e'); - account = await _google.signIn(); } - // If we still don't have an account after both attempts - if (account == null) { - return (null, Err(_errorMessages['connection']!)); + try { + account ??= await _google.signIn(); + } catch (e) { + debugPrint('Sign-in failed: $e'); } + // If we still don't have an account after both attempts + + if (account == null) return (null, Err(_errorMessages['connection']!)); try { final client = await _google.authenticatedClient(); From b1b428a02a3d349dbddc1b1163574700a5e41c32 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Thu, 27 Feb 2025 15:39:21 -0500 Subject: [PATCH 367/401] fix: loading state update for server status check failures --- lib/recoverbull/bloc/keychain_cubit.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/recoverbull/bloc/keychain_cubit.dart b/lib/recoverbull/bloc/keychain_cubit.dart index 1488cd20a..a348f7f30 100644 --- a/lib/recoverbull/bloc/keychain_cubit.dart +++ b/lib/recoverbull/bloc/keychain_cubit.dart @@ -53,7 +53,11 @@ class KeychainCubit extends Cubit { emit(state.copyWith(keyServerUp: true, loading: false)); } catch (e) { debugPrint('Server status check failed: $e'); - emit(state.copyWith(keyServerUp: false)); + emit(state.copyWith( + keyServerUp: false, + loading: false, + error: + 'Unable to reach key server. This could be due to network issues or the server may be temporarily unavailable.')); } } } From dde63715a885dc6d8cd3b225224f90b5231185b9 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Thu, 27 Feb 2025 16:00:46 -0500 Subject: [PATCH 368/401] fix: add key server warning banner to backup & recover wallet options --- lib/recoverbull/backup_settings.dart | 271 +++++++++++++++------------ 1 file changed, 147 insertions(+), 124 deletions(-) diff --git a/lib/recoverbull/backup_settings.dart b/lib/recoverbull/backup_settings.dart index cc09abf43..6ea607b71 100644 --- a/lib/recoverbull/backup_settings.dart +++ b/lib/recoverbull/backup_settings.dart @@ -179,71 +179,76 @@ class _BackupOptionsScreenState extends State { text: '', ), ), - body: Padding( - padding: const EdgeInsets.all(20.0), - child: Column( - children: [ - const BBText.titleLarge( - 'Backup your wallet', - isBold: true, - fontSize: 25, - ), - const Gap(10), - RichText( - textAlign: TextAlign.center, - text: TextSpan( - children: [ - TextSpan( - text: 'Without a backup, you', - style: context.font.bodySmall!.copyWith( - fontSize: 12, - ), - ), - TextSpan( - text: ' will ', - style: context.font.bodySmall!.copyWith( - fontWeight: FontWeight.w900, - fontSize: 12, - ), + body: Column( + children: [ + const KeyServerWarnings(), + Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + children: [ + const BBText.titleLarge( + 'Backup your wallet', + isBold: true, + fontSize: 25, + ), + const Gap(10), + RichText( + textAlign: TextAlign.center, + text: TextSpan( + children: [ + TextSpan( + text: 'Without a backup, you', + style: context.font.bodySmall!.copyWith( + fontSize: 12, + ), + ), + TextSpan( + text: ' will ', + style: context.font.bodySmall!.copyWith( + fontWeight: FontWeight.w900, + fontSize: 12, + ), + ), + TextSpan( + text: + 'eventually lose access to your money. It is critically important to do a backup.', + style: context.font.bodySmall!.copyWith( + fontSize: 12, + ), + ), + ], ), - TextSpan( - text: - 'eventually lose access to your money. It is critically important to do a backup.', - style: context.font.bodySmall!.copyWith( - fontSize: 12, - ), + ), + const Gap(20), + _renderBackupSetting( + title: 'Encrypted vault (quick and easy)', + description: + 'Your backup is encrypted with a secure key that cannot be cracked, and uploaded to your cloud account. The key to unlock your vault is stored in an anonymous password manager and accessible with your PIN.', + onTap: () => state.keyServerUp + ? context.push( + '/wallet-settings/backup-settings/backup-options/encrypted', + extra: widget.wallet, + ) + : ScaffoldMessenger.of(context).showSnackBar( + context.showToast( + '${state.error} Please try backing up again later', + ), + ), + ), + const Gap(20), + _renderBackupSetting( + title: 'Physical backup (take your time)', + description: + 'You have to write down 12 words on a piece of paper or engrave it in metal. Make sure not to lose it. If anybody ever finds those 12 words, they can steal your Bitcoin.', + onTap: () => context.push( + '/wallet-settings/backup-settings/backup-options/physical', + extra: widget.wallet, ), - ], - ), - ), - const Gap(20), - _renderBackupSetting( - title: 'Encrypted vault (quick and easy)', - description: - 'Your backup is encrypted with a secure key that cannot be cracked, and uploaded to your cloud account. The key to unlock your vault is stored in an anonymous password manager and accessible with your PIN.', - onTap: () => state.keyServerUp - ? context.push( - '/wallet-settings/backup-settings/backup-options/encrypted', - extra: widget.wallet, - ) - : ScaffoldMessenger.of(context).showSnackBar( - context.showToast( - '${state.error} Please try backing up again later', - ), - ), - ), - const Gap(20), - _renderBackupSetting( - title: 'Physical backup (take your time)', - description: - 'You have to write down 12 words on a piece of paper or engrave it in metal. Make sure not to lose it. If anybody ever finds those 12 words, they can steal your Bitcoin.', - onTap: () => context.push( - '/wallet-settings/backup-settings/backup-options/physical', - extra: widget.wallet, - ), + ), + ], ), - ], - ), + ), + ], ), ); }, @@ -326,71 +331,77 @@ class _RecoverOptionsScreenState extends State { text: '', ), ), - body: Padding( - padding: const EdgeInsets.all(20.0), - child: Column( - children: [ - const BBText.titleLarge( - 'Recover or test your backup', - isBold: true, - fontSize: 25, - ), - const Gap(10), - RichText( - textAlign: TextAlign.center, - text: TextSpan( - children: [ - TextSpan( - text: 'Testing your backup is ', - style: context.font.bodySmall!.copyWith(fontSize: 12), - ), - TextSpan( - text: 'critically important ', - style: context.font.bodySmall!.copyWith( - fontWeight: FontWeight.w900, - fontSize: 12, - ), - ), - TextSpan( - text: - 'to ensure you can recover your wallet if needed. Choose your recovery method below.', - style: context.font.bodySmall!.copyWith(fontSize: 12), - ), - ], - ), - ), - const Gap(20), - _renderBackupSetting( - title: 'Encrypted vault', - description: - "Restore your wallet using the encrypted backup stored in your cloud account. You'll need your PIN to access the decryption key from the password manager.", - onTap: () => state.keyServerUp - ? context.push( - '/wallet-settings/backup-settings/recover-options/encrypted', - ) - : { - ScaffoldMessenger.of(context).showSnackBar( - context.showToast( - '${state.error} Please try again later!', + body: Column( + children: [ + const KeyServerWarnings(), + Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + children: [ + const BBText.titleLarge( + 'Recover or test your backup', + isBold: true, + fontSize: 25, + ), + const Gap(10), + RichText( + textAlign: TextAlign.center, + text: TextSpan( + children: [ + TextSpan( + text: 'Testing your backup is ', + style: + context.font.bodySmall!.copyWith(fontSize: 12), + ), + TextSpan( + text: 'critically important ', + style: context.font.bodySmall!.copyWith( + fontWeight: FontWeight.w900, + fontSize: 12, ), ), - context.push( - '/wallet-settings/backup-settings/recover-options/encrypted', - ) - }, - ), - const Gap(20), - _renderBackupSetting( - title: 'Physical backup', - description: - "Restore your wallet by entering the 12 words from your physical backup.", - onTap: () => context.push( - '/wallet-settings/backup-settings/backup-options/physical', - extra: widget.wallet, - ), + TextSpan( + text: + 'to ensure you can recover your wallet if needed. Choose your recovery method below.', + style: + context.font.bodySmall!.copyWith(fontSize: 12), + ), + ], + ), + ), + const Gap(20), + _renderBackupSetting( + title: 'Encrypted vault', + description: + "Restore your wallet using the encrypted backup stored in your cloud account. You'll need your PIN to access the decryption key from the password manager.", + onTap: () => state.keyServerUp + ? context.push( + '/wallet-settings/backup-settings/recover-options/encrypted', + ) + : { + ScaffoldMessenger.of(context).showSnackBar( + context.showToast( + '${state.error} Please try again later!', + ), + ), + context.push( + '/wallet-settings/backup-settings/recover-options/encrypted', + ) + }, + ), + const Gap(20), + _renderBackupSetting( + title: 'Physical backup', + description: + "Restore your wallet by entering the 12 words from your physical backup.", + onTap: () => context.push( + '/wallet-settings/backup-settings/recover-options/physical', + ), + ), + ], ), - ], - ), + ), + ], ), ); }, @@ -439,3 +450,15 @@ class _RecoverOptionsScreenState extends State { ); } } + +class KeyServerWarnings extends StatelessWidget { + const KeyServerWarnings({super.key}); + + @override + Widget build(BuildContext context) { + final keyServerUp = context.watch().state.keyServerUp; + return !keyServerUp + ? WarningBanner(onTap: () {}, info: 'Key server is down') + : const SizedBox.shrink(); + } +} From d6409cb16a4a566f3e73031275fb643cedccad83 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Thu, 27 Feb 2025 16:01:02 -0500 Subject: [PATCH 369/401] code cleanup --- lib/recoverbull/backup_settings.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/recoverbull/backup_settings.dart b/lib/recoverbull/backup_settings.dart index 6ea607b71..6e611fe9e 100644 --- a/lib/recoverbull/backup_settings.dart +++ b/lib/recoverbull/backup_settings.dart @@ -2,6 +2,7 @@ import 'package:bb_mobile/_ui/app_bar.dart'; import 'package:bb_mobile/_ui/components/button.dart'; import 'package:bb_mobile/_ui/components/text.dart'; import 'package:bb_mobile/_ui/toast.dart'; +import 'package:bb_mobile/_ui/warning.dart'; import 'package:bb_mobile/recoverbull/bloc/backup_settings_cubit.dart'; import 'package:bb_mobile/recoverbull/bloc/keychain_cubit.dart'; import 'package:bb_mobile/recoverbull/bloc/keychain_state.dart'; From 8f520c9f1619944bc03e28bff3a0682eb5a8c77c Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Thu, 27 Feb 2025 16:20:11 -0500 Subject: [PATCH 370/401] code cleanup --- lib/recoverbull/backup_settings.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/recoverbull/backup_settings.dart b/lib/recoverbull/backup_settings.dart index 6e611fe9e..79a56d191 100644 --- a/lib/recoverbull/backup_settings.dart +++ b/lib/recoverbull/backup_settings.dart @@ -382,7 +382,7 @@ class _RecoverOptionsScreenState extends State { : { ScaffoldMessenger.of(context).showSnackBar( context.showToast( - '${state.error} Please try again later!', + state.error, ), ), context.push( From 545592a76f5bff0b8b400c9fcbbd63d3d70fb715 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Thu, 27 Feb 2025 18:00:17 -0500 Subject: [PATCH 371/401] fix: update recovery button to direct to encrypted vault options --- lib/home/home_page.dart | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/lib/home/home_page.dart b/lib/home/home_page.dart index e2fcad9f9..0ed08a6da 100644 --- a/lib/home/home_page.dart +++ b/lib/home/home_page.dart @@ -1150,11 +1150,8 @@ class HomeNoWalletsView extends StatelessWidget { onSurface: true, isBlue: false, fontSize: 11, - onPressed: () { - context.push( - '/wallet-settings/backup-settings/recover-options/encrypted', - ); - }, + onPressed: () => context.push( + '/wallet-settings/backup-settings/recover-options/encrypted'), ), ], ), From fb51a94608d953fc741ca3c2298f1070faf666aa Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Thu, 27 Feb 2025 18:02:37 -0500 Subject: [PATCH 372/401] fix: update backup action button to trigger --- lib/recoverbull/backup_key.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/recoverbull/backup_key.dart b/lib/recoverbull/backup_key.dart index 78a17e992..0fd625e67 100644 --- a/lib/recoverbull/backup_key.dart +++ b/lib/recoverbull/backup_key.dart @@ -345,7 +345,7 @@ class _BackupKeyInfoPage extends State { leftIcon: widget.backupKey.isNotEmpty ? CupertinoIcons.eye_fill : CupertinoIcons.cloud_download_fill, - onPressed: () => !keyState.keyServerUp + onPressed: () => keyState.keyServerUp ? _handleBackupAction(context) : () {}, ), From d63c04b598d01b6fc1769431914fb75131eba32c Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Thu, 27 Feb 2025 18:24:13 -0500 Subject: [PATCH 373/401] feat: add canPop parameter to EncryptedVaultRecoverPage for navigation control --- lib/recoverbull/encrypted_vault_backup.dart | 11 +++++++++-- lib/routes.dart | 12 +++++++++--- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/lib/recoverbull/encrypted_vault_backup.dart b/lib/recoverbull/encrypted_vault_backup.dart index a5753a8d6..55fd6b1f5 100644 --- a/lib/recoverbull/encrypted_vault_backup.dart +++ b/lib/recoverbull/encrypted_vault_backup.dart @@ -273,8 +273,14 @@ class StorageOptionCard extends StatelessWidget { } class EncryptedVaultRecoverPage extends StatefulWidget { - const EncryptedVaultRecoverPage({super.key, this.wallet}); + const EncryptedVaultRecoverPage({ + super.key, + this.wallet, + this.canPop = true, + }); + final String? wallet; + final bool canPop; @override State createState() => @@ -365,7 +371,8 @@ class _EncryptedVaultRecoverPageState extends State { centerTitle: true, flexibleSpace: BBAppBar( text: '', - onBack: () => context.pop(), + onBack: () => + widget.canPop ? context.pop() : context.go('/home'), ), ), body: state.loadingBackups diff --git a/lib/routes.dart b/lib/routes.dart index 30b67479a..dea03c9eb 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -247,9 +247,15 @@ GoRouter setupRouter() => GoRouter( ), GoRoute( path: 'encrypted', - builder: (context, state) => EncryptedVaultRecoverPage( - wallet: state.extra as String?, - ), + builder: (context, state) { + // Handle both String and bool extra parameters + final extra = state.extra; + if (extra is bool) { + return EncryptedVaultRecoverPage(canPop: extra); + } + return EncryptedVaultRecoverPage( + wallet: extra as String?); + }, routes: [ GoRoute( path: 'info', From f65b7aad6685dcb5bb499e297870da9c08ea6cc9 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Thu, 27 Feb 2025 18:27:53 -0500 Subject: [PATCH 374/401] feat: make canPop false in recovery flow --- lib/recoverbull/keychain_page.dart | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/recoverbull/keychain_page.dart b/lib/recoverbull/keychain_page.dart index 289176997..9de96be5b 100644 --- a/lib/recoverbull/keychain_page.dart +++ b/lib/recoverbull/keychain_page.dart @@ -840,6 +840,8 @@ class _SuccessDialog extends StatelessWidget { String title; String message; String route; + dynamic extra; + if (pageState == KeyChainPageState.recovery) { title = 'Recovery Successful'; message = 'Your wallet has been recovered successfully'; @@ -849,6 +851,7 @@ class _SuccessDialog extends StatelessWidget { message = 'Your wallet has been backed up successfully \n Please test your backup'; route = '/wallet-settings/backup-settings/recover-options/encrypted'; + extra = false; } else if (pageState == KeyChainPageState.delete) { title = 'Backup Key Deleted'; message = 'Your backup key has been permanently deleted'; @@ -881,7 +884,11 @@ class _SuccessDialog extends StatelessWidget { label: 'Continue', onPressed: () { Navigator.of(context).pop(); - context.go(route); + if (extra != null) { + context.push(route, extra: extra); + } else { + context.go(route); + } }, ) ], From 5aaa870b53badd967f6756abf75da858d946a715 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 3 Mar 2025 07:44:12 -0500 Subject: [PATCH 375/401] feat: add KeyServerWarnings to delete backup key flow --- lib/recoverbull/backup_key.dart | 41 +++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/lib/recoverbull/backup_key.dart b/lib/recoverbull/backup_key.dart index 0fd625e67..091fcc7d8 100644 --- a/lib/recoverbull/backup_key.dart +++ b/lib/recoverbull/backup_key.dart @@ -2,6 +2,7 @@ import 'package:bb_mobile/_ui/app_bar.dart'; import 'package:bb_mobile/_ui/components/button.dart'; import 'package:bb_mobile/_ui/components/text.dart'; import 'package:bb_mobile/_ui/toast.dart'; +import 'package:bb_mobile/recoverbull/backup_settings.dart'; import 'package:bb_mobile/recoverbull/bloc/backup_settings_cubit.dart'; import 'package:bb_mobile/recoverbull/bloc/backup_settings_state.dart'; import 'package:bb_mobile/recoverbull/bloc/keychain_cubit.dart'; @@ -55,25 +56,31 @@ class _BackupKeyPageState extends State { } Widget _buildContent(BuildContext context, BackupSettingsState state) { - return SingleChildScrollView( - padding: const EdgeInsets.all(20), - child: Column( - children: [ - const BBText.titleLarge('Where is your latest backup?', isBold: true), - const Gap(20), - ...BackupProvider.values.map( - (provider) => Padding( - padding: const EdgeInsets.only(bottom: 10), - child: StorageOptionCard( - title: provider.title, - description: provider.description, - icon: Icon(provider.icon, size: 40), - onTap: () => _handleRecover(context, provider), + return Column( + children: [ + const KeyServerWarnings(), + Padding( + padding: const EdgeInsets.all(20), + child: Column( + children: [ + const BBText.titleLarge('Where is your latest backup?', + isBold: true), + const Gap(20), + ...BackupProvider.values.map( + (provider) => Padding( + padding: const EdgeInsets.only(bottom: 10), + child: StorageOptionCard( + title: provider.title, + description: provider.description, + icon: Icon(provider.icon, size: 40), + onTap: () => _handleRecover(context, provider), + ), + ), ), - ), + ], ), - ], - ), + ), + ], ); } From a2bfe85b71db584a466542c7c7563984ffc3eedf Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 3 Mar 2025 07:59:22 -0500 Subject: [PATCH 376/401] feat: add KeyServerWarnings to backup recovery UI --- lib/recoverbull/encrypted_vault_backup.dart | 40 ++++++++++++--------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/lib/recoverbull/encrypted_vault_backup.dart b/lib/recoverbull/encrypted_vault_backup.dart index 55fd6b1f5..072272a90 100644 --- a/lib/recoverbull/encrypted_vault_backup.dart +++ b/lib/recoverbull/encrypted_vault_backup.dart @@ -4,6 +4,7 @@ import 'package:bb_mobile/_ui/app_bar.dart'; import 'package:bb_mobile/_ui/components/button.dart'; import 'package:bb_mobile/_ui/components/text.dart'; import 'package:bb_mobile/_ui/toast.dart'; +import 'package:bb_mobile/recoverbull/backup_settings.dart'; import 'package:bb_mobile/recoverbull/bloc/backup_settings_cubit.dart'; import 'package:bb_mobile/recoverbull/bloc/backup_settings_state.dart'; import 'package:bb_mobile/recoverbull/bloc/keychain_cubit.dart'; @@ -317,25 +318,30 @@ class _EncryptedVaultRecoverPageState extends State { } Widget _buildContent(BuildContext context, BackupSettingsState state) { - return SingleChildScrollView( - padding: const EdgeInsets.all(20), - child: Column( - children: [ - const BBText.titleLarge('Where is your backup?', isBold: true), - const Gap(20), - ...BackupProvider.values.map( - (provider) => Padding( - padding: const EdgeInsets.only(bottom: 10), - child: StorageOptionCard( - title: provider.title, - description: provider.description, - icon: Icon(provider.icon, size: 40), - onTap: () => _handleRecover(context, provider), + return Column( + children: [ + const KeyServerWarnings(), + Padding( + padding: const EdgeInsets.all(20), + child: Column( + children: [ + const BBText.titleLarge('Where is your backup?', isBold: true), + const Gap(20), + ...BackupProvider.values.map( + (provider) => Padding( + padding: const EdgeInsets.only(bottom: 10), + child: StorageOptionCard( + title: provider.title, + description: provider.description, + icon: Icon(provider.icon, size: 40), + onTap: () => _handleRecover(context, provider), + ), + ), ), - ), + ], ), - ], - ), + ), + ], ); } From e76e2505ae44f5dd28d83cdca4cef2aa1d2192d2 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 3 Mar 2025 08:01:58 -0500 Subject: [PATCH 377/401] feat: default input state to backupKey on unreachable keyServer --- lib/recoverbull/keychain_page.dart | 47 +++++++++++++++++++++++++++--- 1 file changed, 43 insertions(+), 4 deletions(-) diff --git a/lib/recoverbull/keychain_page.dart b/lib/recoverbull/keychain_page.dart index 9de96be5b..8da8c408d 100644 --- a/lib/recoverbull/keychain_page.dart +++ b/lib/recoverbull/keychain_page.dart @@ -45,7 +45,8 @@ class KeychainBackupPage extends StatelessWidget { backupId ?? '', backupKey, backupSalt ?? '', - ), + ) + ..keyServerStatus(), ), BlocProvider.value(value: createBackupSettingsCubit()), ], @@ -184,6 +185,19 @@ class _Screen extends StatelessWidget { } }, ), + BlocListener( + listenWhen: (previous, current) => + previous.keyServerUp != current.keyServerUp, + listener: (context, state) { + if (!state.keyServerUp && + state.inputType != KeyChainInputType.backupKey) { + context.read().updatePageState( + KeyChainInputType.backupKey, + state.pageState, + ); + } + }, + ), ], child: BlocBuilder( builder: (context, state) { @@ -579,8 +593,13 @@ class _SetButton extends StatelessWidget { const _SetButton({required this.inputType}); @override Widget build(BuildContext context) { - final canStoreKey = - context.select((KeychainCubit x) => x.state.canStoreKey); + final state = context.select((KeychainCubit x) => x.state); + final canStoreKey = state.canStoreKey; + final keyServerUp = state.keyServerUp; + + // Don't show the button if keyserver is down + if (!keyServerUp) return const SizedBox.shrink(); + return AnimatedSwitcher( duration: const Duration(milliseconds: 300), child: Column( @@ -660,7 +679,8 @@ class _RecoverButton extends StatelessWidget { buildWhen: (previous, current) => previous.canRecoverKey != current.canRecoverKey || previous.loading != current.loading || - previous.pageState != current.pageState, + previous.pageState != current.pageState || + previous.keyServerUp != current.keyServerUp, builder: (context, state) { final canRecover = inputType == KeyChainInputType.backupKey ? state.canRecoverWithBckupKey @@ -670,6 +690,25 @@ class _RecoverButton extends StatelessWidget { final isDownloadFlow = state.pageState == KeyChainPageState.download || state.originalPageState == KeyChainPageState.download; + // Show only backup key option if server is down + if (!state.keyServerUp && inputType != KeyChainInputType.backupKey) { + return Column( + children: [ + const BBText.bodySmall( + 'Server is currently unavailable.\nPlease use your backup key to recover.', + textAlign: TextAlign.center, + isRed: true, + ), + const Gap(16), + BBButton.withColour( + fillWidth: true, + label: 'Switch to Backup Key', + onPressed: () => _switchToBackupKey(context), + ), + ], + ); + } + return Column( children: [ const Gap(10), From acef6a5f782884faa06aaa9c8788b86eacfe309f Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 3 Mar 2025 08:15:05 -0500 Subject: [PATCH 378/401] feat: add common gap and padding constants for layout consistency --- lib/recoverbull/keychain_page.dart | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/recoverbull/keychain_page.dart b/lib/recoverbull/keychain_page.dart index 8da8c408d..d582030cd 100644 --- a/lib/recoverbull/keychain_page.dart +++ b/lib/recoverbull/keychain_page.dart @@ -18,6 +18,13 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:gap/gap.dart'; import 'package:go_router/go_router.dart'; +/// Common constants +const _kGapSmall = 8.0; +const _kGapMedium = 16.0; +const _kGapLarge = 24.0; +const _kGapXLarge = 50.0; +const _kHorizontalPadding = 32.0; + class KeychainBackupPage extends StatelessWidget { KeychainBackupPage({ super.key, From 0b6af94597d9a15188b638601d71c7f3acf43b51 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 3 Mar 2025 08:16:35 -0500 Subject: [PATCH 379/401] feat: refactor page layouts to use shared components --- lib/recoverbull/keychain_page.dart | 211 ++++++++++++++--------------- 1 file changed, 105 insertions(+), 106 deletions(-) diff --git a/lib/recoverbull/keychain_page.dart b/lib/recoverbull/keychain_page.dart index d582030cd..666b9efc1 100644 --- a/lib/recoverbull/keychain_page.dart +++ b/lib/recoverbull/keychain_page.dart @@ -39,19 +39,15 @@ class KeychainBackupPage extends StatelessWidget { @override Widget build(BuildContext context) { - // Extract backup data - final backupId = backup['id'] as String?; - final backupSalt = backup['salt'] as String?; - return MultiBlocProvider( providers: [ BlocProvider( create: (context) => KeychainCubit() ..setChainState( _pState, - backupId ?? '', + backup['id'] as String? ?? '', backupKey, - backupSalt ?? '', + backup['salt'] as String? ?? '', ) ..keyServerStatus(), ), @@ -264,72 +260,95 @@ class _Screen extends StatelessWidget { } } -/// Page Type Widgets -class _EnterPage extends StatelessWidget { - const _EnterPage({super.key, required this.inputType}); - final KeyChainInputType inputType; +/// Shared layout widget for all pages +class _PageLayout extends StatelessWidget { + const _PageLayout({ + required this.bottomChild, + required this.children, + this.bottomHeight, + }); + + final Widget bottomChild; + final List children; + final double? bottomHeight; @override Widget build(BuildContext context) { return StackedPage( - bottomChildHeight: MediaQuery.of(context).size.height * 0.11, - bottomChild: _SetButton(inputType: inputType), + bottomChildHeight: + bottomHeight ?? MediaQuery.of(context).size.height * 0.11, + bottomChild: bottomChild, child: Padding( - key: ValueKey('enter$inputType'), - padding: const EdgeInsets.symmetric(horizontal: 32), + padding: const EdgeInsets.symmetric(horizontal: _kHorizontalPadding), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const Gap(50), - const _TitleText(), - const Gap(8), - const _SubtitleText(), - const Gap(50), - if (inputType == KeyChainInputType.pin) ...[ - _PinField(), - const KeyPad(), - ] else - _PasswordField(), - const Gap(30), - ], + children: children, ), ), ); } } +/// Shared input section widget +class _InputSection extends StatelessWidget { + const _InputSection({required this.inputType}); + + final KeyChainInputType inputType; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + if (inputType == KeyChainInputType.pin) ...[ + _PinField(), + const KeyPad(), + ] else + _PasswordField(), + ], + ); + } +} + +// Optimize page type widgets +class _EnterPage extends StatelessWidget { + const _EnterPage({super.key, required this.inputType}); + final KeyChainInputType inputType; + + @override + Widget build(BuildContext context) { + return _PageLayout( + bottomChild: _SetButton(inputType: inputType), + children: [ + const Gap(_kGapXLarge), + const _TitleText(), + const Gap(_kGapSmall), + const _SubtitleText(), + const Gap(_kGapXLarge), + _InputSection(inputType: inputType), + const Gap(_kGapLarge), + ], + ); + } +} + class _ConfirmPage extends StatelessWidget { const _ConfirmPage({super.key, required this.inputType}); final KeyChainInputType inputType; @override Widget build(BuildContext context) { - return StackedPage( + return _PageLayout( bottomChild: _ConfirmButton(inputType: inputType), - bottomChildHeight: MediaQuery.of(context).size.height * 0.11, - child: SingleChildScrollView( - key: ValueKey('confirm$inputType'), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 32), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Gap(20), - const _ConfirmTitleText(), - const Gap(8), - const _ConfirmSubtitleText(), - const Gap(48), - if (inputType == KeyChainInputType.pin) ...[ - _PinField(), - const KeyPad(), - ] else - _PasswordField(), - const Gap(24), - ], - ), - ), - ), + bottomHeight: MediaQuery.of(context).size.height * 0.11, + children: [ + const Gap(20), + const _ConfirmTitleText(), + const Gap(_kGapSmall), + const _ConfirmSubtitleText(), + const Gap(48), + _InputSection(inputType: inputType), + const Gap(24), + ], ); } } @@ -340,35 +359,25 @@ class _RecoveryPage extends StatelessWidget { @override Widget build(BuildContext context) { - return StackedPage( - bottomChildHeight: MediaQuery.of(context).size.height * 0.16, + return _PageLayout( bottomChild: _RecoverButton(inputType: inputType), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 32), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const Gap(50), - BBText.titleLarge( - 'Enter Recovery ${_getInputTypeText(inputType)}', - textAlign: TextAlign.center, - isBold: true, - ), - const Gap(8), - BBText.bodySmall( - 'Enter the ${_getInputTypeText(inputType).toLowerCase()} you used to backup your keychain', - textAlign: TextAlign.center, - ), - const Gap(50), - if (inputType == KeyChainInputType.pin) ...[ - _PinField(), - const KeyPad(), - ] else - _PasswordField(), - const Gap(30), - ], + bottomHeight: MediaQuery.of(context).size.height * 0.16, + children: [ + const Gap(_kGapXLarge), + BBText.titleLarge( + 'Enter Recovery ${_getInputTypeText(inputType)}', + textAlign: TextAlign.center, + isBold: true, ), - ), + const Gap(_kGapSmall), + BBText.bodySmall( + 'Enter the ${_getInputTypeText(inputType).toLowerCase()} you used to backup your keychain', + textAlign: TextAlign.center, + ), + const Gap(_kGapXLarge), + _InputSection(inputType: inputType), + const Gap(_kGapLarge), + ], ); } @@ -390,35 +399,25 @@ class _DeletePage extends StatelessWidget { @override Widget build(BuildContext context) { - return StackedPage( - bottomChildHeight: MediaQuery.of(context).size.height * 0.11, + return _PageLayout( bottomChild: _DeleteButton(inputType: inputType), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 32), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const Gap(50), - const BBText.titleLarge( - 'Delete Backup Key', - textAlign: TextAlign.center, - isBold: true, - ), - const Gap(8), - BBText.bodySmall( - 'Enter your ${inputType == KeyChainInputType.pin ? 'PIN' : 'password'} to delete this backup key', - textAlign: TextAlign.center, - ), - const Gap(50), - if (inputType == KeyChainInputType.pin) ...[ - _PinField(), - const KeyPad(), - ] else - _PasswordField(), - const Gap(30), - ], + bottomHeight: MediaQuery.of(context).size.height * 0.11, + children: [ + const Gap(_kGapXLarge), + const BBText.titleLarge( + 'Delete Backup Key', + textAlign: TextAlign.center, + isBold: true, ), - ), + const Gap(_kGapSmall), + BBText.bodySmall( + 'Enter your ${inputType == KeyChainInputType.pin ? 'PIN' : 'password'} to delete this backup key', + textAlign: TextAlign.center, + ), + const Gap(_kGapXLarge), + _InputSection(inputType: inputType), + const Gap(_kGapLarge), + ], ); } } From e1e73f980103c6cc40459d3e6a783e0ce81a9199 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 3 Mar 2025 08:17:01 -0500 Subject: [PATCH 380/401] feat: implement button logic mixin for server status handling --- lib/recoverbull/keychain_page.dart | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/lib/recoverbull/keychain_page.dart b/lib/recoverbull/keychain_page.dart index 666b9efc1..018b66588 100644 --- a/lib/recoverbull/keychain_page.dart +++ b/lib/recoverbull/keychain_page.dart @@ -594,7 +594,23 @@ class _NumberButtonState extends State { } /// Action Buttons -class _SetButton extends StatelessWidget { +mixin _ButtonLogicMixin { + void handleServerCheck(BuildContext context, VoidCallback onSuccess) { + context.read().keyServerStatus(); + final state = context.read().state; + if (state.canStoreKey) onSuccess(); + } + + Widget buildServerDownMessage() { + return const BBText.bodySmall( + 'Server is currently unavailable.\nPlease use your backup key.', + textAlign: TextAlign.center, + isRed: true, + ); + } +} + +class _SetButton extends StatelessWidget with _ButtonLogicMixin { final KeyChainInputType inputType; const _SetButton({required this.inputType}); @override @@ -649,7 +665,7 @@ class _SetButton extends StatelessWidget { } } -class _ConfirmButton extends StatelessWidget { +class _ConfirmButton extends StatelessWidget with _ButtonLogicMixin { const _ConfirmButton({required this.inputType}); final KeyChainInputType inputType; @override @@ -675,7 +691,7 @@ class _ConfirmButton extends StatelessWidget { } } -class _RecoverButton extends StatelessWidget { +class _RecoverButton extends StatelessWidget with _ButtonLogicMixin { const _RecoverButton({required this.inputType}); final KeyChainInputType inputType; @@ -787,7 +803,7 @@ class _RecoverButton extends StatelessWidget { } } -class _DeleteButton extends StatelessWidget { +class _DeleteButton extends StatelessWidget with _ButtonLogicMixin { const _DeleteButton({required this.inputType}); final KeyChainInputType inputType; From 29a9a60bd09cacbc60a12cebf6b5559b56a20eeb Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 3 Mar 2025 08:17:22 -0500 Subject: [PATCH 381/401] feat: create reusable dialog component for success and error handling --- lib/recoverbull/keychain_page.dart | 129 +++++++++++++++-------------- 1 file changed, 69 insertions(+), 60 deletions(-) diff --git a/lib/recoverbull/keychain_page.dart b/lib/recoverbull/keychain_page.dart index 018b66588..0142a7fa8 100644 --- a/lib/recoverbull/keychain_page.dart +++ b/lib/recoverbull/keychain_page.dart @@ -891,6 +891,54 @@ class _LoadingView extends StatelessWidget { } } +class _DialogBase extends StatelessWidget { + const _DialogBase({ + required this.icon, + required this.title, + required this.message, + required this.buttonText, + required this.onButtonPressed, + this.iconColor, + }); + + final IconData icon; + final String title; + final String message; + final String buttonText; + final VoidCallback onButtonPressed; + final Color? iconColor; + + @override + Widget build(BuildContext context) { + return Dialog( + backgroundColor: context.colour.primaryContainer, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + child: Padding( + padding: const EdgeInsets.all(_kGapLarge), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + icon, + color: iconColor ?? context.colour.primary, + size: 48, + ), + const Gap(_kGapMedium), + BBText.title(title, textAlign: TextAlign.center, isBold: true), + const Gap(_kGapSmall), + BBText.bodySmall(message, textAlign: TextAlign.center), + const Gap(_kGapLarge), + BBButton.withColour( + label: buttonText, + onPressed: onButtonPressed, + ), + ], + ), + ), + ); + } +} + class _SuccessDialog extends StatelessWidget { const _SuccessDialog({required this.pageState}); @@ -923,38 +971,19 @@ class _SuccessDialog extends StatelessWidget { route = '/home'; } - return Dialog( - backgroundColor: context.colour.primaryContainer, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), - child: Padding( - padding: const EdgeInsets.all(24), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.check_circle_outline, - color: context.colour.primary, - size: 48, - ), - const Gap(16), - BBText.title(title, textAlign: TextAlign.center, isBold: true), - const Gap(8), - BBText.bodySmall(message, textAlign: TextAlign.center), - const Gap(24), - BBButton.withColour( - label: 'Continue', - onPressed: () { - Navigator.of(context).pop(); - if (extra != null) { - context.push(route, extra: extra); - } else { - context.go(route); - } - }, - ) - ], - ), - ), + return _DialogBase( + icon: Icons.check_circle_outline, + title: title, + message: message, + buttonText: 'Continue', + onButtonPressed: () { + Navigator.of(context).pop(); + if (extra != null) { + context.push(route, extra: extra); + } else { + context.go(route); + } + }, ); } } @@ -965,34 +994,14 @@ class _ErrorDialog extends StatelessWidget { final bool isRecovery; @override Widget build(BuildContext context) { - return Dialog( - backgroundColor: context.colour.primaryContainer, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), - child: Padding( - padding: const EdgeInsets.all(24), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.error_outline, - color: context.colour.primary, - size: 48, - ), - const Gap(16), - BBText.title(isRecovery ? 'Recovery failed' : 'Backup failed', - textAlign: TextAlign.center, isBold: true), - const Gap(8), - BBText.bodySmall(error, textAlign: TextAlign.center), - const Gap(24), - BBButton.withColour( - label: 'Continue', - onPressed: () { - Navigator.of(context).pop(); - }, - ) - ], - ), - ), + return _DialogBase( + icon: Icons.error_outline, + title: isRecovery ? 'Recovery failed' : 'Backup failed', + message: error, + buttonText: 'Continue', + onButtonPressed: () { + Navigator.of(context).pop(); + }, ); } } From 2ba5eea7bb5ceab470dca6c6fd029c8d205fe7f9 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 3 Mar 2025 09:26:15 -0500 Subject: [PATCH 382/401] feat: add updateWallet method to emit updated wallet state --- lib/wallet/bloc/wallet_bloc.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/wallet/bloc/wallet_bloc.dart b/lib/wallet/bloc/wallet_bloc.dart index 0449036b2..186deaf18 100644 --- a/lib/wallet/bloc/wallet_bloc.dart +++ b/lib/wallet/bloc/wallet_bloc.dart @@ -88,6 +88,10 @@ class WalletBloc extends Bloc { .getWalletServiceById(state.wallet.id) ?.syncWallet(); } + + void updateWallet(Wallet updatedWallet) { + emit(state.copyWith(wallet: updatedWallet)); + } } WalletBloc createOrRetreiveWalletBloc(String walletId, {Wallet? wallet}) { From ff65016bfcc5408d5b930063d34b3ca07e9807fe Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 3 Mar 2025 09:26:33 -0500 Subject: [PATCH 383/401] feat: update wallet handling in BackupSettingsCubit to update WalletBloc directly --- lib/recoverbull/bloc/backup_settings_cubit.dart | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/recoverbull/bloc/backup_settings_cubit.dart b/lib/recoverbull/bloc/backup_settings_cubit.dart index bbf1fc2b8..2e72b2ef9 100644 --- a/lib/recoverbull/bloc/backup_settings_cubit.dart +++ b/lib/recoverbull/bloc/backup_settings_cubit.dart @@ -18,6 +18,7 @@ import 'package:bb_mobile/_repository/wallet/wallet_storage.dart'; import 'package:bb_mobile/_repository/wallet_service.dart'; import 'package:bb_mobile/home/bloc/home_bloc.dart'; import 'package:bb_mobile/home/bloc/home_event.dart'; +import 'package:bb_mobile/home/home_page.dart'; import 'package:bb_mobile/locator.dart'; import 'package:bb_mobile/recoverbull/bloc/backup_settings_state.dart'; import 'package:flutter/material.dart'; @@ -377,8 +378,7 @@ class BackupSettingsCubit extends Cubit { // Update home state and sort wallets locator().add(LoadWalletsFromStorage()); - await locator().sortWallets(); - + await locator().readAllWallets(); _emitSafe( state.copyWith( loadingBackups: false, @@ -1007,6 +1007,13 @@ class BackupSettingsCubit extends Cubit { updatedWallet, updateTypes: [UpdateWalletTypes.settings], ); + + // Get the WalletBloc and update it directly + final walletBloc = locator() + .state + .firstWhere((bloc) => bloc.state.wallet.id == updatedWallet.id); + walletBloc.updateWallet(updatedWallet); + _currentWallet = updatedWallet; } } From 97174b65315370120188db5a4858519f2ff5cd43 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 3 Mar 2025 09:26:53 -0500 Subject: [PATCH 384/401] feat: refactor BackupSettings to use StatefulWidget and add BlocListener for wallet state updates --- lib/recoverbull/backup_settings.dart | 156 +++++++++++++++------------ 1 file changed, 87 insertions(+), 69 deletions(-) diff --git a/lib/recoverbull/backup_settings.dart b/lib/recoverbull/backup_settings.dart index 79a56d191..75672d20f 100644 --- a/lib/recoverbull/backup_settings.dart +++ b/lib/recoverbull/backup_settings.dart @@ -7,6 +7,7 @@ import 'package:bb_mobile/recoverbull/bloc/backup_settings_cubit.dart'; import 'package:bb_mobile/recoverbull/bloc/keychain_cubit.dart'; import 'package:bb_mobile/recoverbull/bloc/keychain_state.dart'; import 'package:bb_mobile/styles.dart'; +import 'package:bb_mobile/wallet/bloc/state.dart'; import 'package:bb_mobile/wallet/bloc/wallet_bloc.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -64,8 +65,14 @@ class _BackupSettingsState extends State { } } -class _Screen extends StatelessWidget { +class _Screen extends StatefulWidget { const _Screen(); + + @override + State<_Screen> createState() => _ScreenState(); +} + +class _ScreenState extends State<_Screen> { @override Widget build(BuildContext context) { final watchOnly = @@ -74,78 +81,89 @@ class _Screen extends StatelessWidget { context.select((WalletBloc x) => x.state.wallet.physicalBackupTested); final isVaultBackupTested = context.select((WalletBloc x) => x.state.wallet.vaultBackupTested); - return Container( - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: context.colour.primaryContainer, - borderRadius: BorderRadius.circular(12), - boxShadow: [ - BoxShadow( - color: NewColours.lightGray.withAlpha(50), - blurRadius: 30, - spreadRadius: 2, - offset: const Offset(0, 10), - ), - ], - border: Border.all(color: NewColours.lightGray.withAlpha(50)), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const BBText.titleLarge("Backup settings", isBold: true), - const Gap(10), - if (!watchOnly) ...[ + + return BlocListener( + listenWhen: (previous, current) => + previous.wallet.vaultBackupTested != + current.wallet.vaultBackupTested || + previous.wallet.physicalBackupTested != + current.wallet.physicalBackupTested, + listener: (context, state) { + setState(() {}); + }, + child: Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: context.colour.primaryContainer, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: NewColours.lightGray.withAlpha(50), + blurRadius: 30, + spreadRadius: 2, + offset: const Offset(0, 10), + ), + ], + border: Border.all(color: NewColours.lightGray.withAlpha(50)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const BBText.titleLarge("Backup settings", isBold: true), + const Gap(10), + if (!watchOnly) ...[ + BBButton.textWithStatus( + label: "Physical backup", + onPressed: () {}, + statusText: isPhysicalBackupTested ? 'Tested' : 'Not Tested', + isGreen: isPhysicalBackupTested, + isRed: !isPhysicalBackupTested, + ), + ], BBButton.textWithStatus( - label: "Physical backup", + label: "Encrypted vault", onPressed: () {}, - statusText: isPhysicalBackupTested ? 'Tested' : 'Not Tested', - isGreen: isPhysicalBackupTested, - isRed: !isPhysicalBackupTested, + statusText: isVaultBackupTested ? 'Tested' : 'Not Tested', + isGreen: isVaultBackupTested, + isRed: !isVaultBackupTested, + ), + BBButton.withColour( + label: "Start Backup", + onPressed: () => { + context.push( + '/wallet-settings/backup-settings/backup-options', + extra: context.read().state.wallet.id, + ), + }, + fillWidth: true, + center: true, + ), + const Gap(20), + BBButton.withColour( + label: "Recover/Test Backup", + onPressed: () { + context.push( + '/wallet-settings/backup-settings/recover-options', + extra: context.read().state.wallet.id, + ); + }, + fillWidth: true, + center: true, + ), + const Gap(20), + BBButton.withColour( + label: "View/Delete Backup Key", + onPressed: () => { + context.push( + '/wallet-settings/backup-settings/key', + extra: context.read().state.wallet.id, + ), + }, + fillWidth: true, + center: true, ), ], - BBButton.textWithStatus( - label: "Encrypted vault", - onPressed: () {}, - statusText: isVaultBackupTested ? 'Tested' : 'Not Tested', - isGreen: isVaultBackupTested, - isRed: !isVaultBackupTested, - ), - BBButton.withColour( - label: "Start Backup", - onPressed: () => { - context.push( - '/wallet-settings/backup-settings/backup-options', - extra: context.read().state.wallet.id, - ), - }, - fillWidth: true, - center: true, - ), - const Gap(20), - BBButton.withColour( - label: "Recover/Test Backup", - onPressed: () { - context.push( - '/wallet-settings/backup-settings/recover-options', - extra: context.read().state.wallet.id, - ); - }, - fillWidth: true, - center: true, - ), - const Gap(20), - BBButton.withColour( - label: "View/Delete Backup Key", - onPressed: () => { - context.push( - '/wallet-settings/backup-settings/key', - extra: context.read().state.wallet.id, - ), - }, - fillWidth: true, - center: true, - ), - ], + ), ), ); } From f39694e9667169f5287013b9afc8d5f2deecfa12 Mon Sep 17 00:00:00 2001 From: ethicnology Date: Mon, 3 Mar 2025 10:20:17 -0500 Subject: [PATCH 385/401] fix: #497 disclaimer message before google sign in --- lib/recoverbull/encrypted_vault_backup.dart | 35 ++++++++++++++++++--- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/lib/recoverbull/encrypted_vault_backup.dart b/lib/recoverbull/encrypted_vault_backup.dart index 55fd6b1f5..c54733c16 100644 --- a/lib/recoverbull/encrypted_vault_backup.dart +++ b/lib/recoverbull/encrypted_vault_backup.dart @@ -20,15 +20,22 @@ import 'package:intl/intl.dart'; const double _kSpacing = 15.0; enum BackupProvider { - googleDrive('Google Drive', 'Easy', Icons.add_to_drive_rounded), - iCloud('Apple iCloud', 'Easy', CupertinoIcons.cloud_upload), - custom('Custom location', 'Private', Icons.folder); + googleDrive('Google Drive', 'Easy', Icons.add_to_drive_rounded, + 'Your Google account information is never collected by Bull Bitcoin. It stays within the app and is not shared to our organization or to any third party.'), + iCloud('Apple iCloud', 'Easy', CupertinoIcons.cloud_upload, ''), + custom('Custom location', 'Private', Icons.folder, ''); final String title; final String description; final IconData icon; - - const BackupProvider(this.title, this.description, this.icon); + final String disclaimer; + + const BackupProvider( + this.title, + this.description, + this.icon, + this.disclaimer, + ); } class EncryptedVaultBackupPage extends StatefulWidget { @@ -58,6 +65,11 @@ class _EncryptedVaultBackupPageState extends State { BuildContext context, BackupProvider provider, ) async { + if (provider.disclaimer.isNotEmpty) { + _showDisclaimer(context, provider.disclaimer); + await Future.delayed(const Duration(seconds: 3)); + } + switch (provider) { case BackupProvider.googleDrive: await _cubit.saveGoogleDriveBackup(); @@ -590,3 +602,16 @@ class _RecoveredBackupInfoPageState extends State { ); } } + +void _showDisclaimer(BuildContext context, String disclaimer) { + showDialog( + context: context, + builder: (context) => AlertDialog( + backgroundColor: context.colour.primaryContainer, + content: Text( + disclaimer, + style: context.font.bodySmall!.copyWith(fontWeight: FontWeight.bold), + ), + ), + ); +} From e887870f25814baf1d140848d5380954e9f6122f Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 3 Mar 2025 10:55:54 -0500 Subject: [PATCH 386/401] fix: removed dead code --- lib/recoverbull/backup_settings.dart | 154 ++++++++++++--------------- 1 file changed, 69 insertions(+), 85 deletions(-) diff --git a/lib/recoverbull/backup_settings.dart b/lib/recoverbull/backup_settings.dart index 75672d20f..43b0051c9 100644 --- a/lib/recoverbull/backup_settings.dart +++ b/lib/recoverbull/backup_settings.dart @@ -7,7 +7,6 @@ import 'package:bb_mobile/recoverbull/bloc/backup_settings_cubit.dart'; import 'package:bb_mobile/recoverbull/bloc/keychain_cubit.dart'; import 'package:bb_mobile/recoverbull/bloc/keychain_state.dart'; import 'package:bb_mobile/styles.dart'; -import 'package:bb_mobile/wallet/bloc/state.dart'; import 'package:bb_mobile/wallet/bloc/wallet_bloc.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -65,14 +64,9 @@ class _BackupSettingsState extends State { } } -class _Screen extends StatefulWidget { +class _Screen extends StatelessWidget { const _Screen(); - @override - State<_Screen> createState() => _ScreenState(); -} - -class _ScreenState extends State<_Screen> { @override Widget build(BuildContext context) { final watchOnly = @@ -82,88 +76,78 @@ class _ScreenState extends State<_Screen> { final isVaultBackupTested = context.select((WalletBloc x) => x.state.wallet.vaultBackupTested); - return BlocListener( - listenWhen: (previous, current) => - previous.wallet.vaultBackupTested != - current.wallet.vaultBackupTested || - previous.wallet.physicalBackupTested != - current.wallet.physicalBackupTested, - listener: (context, state) { - setState(() {}); - }, - child: Container( - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: context.colour.primaryContainer, - borderRadius: BorderRadius.circular(12), - boxShadow: [ - BoxShadow( - color: NewColours.lightGray.withAlpha(50), - blurRadius: 30, - spreadRadius: 2, - offset: const Offset(0, 10), - ), - ], - border: Border.all(color: NewColours.lightGray.withAlpha(50)), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const BBText.titleLarge("Backup settings", isBold: true), - const Gap(10), - if (!watchOnly) ...[ - BBButton.textWithStatus( - label: "Physical backup", - onPressed: () {}, - statusText: isPhysicalBackupTested ? 'Tested' : 'Not Tested', - isGreen: isPhysicalBackupTested, - isRed: !isPhysicalBackupTested, - ), - ], + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: context.colour.primaryContainer, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: NewColours.lightGray.withAlpha(50), + blurRadius: 30, + spreadRadius: 2, + offset: const Offset(0, 10), + ), + ], + border: Border.all(color: NewColours.lightGray.withAlpha(50)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const BBText.titleLarge("Backup settings", isBold: true), + const Gap(10), + if (!watchOnly) ...[ BBButton.textWithStatus( - label: "Encrypted vault", + label: "Physical backup", onPressed: () {}, - statusText: isVaultBackupTested ? 'Tested' : 'Not Tested', - isGreen: isVaultBackupTested, - isRed: !isVaultBackupTested, - ), - BBButton.withColour( - label: "Start Backup", - onPressed: () => { - context.push( - '/wallet-settings/backup-settings/backup-options', - extra: context.read().state.wallet.id, - ), - }, - fillWidth: true, - center: true, - ), - const Gap(20), - BBButton.withColour( - label: "Recover/Test Backup", - onPressed: () { - context.push( - '/wallet-settings/backup-settings/recover-options', - extra: context.read().state.wallet.id, - ); - }, - fillWidth: true, - center: true, - ), - const Gap(20), - BBButton.withColour( - label: "View/Delete Backup Key", - onPressed: () => { - context.push( - '/wallet-settings/backup-settings/key', - extra: context.read().state.wallet.id, - ), - }, - fillWidth: true, - center: true, + statusText: isPhysicalBackupTested ? 'Tested' : 'Not Tested', + isGreen: isPhysicalBackupTested, + isRed: !isPhysicalBackupTested, ), ], - ), + BBButton.textWithStatus( + label: "Encrypted vault", + onPressed: () {}, + statusText: isVaultBackupTested ? 'Tested' : 'Not Tested', + isGreen: isVaultBackupTested, + isRed: !isVaultBackupTested, + ), + BBButton.withColour( + label: "Start Backup", + onPressed: () => { + context.push( + '/wallet-settings/backup-settings/backup-options', + extra: context.read().state.wallet.id, + ), + }, + fillWidth: true, + center: true, + ), + const Gap(20), + BBButton.withColour( + label: "Recover/Test Backup", + onPressed: () { + context.push( + '/wallet-settings/backup-settings/recover-options', + extra: context.read().state.wallet.id, + ); + }, + fillWidth: true, + center: true, + ), + const Gap(20), + BBButton.withColour( + label: "View/Delete Backup Key", + onPressed: () => { + context.push( + '/wallet-settings/backup-settings/key', + extra: context.read().state.wallet.id, + ), + }, + fillWidth: true, + center: true, + ), + ], ), ); } From d59147977e0a27e8fea0d94791ea29628f46a4a8 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 3 Mar 2025 11:00:48 -0500 Subject: [PATCH 387/401] fix: double the gap size in RecoverButton --- lib/recoverbull/keychain_page.dart | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/recoverbull/keychain_page.dart b/lib/recoverbull/keychain_page.dart index 0142a7fa8..5cdaa4b66 100644 --- a/lib/recoverbull/keychain_page.dart +++ b/lib/recoverbull/keychain_page.dart @@ -733,7 +733,6 @@ class _RecoverButton extends StatelessWidget with _ButtonLogicMixin { return Column( children: [ - const Gap(10), // Always show PIN/password switch InkWell( onTap: () => _switchInputType(context), @@ -741,7 +740,7 @@ class _RecoverButton extends StatelessWidget with _ButtonLogicMixin { ), if (!isDownloadFlow && inputType != KeyChainInputType.backupKey) ...[ - const Gap(10), + const Gap(20), InkWell( onTap: () => _switchToBackupKey(context), child: const BBText.bodySmall( @@ -750,7 +749,7 @@ class _RecoverButton extends StatelessWidget with _ButtonLogicMixin { ), ), ], - const Gap(8), + const Gap(10), BBButton.withColour( fillWidth: true, label: 'Recover with ${_getInputTypeText()}', From 9e4a4ed123f6753605baeaf349d206e20568781b Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 3 Mar 2025 16:14:46 -0500 Subject: [PATCH 388/401] feat: implement connect and disconnect methods in IRecoverbullManager --- lib/_pkg/recoverbull/_interface.dart | 6 ++++++ lib/_pkg/recoverbull/google_drive.dart | 4 +++- lib/_pkg/recoverbull/local.dart | 13 +++++++++++-- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/lib/_pkg/recoverbull/_interface.dart b/lib/_pkg/recoverbull/_interface.dart index 9fb4af909..d98397ef4 100644 --- a/lib/_pkg/recoverbull/_interface.dart +++ b/lib/_pkg/recoverbull/_interface.dart @@ -12,6 +12,12 @@ import 'package:hex/hex.dart'; import 'package:recoverbull/recoverbull.dart'; abstract class IRecoverbullManager { + /// Initialize connection and verify access + Future<(dynamic, Err?)> connect(); + + /// Clean up and disconnect + Future disconnect(); + /// Encrypts a list of backups using BIP85 derivation Future<(({String key, BullBackup backup})?, Err?)> createEncryptedBackup({ required List wallets, diff --git a/lib/_pkg/recoverbull/google_drive.dart b/lib/_pkg/recoverbull/google_drive.dart index 9b4401e5b..981c09d41 100644 --- a/lib/_pkg/recoverbull/google_drive.dart +++ b/lib/_pkg/recoverbull/google_drive.dart @@ -23,6 +23,7 @@ class GoogleDriveBackupManager extends IRecoverbullManager { DriveApi? _api; + @override Future<(DriveApi?, Err?)> connect() async { GoogleSignInAccount? account; try { @@ -51,6 +52,7 @@ class GoogleDriveBackupManager extends IRecoverbullManager { } } + @override Future disconnect() async { await _google.disconnect(); _api = null; @@ -105,7 +107,7 @@ class GoogleDriveBackupManager extends IRecoverbullManager { final file = File() ..name = filename ..mimeType = 'application/json' - ..parents = ['appDataFolder']; + ..parents = [backupFolder]; final jsonBackup = backup.toJson(); diff --git a/lib/_pkg/recoverbull/local.dart b/lib/_pkg/recoverbull/local.dart index 5f31d8412..18a63c0d7 100644 --- a/lib/_pkg/recoverbull/local.dart +++ b/lib/_pkg/recoverbull/local.dart @@ -8,10 +8,19 @@ import 'package:bb_mobile/locator.dart'; import 'package:recoverbull/recoverbull.dart'; class FileSystemBackupManager extends IRecoverbullManager { - final FileStorage fileStorage = locator(); - FileSystemBackupManager(); + @override + Future<(String?, Err?)> connect() async { + // Don't show picker here since path will be provided by cubit + return (null, null); + } + + @override + Future disconnect() async {} + + final FileStorage fileStorage = locator(); + /// Deletes the encrypted backup from the specified directory. /// Returns the path to the deleted backup or an error message. @override From 40997813a743e964e5ab4b50a495075346cfa71f Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 3 Mar 2025 16:16:11 -0500 Subject: [PATCH 389/401] feat: add BackupType enum and backupType field to BackupSettingsState --- lib/recoverbull/bloc/backup_settings_state.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/recoverbull/bloc/backup_settings_state.dart b/lib/recoverbull/bloc/backup_settings_state.dart index 53be51b14..82d27c174 100644 --- a/lib/recoverbull/bloc/backup_settings_state.dart +++ b/lib/recoverbull/bloc/backup_settings_state.dart @@ -1,7 +1,10 @@ import 'package:bb_mobile/_model/wallet_sensitive_data.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; + part 'backup_settings_state.freezed.dart'; +enum BackupType { fileSystem, googleDrive, iCloud } + @freezed class BackupSettingsState with _$BackupSettingsState { const factory BackupSettingsState({ @@ -27,6 +30,7 @@ class BackupSettingsState with _$BackupSettingsState { @Default('') String backupKey, @Default({}) Map latestRecoveredBackup, @Default(null) DateTime? lastBackupAttempt, + @Default(BackupType.fileSystem) BackupType backupType, }) = _BackupSettingsState; const BackupSettingsState._(); From 29bb6f834b81ebe5f6fdcc77bde89c13aa6a7d71 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 3 Mar 2025 16:17:30 -0500 Subject: [PATCH 390/401] feat: add method to set backup type and update backup manager selection --- lib/recoverbull/bloc/backup_settings_cubit.dart | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/recoverbull/bloc/backup_settings_cubit.dart b/lib/recoverbull/bloc/backup_settings_cubit.dart index 2e72b2ef9..e07152811 100644 --- a/lib/recoverbull/bloc/backup_settings_cubit.dart +++ b/lib/recoverbull/bloc/backup_settings_cubit.dart @@ -92,6 +92,15 @@ class BackupSettingsCubit extends Cubit { static const _kShuffleDelay = Duration(milliseconds: 500); static const _kMinBackupInterval = Duration(seconds: 5); + IRecoverbullManager get _backupManager => + state.backupType == BackupType.googleDrive + ? _googleDriveBackupManager + : _fileSystemBackupManager; + + void setBackupType(BackupType type) { + emit(state.copyWith(backupType: type)); + } + void changePassword(String password) { emit( state.copyWith( From 65535bcc92d0b3cfe02a8d00367e9ec094cf0684 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 3 Mar 2025 16:25:28 -0500 Subject: [PATCH 391/401] feat: add comments for backup-related methods --- .../bloc/backup_settings_cubit.dart | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/lib/recoverbull/bloc/backup_settings_cubit.dart b/lib/recoverbull/bloc/backup_settings_cubit.dart index e07152811..a28ae6de2 100644 --- a/lib/recoverbull/bloc/backup_settings_cubit.dart +++ b/lib/recoverbull/bloc/backup_settings_cubit.dart @@ -92,6 +92,7 @@ class BackupSettingsCubit extends Cubit { static const _kShuffleDelay = Duration(milliseconds: 500); static const _kMinBackupInterval = Duration(seconds: 5); + /// Gets the appropriate backup manager based on backup type IRecoverbullManager get _backupManager => state.backupType == BackupType.googleDrive ? _googleDriveBackupManager @@ -345,6 +346,9 @@ class BackupSettingsCubit extends Cubit { _emitBackupState(seed); } + /// Recovers wallet data from encrypted backup + /// [encrypted] - Encrypted backup data + /// [backupKey] - Key used to decrypt the backup Future recoverBackup(String encrypted, String backupKey) async { _emitSafe( state.copyWith( @@ -397,6 +401,8 @@ class BackupSettingsCubit extends Cubit { ); } + /// Derives backup key from mnemonic phrase + /// [derivationPath] - BIP32 derivation path for key generation Future recoverBackupKeyFromMnemonic(String? derivationPath) async { _emitSafe(state.copyWith(loadingBackups: true, errorLoadingBackups: '')); @@ -441,7 +447,9 @@ class BackupSettingsCubit extends Cubit { emit(state.copyWith(backupTested: false)); } - Future saveFileSystemBackup() async { + /// Saves encrypted backup to selected location (filesystem or Google Drive) + /// Handles backup key derivation and encryption process + Future saveBackup() async { if (!_canStartBackup()) { _handleSaveError('Please wait before attempting another backup'); return; @@ -634,6 +642,8 @@ class BackupSettingsCubit extends Cubit { emit(state.copyWith(testMnemonicOrder: testMnemonic)); } + /// Handles word selection during backup verification + /// Validates word order and updates test state void wordClicked(int shuffledIdx) { emit(state.copyWith(errTestingBackup: '')); final testMnemonic = state.testMnemonicOrder.toList(); @@ -657,6 +667,8 @@ class BackupSettingsCubit extends Cubit { emit(state.copyWith(testMnemonicOrder: testMnemonic)); } + /// Creates or updates wallet with provided parameters + /// Returns the created/updated wallet or error Future<(Wallet?, Err?)> _addOrUpdateWallet( BBNetwork network, BaseWalletType layer, @@ -721,6 +733,8 @@ class BackupSettingsCubit extends Cubit { } } + /// Checks if enough time has passed since last backup attempt + /// Prevents too frequent backup operations bool _canStartBackup() { final lastAttempt = state.lastBackupAttempt; if (lastAttempt != null) { @@ -766,6 +780,8 @@ class BackupSettingsCubit extends Cubit { } } + /// Creates encrypted backups for all wallets + /// Returns list of wallet sensitive data for backup Future> _createBackupsForAllWallets() async { final backups = []; @@ -782,6 +798,8 @@ class BackupSettingsCubit extends Cubit { } } + /// Creates backup data for a single wallet + /// Returns WalletSensitiveData containing encrypted wallet information Future _createBackupForWallet(Wallet wallet) async { try { final (seed, err) = await _loadWalletSeed(wallet); @@ -936,6 +954,7 @@ class BackupSettingsCubit extends Cubit { _ => null }; + /// Error handling helpers void _handleLoadError(String message, {bool loading = false}) { _emitSafe( state.copyWith( @@ -971,6 +990,8 @@ class BackupSettingsCubit extends Cubit { ); } + /// Loads seed data for given wallet + /// Returns seed or error if failed to load Future<(Seed?, Err?)> _loadWalletSeed(Wallet wallet) async { final (seed, err) = await _walletSensRepository.readSeed( fingerprintIndex: wallet.getRelatedSeedStorageString(), @@ -978,6 +999,8 @@ class BackupSettingsCubit extends Cubit { return (seed, err); } + /// Processes a single wallet backup during recovery + /// Creates or updates wallet with recovered data Future _processBackupRecovery(WalletSensitiveData backup) async { final network = BBNetwork.fromString(backup.network); final layer = _getLayer(backup.layer); @@ -1008,6 +1031,8 @@ class BackupSettingsCubit extends Cubit { return err; } + /// Updates wallet backup testing status + /// Propagates changes to wallet services and UI Future _updateWalletBackupStatus(Wallet updatedWallet) async { final service = _appWalletsRepository.getWalletServiceById(updatedWallet.id); @@ -1027,6 +1052,7 @@ class BackupSettingsCubit extends Cubit { } } + /// Verification helper functions bool _verifyPassphrase(Seed seed, String password) { final storedPassphrase = seed .getPassphraseFromIndex(_currentWallet!.sourceFingerprint) From 3a7401b0a34b0cf7cb8bd50a31a1098b03b94554 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 3 Mar 2025 16:26:19 -0500 Subject: [PATCH 392/401] refactor: code optimized --- .../bloc/backup_settings_cubit.dart | 437 +++++++----------- 1 file changed, 173 insertions(+), 264 deletions(-) diff --git a/lib/recoverbull/bloc/backup_settings_cubit.dart b/lib/recoverbull/bloc/backup_settings_cubit.dart index a28ae6de2..a3c06e538 100644 --- a/lib/recoverbull/bloc/backup_settings_cubit.dart +++ b/lib/recoverbull/bloc/backup_settings_cubit.dart @@ -4,8 +4,10 @@ import 'dart:convert'; import 'package:bb_mobile/_model/seed.dart'; import 'package:bb_mobile/_model/wallet.dart'; import 'package:bb_mobile/_model/wallet_sensitive_data.dart'; +import 'package:bb_mobile/_pkg/consts/configs.dart'; import 'package:bb_mobile/_pkg/error.dart'; import 'package:bb_mobile/_pkg/file_picker.dart'; +import 'package:bb_mobile/_pkg/recoverbull/_interface.dart'; import 'package:bb_mobile/_pkg/recoverbull/google_drive.dart'; import 'package:bb_mobile/_pkg/recoverbull/local.dart'; import 'package:bb_mobile/_pkg/wallet/bdk/sensitive_create.dart'; @@ -140,122 +142,83 @@ class BackupSettingsCubit extends Cubit { ); } - Future connectToGoogleDrive() async { - try { - final (api, err) = await _googleDriveBackupManager.connect(); - if (err != null) { - _emitBackupError('Failed to connect to Google Drive: ${err.message}'); + Future deleteBackup([String? path]) async { + if (state.backupType == BackupType.fileSystem) { + if (_filePicker == null) return; + + final (file, error) = await _filePicker.pickFile(); + if (error != null || file == null) { + _handleLoadError(error?.message ?? 'No file selected'); return; } - _emitSafe(state.copyWith(errorSavingBackups: '')); - } catch (e) { - _emitBackupError('Google Drive connection error: $e'); - } - } - - void disconnectGoogleDrive() { - _googleDriveBackupManager.disconnect(); - emit(state.copyWith(backupFolderPath: '')); - } - - Future deleteFsBackup() async { - if (_filePicker == null) return; - - final (file, error) = await _filePicker.pickFile(); - - if (error != null) { - debugPrint('Error picking the file: ${error.message}'); - emit(state.copyWith(errorLoadingBackups: "Error picking file")); - return; - } - if (file == null) { - emit(state.copyWith(errorLoadingBackups: 'Corrupted backup file')); + path = file.path; + } else if (path?.isEmpty ?? true) { + _handleSaveError('No backup to delete'); return; } - final (deleted, err) = await _fileSystemBackupManager.removeEncryptedBackup( - path: file.path, + final (deleted, err) = await _backupManager.removeEncryptedBackup( + path: path!, ); if (err != null) { - emit(state.copyWith(errorSavingBackups: 'Failed to delete backup')); + _handleSaveError('Failed to delete backup: ${err.message}'); return; } emit(state.copyWith(backupFolderPath: '')); } - Future deleteGoogleDriveBackup(String path) async { - if (state.backupFolderPath.isEmpty) { - emit(state.copyWith(errorSavingBackups: 'No backup to delete')); - return; - } - - final (deleted, err) = - await _googleDriveBackupManager.removeEncryptedBackup(path: path); + /// Fetches and processes backup data from either filesystem or Google Drive + /// [forceRefresh] - If true, forces reload of backup data even if cached + Future fetchBackup({bool forceRefresh = false}) async { + try { + _emitSafe(state.copyWith(loadingBackups: true)); - if (err != null) { - emit(state.copyWith(errorSavingBackups: 'Failed to delete backup')); - return; - } + final (_, connectErr) = await _backupManager.connect(); + if (connectErr != null) { + _handleLoadError(connectErr.message); + return; + } - emit(state.copyWith(backupFolderPath: '')); - } + if (state.backupType == BackupType.fileSystem) { + if (_filePicker == null) return; - Future fetchFsBackup() async { - if (_filePicker == null) return; + final (file, error) = await _filePicker.pickFile(); + if (error != null || file == null) { + _handleLoadError(error?.message ?? 'No file selected'); + return; + } - final (file, error) = await _filePicker.pickFile(); + final fileContent = await file.readAsString(); + final (loadedBackup, err) = _backupManager.loadEncryptedBackup( + file: fileContent, + ); - if (error != null) { - emit(state.copyWith(errorLoadingBackups: "Error picking file")); - return; - } - final fileContent = await file?.readAsString(); - if (file == null || fileContent == null) { - emit(state.copyWith(errorLoadingBackups: 'Corrupted backup file')); - return; - } - final (loadedBackup, err) = - _fileSystemBackupManager.loadEncryptedBackup(file: fileContent); - if (loadedBackup != null) { - emit( - state.copyWith( - loadingBackups: false, - latestRecoveredBackup: loadedBackup.toMap(), - lastBackupAttempt: DateTime.now(), - ), - ); - return; - } else if (err != null) { - debugPrint('Error loading backups: ${err.message}'); - emit( - state.copyWith( - loadingBackups: false, - errorLoadingBackups: "Corrupted backup file", - ), - ); - return; - } - } + if (err != null || loadedBackup == null) { + _handleLoadError(err?.message ?? 'Failed to load backup'); + return; + } - Future fetchGoogleDriveBackup({bool forceRefresh = false}) async { - try { - if (!forceRefresh && state.loadedBackups.isNotEmpty) { - emit(state.copyWith(loadingBackups: false)); + _emitSafe( + state.copyWith( + loadingBackups: false, + latestRecoveredBackup: loadedBackup.toMap(), + lastBackupAttempt: DateTime.now(), + ), + ); return; } - _emitSafe(state.copyWith(loadingBackups: true)); - - final (api, connectErr) = await _googleDriveBackupManager.connect(); - if (connectErr != null) { - _handleLoadError(connectErr.message); + // Google Drive fetch logic + if (!forceRefresh && state.loadedBackups.isNotEmpty) { + emit(state.copyWith(loadingBackups: false)); return; } final (availableBackups, err) = - await _googleDriveBackupManager.loadAllEncryptedBackupFiles(); + await (_backupManager as GoogleDriveBackupManager) + .loadAllEncryptedBackupFiles(); if (err != null) { debugPrint('Error loading backups: ${err.message}'); @@ -263,59 +226,63 @@ class BackupSettingsCubit extends Cubit { return; } - if (availableBackups != null && availableBackups.isNotEmpty) { - final latestBackup = availableBackups.reduce((a, b) { - final aTime = a.createdTime; - final bTime = b.createdTime; - if (aTime == null) return b; - if (bTime == null) return a; - return aTime.compareTo(bTime) > 0 ? a : b; - }); - - final backupId = latestBackup.name?.split('_').last.split('.').first; - if (backupId == null) { - _handleLoadError("Corrupted backup file"); - return; - } + if (availableBackups == null || availableBackups.isEmpty) { + _handleLoadError("No backup files found"); + return; + } - final (loadedBackupMetaData, mediaErr) = - await _googleDriveBackupManager.fetchMediaStream( - file: latestBackup, - ); + // Get latest backup by creation time + final latestBackup = availableBackups.reduce((a, b) { + final aTime = a.createdTime; + final bTime = b.createdTime; + if (aTime == null) return b; + if (bTime == null) return a; + return aTime.compareTo(bTime) > 0 ? a : b; + }); + + final backupId = latestBackup.name?.split('_').last.split('.').first; + if (backupId == null) { + _handleLoadError("Corrupted backup file name"); + return; + } - if (mediaErr != null || loadedBackupMetaData == null) { - debugPrint('Error loading backups: ${mediaErr?.message}'); - _handleLoadError("Failed to load backup data"); - return; - } + final (loadedBackupMetaData, mediaErr) = + await (_backupManager as GoogleDriveBackupManager).fetchMediaStream( + file: latestBackup, + ); - final (backup, err) = _googleDriveBackupManager.loadEncryptedBackup( - file: utf8.decode(loadedBackupMetaData), - ); - if (backup != null) { - final backupMap = backup.toMap(); - backupMap.addAll({ - 'source': 'drive', - 'filename': latestBackup.name, - }); - - emit( - state.copyWith( - loadingBackups: false, - latestRecoveredBackup: backupMap, - lastBackupAttempt: DateTime.now(), - ), - ); - return; - } else if (err != null) { - debugPrint('Error loading backups: ${err.message}'); - _handleLoadError("Corrupted backup file"); - return; - } - } else { - _handleLoadError("Failed to get backup files"); + if (mediaErr != null || loadedBackupMetaData == null) { + debugPrint('Error loading backup data: ${mediaErr?.message}'); + _handleLoadError("Failed to load backup data"); + return; } + + final (backup, decodeErr) = _backupManager.loadEncryptedBackup( + file: utf8.decode(loadedBackupMetaData), + ); + + if (decodeErr != null || backup == null) { + debugPrint('Error decoding backup: ${decodeErr?.message}'); + _handleLoadError("Corrupted backup file"); + return; + } + + final backupMap = backup.toMap(); + backupMap.addAll({ + 'source': 'drive', + 'filename': latestBackup.name, + }); + + _emitSafe( + state.copyWith( + loadingBackups: false, + latestRecoveredBackup: backupMap, + lastBackupAttempt: DateTime.now(), + errorLoadingBackups: '', + ), + ); } catch (e) { + debugPrint('Fetch backup error: $e'); _handleLoadError('Failed to fetch backup: $e'); } } @@ -456,118 +423,84 @@ class BackupSettingsCubit extends Cubit { } _emitSafe(state.copyWith(savingBackups: true, errorSavingBackups: '')); - if (_wallets.isEmpty) { - _handleLoadError('No wallets available for backup'); - return; - } - final backups = await _createBackupsForAllWallets(); - if (backups.isEmpty) { - _handleSaveError('Failed to create backups'); - return; - } - - final (result, err) = await _createBackup(backups); - if (err != null || result == null) { - _handleSaveError(err?.message ?? 'Encryption failed'); - return; - } - - final backup = result.backup; - final backupKey = result.key; - - final (savePath, pickErr) = await _filePicker?.getDirectoryPath() ?? - (null, Err('File picker not initialized')); - if (pickErr != null) { - _handleSaveError('Failed to select backup location: ${pickErr.message}'); - return; - } - - if (savePath == null || savePath.isEmpty) { - _handleSaveError('No location selected for backup'); - return; - } - - final (filePath, saveErr) = - await _fileSystemBackupManager.saveEncryptedBackup( - backup: backup, - backupFolder: savePath, - ); - - if (saveErr != null) { - _handleSaveError('Save failed: ${saveErr.message}'); - return; - } - _emitSafe( - state.copyWith( - backupId: backup.id, - backupKey: backupKey, - backupFolderPath: filePath ?? '', - backupSalt: backup.salt, - savingBackups: false, - lastBackupAttempt: DateTime.now(), - ), - ); - } - - Future saveGoogleDriveBackup() async { - if (!_canStartBackup()) { - _handleSaveError('Please wait before attempting another backup'); - return; - } - - _emitSafe(state.copyWith(savingBackups: true, errorSavingBackups: '')); + try { + // Get the file path first if needed for filesystem backup + String? savePath; + + if (state.backupType == BackupType.fileSystem) { + // For filesystem backup, get directory first + final (path, pickErr) = await _filePicker?.getDirectoryPath() ?? + (null, Err('File picker not initialized')); + if (pickErr != null || path == null) { + _handleSaveError(pickErr?.message ?? 'No location selected'); + return; + } + savePath = path; + } else { + // For other backup types, connect normally + final (result, connectErr) = await _backupManager.connect(); + if (connectErr != null) { + _handleSaveError(connectErr.message); + return; + } + } - if (_wallets.isEmpty) { - _handleLoadError('No wallets available for backup'); - return; - } + // Create and validate backups + final backups = await _createBackupsForAllWallets(); + if (backups.isEmpty) { + _handleSaveError('No wallets to backup'); + return; + } - final (api, connectErr) = await _googleDriveBackupManager.connect(); - if (connectErr != null) { - _handleSaveError(connectErr.message); - return; - } + // Get main seed for backup key derivation + final (mainSeed, fetchMainSeedErr) = await _fetchMainSeed(); + if (fetchMainSeedErr != null || mainSeed == null) { + _handleSaveError('Failed to get main seed'); + return; + } - final backups = await _createBackupsForAllWallets(); - if (backups.isEmpty) { - _handleSaveError('Failed to create backups'); - return; - } + // Create encrypted backup + final (result, err) = await _backupManager.createEncryptedBackup( + wallets: backups, + mnemonic: mainSeed.mnemonic.split(' '), + network: mainSeed.network.toString().toLowerCase(), + ); - final (result, encryptErr) = await _createBackup(backups); - if (encryptErr != null || result == null) { - _handleSaveError(encryptErr?.message ?? 'Encryption failed'); - return; - } + if (err != null || result == null) { + _handleSaveError(err?.message ?? 'Failed to create backup'); + return; + } - final backupKey = result.key; - final backup = result.backup; + // Save backup to selected location + final backupFolder = state.backupType == BackupType.fileSystem + ? savePath ?? defaultBackupPath + : 'appDataFolder'; - final (filePath, saveErr) = - await _googleDriveBackupManager.saveEncryptedBackup(backup: backup); + final (filePath, saveErr) = await _backupManager.saveEncryptedBackup( + backup: result.backup, + backupFolder: backupFolder, + ); - if (saveErr != null) { - _handleSaveError('Failed to save to Google Drive: ${saveErr.message}'); - return; - } + if (saveErr != null) { + _handleSaveError('Save failed: ${saveErr.message}'); + return; + } - final filename = filePath?.split('/').last; - if (filename == null) { - _handleSaveError('filename is null'); - return; + // Update state with success + _emitSafe( + state.copyWith( + backupId: result.backup.id, + backupKey: result.key, + backupFolderPath: filePath ?? '', + backupSalt: result.backup.salt, + savingBackups: false, + lastBackupAttempt: DateTime.now(), + ), + ); + } catch (e) { + _handleSaveError('Backup failed: $e'); } - - _emitSafe( - state.copyWith( - backupId: backup.id, - backupKey: backupKey, - backupFolderPath: filename, - backupSalt: backup.salt, - savingBackups: false, - lastBackupAttempt: DateTime.now(), - ), - ); } Future testBackupClicked() async { @@ -887,31 +820,7 @@ class BackupSettingsCubit extends Cubit { if (!isClosed) emit(newState); } - Future<(({String key, BullBackup backup})?, Err?)> _createBackup( - List wallets, - ) async { - try { - final (mainSeed, fetchMainMnemonicErr) = await _fetchMainSeed(); - if (fetchMainMnemonicErr != null || mainSeed == null) { - return (null, fetchMainMnemonicErr); - } - final (backup, err) = - await _fileSystemBackupManager.createEncryptedBackup( - wallets: wallets, - mnemonic: mainSeed.mnemonic.split(' '), - network: mainSeed.network.toString().toLowerCase(), - ); - - if (err != null || backup == null) { - return (null, err); - } - - return (backup, null); - } catch (e) { - return (null, Err(e.toString())); - } - } - + /// Fetches main wallet seed used for backup key derivation Future<(Seed?, Err?)> _fetchMainSeed() async { final mainWallet = _wallets.firstWhere( (wallet) => From 838f078e4bb48c8ae3df638c0c265b16d06de537 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 3 Mar 2025 16:28:18 -0500 Subject: [PATCH 393/401] feat: add BackupKeyDialog for displaying and copying backup key --- lib/recoverbull/backup_key.dart | 37 +++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/lib/recoverbull/backup_key.dart b/lib/recoverbull/backup_key.dart index 091fcc7d8..8e2ecfea7 100644 --- a/lib/recoverbull/backup_key.dart +++ b/lib/recoverbull/backup_key.dart @@ -422,3 +422,40 @@ class _BackupKeyInfoPage extends State { ); } } + +class _BackupKeyDialog extends StatelessWidget { + final String backupKey; + + const _BackupKeyDialog({required this.backupKey}); + + @override + Widget build(BuildContext context) { + return AlertDialog( + backgroundColor: context.colour.primaryContainer, + title: const BBText.title('Backup key', isBold: true), + content: Row( + children: [ + Expanded( + child: Text( + backupKey, + style: + context.font.bodySmall!.copyWith(fontWeight: FontWeight.bold), + ), + ), + IconButton( + onPressed: () => _copyToClipboard(context), + icon: Icon(Icons.copy, color: context.colour.primary), + ), + ], + ), + ); + } + + void _copyToClipboard(BuildContext context) { + Clipboard.setData(ClipboardData(text: backupKey)); + Navigator.of(context).pop(); + ScaffoldMessenger.of(context).showSnackBar( + context.showToast('Copied to clipboard'), + ); + } +} From 10c81019221e6aa4b6a74c510fcccba2454271cb Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 3 Mar 2025 16:28:40 -0500 Subject: [PATCH 394/401] refactor: rename backupSettingsCubit to cubit --- lib/recoverbull/backup_key.dart | 97 +++++++++++++-------------------- 1 file changed, 39 insertions(+), 58 deletions(-) diff --git a/lib/recoverbull/backup_key.dart b/lib/recoverbull/backup_key.dart index 8e2ecfea7..b2e3680b7 100644 --- a/lib/recoverbull/backup_key.dart +++ b/lib/recoverbull/backup_key.dart @@ -26,35 +26,32 @@ class BackupKeyPage extends StatefulWidget { } class _BackupKeyPageState extends State { - late final BackupSettingsCubit _backupSettingsCubit; + late final BackupSettingsCubit _cubit; @override void initState() { super.initState(); - _backupSettingsCubit = createBackupSettingsCubit(walletId: widget.wallet); + _cubit = createBackupSettingsCubit(walletId: widget.wallet); + _initializeKeyServer(); + } + + void _initializeKeyServer() { context.read().keyServerStatus(); } @override void dispose() { - _backupSettingsCubit.close(); + _cubit.close(); super.dispose(); } - Future _handleRecover( - BuildContext context, - BackupProvider provider, - ) async { - switch (provider) { - case BackupProvider.googleDrive: - await _backupSettingsCubit.fetchGoogleDriveBackup(); - case BackupProvider.iCloud: - debugPrint('iCloud backup'); - case BackupProvider.custom: - _backupSettingsCubit.fetchFsBackup(); - } + Future _handleRecover(BackupProvider provider) async { + await provider.handleRecover(_cubit); } + Widget _buildLoadingState() => + const Center(child: CircularProgressIndicator()); + Widget _buildContent(BuildContext context, BackupSettingsState state) { return Column( children: [ @@ -73,7 +70,7 @@ class _BackupKeyPageState extends State { title: provider.title, description: provider.description, icon: Icon(provider.icon, size: 40), - onTap: () => _handleRecover(context, provider), + onTap: () => _handleRecover(provider), ), ), ), @@ -98,7 +95,7 @@ class _BackupKeyPageState extends State { previous.keyServerUp != current.keyServerUp || current.loading != previous.loading, child: BlocProvider.value( - value: _backupSettingsCubit, + value: _cubit, child: BlocConsumer( listenWhen: (previous, current) => previous.errorLoadingBackups != current.errorLoadingBackups || @@ -108,7 +105,7 @@ class _BackupKeyPageState extends State { ScaffoldMessenger.of(context).showSnackBar( context.showToast(state.errorLoadingBackups), ); - _backupSettingsCubit.clearError(); + _cubit.clearError(); return; } if (state.latestRecoveredBackup.isNotEmpty) { @@ -116,7 +113,7 @@ class _BackupKeyPageState extends State { '/wallet-settings/backup-settings/key/options', extra: ('', state.latestRecoveredBackup), ); - _backupSettingsCubit.clearError(); + _cubit.clearError(); } }, builder: (context, state) { @@ -131,7 +128,7 @@ class _BackupKeyPageState extends State { ), ), body: state.loadingBackups - ? const Center(child: CircularProgressIndicator()) + ? _buildLoadingState() : _buildContent(context, state), ); }, @@ -156,64 +153,48 @@ class BackupKeyOptionsPage extends StatefulWidget { } class _BackupKeyInfoPage extends State { - late final BackupSettingsCubit _backupSettingsCubit; + late final BackupSettingsCubit _cubit; @override void initState() { super.initState(); - _backupSettingsCubit = createBackupSettingsCubit(); + _cubit = createBackupSettingsCubit(); + _initializeKeyServer(); + } + + void _initializeKeyServer() { context.read().keyServerStatus(); } @override void dispose() { - _backupSettingsCubit.close(); + _cubit.close(); super.dispose(); } - void _handleBackupAction(BuildContext context) { + Future _handleBackupAction(BuildContext context) async { if (widget.backupKey.isNotEmpty) { _showBackupKeyDialog(context, widget.backupKey); } else { - context.push( - '/wallet-settings/backup-settings/keychain', - extra: ( - '', - widget.recoveredBackup, - KeyChainPageState.download.name.toLowerCase() - ), - ); + _navigateToKeychain(context); } } + void _navigateToKeychain(BuildContext context) { + context.push( + '/wallet-settings/backup-settings/keychain', + extra: ( + '', + widget.recoveredBackup, + KeyChainPageState.download.name.toLowerCase(), + ), + ); + } + void _showBackupKeyDialog(BuildContext context, String backupKey) { showDialog( context: context, - builder: (context) => AlertDialog( - backgroundColor: context.colour.primaryContainer, - title: const BBText.title('Backup key', isBold: true), - content: Row( - children: [ - Expanded( - child: Text( - backupKey, - style: context.font.bodySmall! - .copyWith(fontWeight: FontWeight.bold), - ), - ), - IconButton( - onPressed: () { - Clipboard.setData(ClipboardData(text: backupKey)); - Navigator.of(context).pop(); - ScaffoldMessenger.of(context).showSnackBar( - context.showToast('Copied to clipboard'), - ); - }, - icon: Icon(Icons.copy, color: context.colour.primary), - ), - ], - ), - ), + builder: (context) => _BackupKeyDialog(backupKey: backupKey), ); } @@ -279,7 +260,7 @@ class _BackupKeyInfoPage extends State { } return MultiBlocProvider( - providers: [BlocProvider.value(value: _backupSettingsCubit)], + providers: [BlocProvider.value(value: _cubit)], child: Builder( builder: (context) { return BlocBuilder( From 025e5531c210217706ae19b98cd2529c84725cc7 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Mon, 3 Mar 2025 16:29:06 -0500 Subject: [PATCH 395/401] feat: add handleBackup and handleRecover methods to BackupProvider enum --- lib/recoverbull/encrypted_vault_backup.dart | 39 ++++++++++++--------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/lib/recoverbull/encrypted_vault_backup.dart b/lib/recoverbull/encrypted_vault_backup.dart index 072272a90..f96f59eec 100644 --- a/lib/recoverbull/encrypted_vault_backup.dart +++ b/lib/recoverbull/encrypted_vault_backup.dart @@ -30,6 +30,27 @@ enum BackupProvider { final IconData icon; const BackupProvider(this.title, this.description, this.icon); + + Future handleBackup(BackupSettingsCubit cubit) async { + cubit.setBackupType(_getBackupType()); + await cubit.saveBackup(); + } + + Future handleRecover(BackupSettingsCubit cubit) async { + cubit.setBackupType(_getBackupType()); + await cubit.fetchBackup(); + } + + BackupType _getBackupType() { + switch (this) { + case BackupProvider.googleDrive: + return BackupType.googleDrive; + case BackupProvider.iCloud: + return BackupType.iCloud; + case BackupProvider.custom: + return BackupType.fileSystem; + } + } } class EncryptedVaultBackupPage extends StatefulWidget { @@ -59,14 +80,7 @@ class _EncryptedVaultBackupPageState extends State { BuildContext context, BackupProvider provider, ) async { - switch (provider) { - case BackupProvider.googleDrive: - await _cubit.saveGoogleDriveBackup(); - case BackupProvider.iCloud: - debugPrint('iCloud backup'); - case BackupProvider.custom: - _cubit.saveFileSystemBackup(); - } + await provider.handleBackup(_cubit); } @override @@ -307,14 +321,7 @@ class _EncryptedVaultRecoverPageState extends State { BuildContext context, BackupProvider provider, ) async { - switch (provider) { - case BackupProvider.googleDrive: - await _backupSettingsCubit.fetchGoogleDriveBackup(); - case BackupProvider.iCloud: - debugPrint('iCloud backup'); - case BackupProvider.custom: - _backupSettingsCubit.fetchFsBackup(); - } + await provider.handleRecover(_backupSettingsCubit); } Widget _buildContent(BuildContext context, BackupSettingsState state) { From e1f79539f8ec9331608ef23515b03621977fd65f Mon Sep 17 00:00:00 2001 From: ethicnology Date: Mon, 3 Mar 2025 18:23:03 -0500 Subject: [PATCH 396/401] refactor: remove BackupType, getBackupType, setBackupType connect, disconnect, iconColor --- lib/_pkg/recoverbull/_interface.dart | 6 - lib/_pkg/recoverbull/google_drive.dart | 2 - lib/_pkg/recoverbull/local.dart | 9 - .../bloc/backup_settings_cubit.dart | 185 +++++++++--------- .../bloc/backup_settings_state.dart | 3 - lib/recoverbull/encrypted_vault_backup.dart | 45 ++--- lib/recoverbull/keychain_page.dart | 16 +- 7 files changed, 124 insertions(+), 142 deletions(-) diff --git a/lib/_pkg/recoverbull/_interface.dart b/lib/_pkg/recoverbull/_interface.dart index d98397ef4..9fb4af909 100644 --- a/lib/_pkg/recoverbull/_interface.dart +++ b/lib/_pkg/recoverbull/_interface.dart @@ -12,12 +12,6 @@ import 'package:hex/hex.dart'; import 'package:recoverbull/recoverbull.dart'; abstract class IRecoverbullManager { - /// Initialize connection and verify access - Future<(dynamic, Err?)> connect(); - - /// Clean up and disconnect - Future disconnect(); - /// Encrypts a list of backups using BIP85 derivation Future<(({String key, BullBackup backup})?, Err?)> createEncryptedBackup({ required List wallets, diff --git a/lib/_pkg/recoverbull/google_drive.dart b/lib/_pkg/recoverbull/google_drive.dart index 981c09d41..4bf9bc9d1 100644 --- a/lib/_pkg/recoverbull/google_drive.dart +++ b/lib/_pkg/recoverbull/google_drive.dart @@ -23,7 +23,6 @@ class GoogleDriveBackupManager extends IRecoverbullManager { DriveApi? _api; - @override Future<(DriveApi?, Err?)> connect() async { GoogleSignInAccount? account; try { @@ -52,7 +51,6 @@ class GoogleDriveBackupManager extends IRecoverbullManager { } } - @override Future disconnect() async { await _google.disconnect(); _api = null; diff --git a/lib/_pkg/recoverbull/local.dart b/lib/_pkg/recoverbull/local.dart index 18a63c0d7..b1e75d952 100644 --- a/lib/_pkg/recoverbull/local.dart +++ b/lib/_pkg/recoverbull/local.dart @@ -10,15 +10,6 @@ import 'package:recoverbull/recoverbull.dart'; class FileSystemBackupManager extends IRecoverbullManager { FileSystemBackupManager(); - @override - Future<(String?, Err?)> connect() async { - // Don't show picker here since path will be provided by cubit - return (null, null); - } - - @override - Future disconnect() async {} - final FileStorage fileStorage = locator(); /// Deletes the encrypted backup from the specified directory. diff --git a/lib/recoverbull/bloc/backup_settings_cubit.dart b/lib/recoverbull/bloc/backup_settings_cubit.dart index a3c06e538..b08df8387 100644 --- a/lib/recoverbull/bloc/backup_settings_cubit.dart +++ b/lib/recoverbull/bloc/backup_settings_cubit.dart @@ -95,21 +95,16 @@ class BackupSettingsCubit extends Cubit { static const _kMinBackupInterval = Duration(seconds: 5); /// Gets the appropriate backup manager based on backup type - IRecoverbullManager get _backupManager => - state.backupType == BackupType.googleDrive - ? _googleDriveBackupManager - : _fileSystemBackupManager; - void setBackupType(BackupType type) { - emit(state.copyWith(backupType: type)); + IRecoverbullManager _getBackupManager(Type type) { + if (type == GoogleDriveBackupManager) return _googleDriveBackupManager; + if (type == FileSystemBackupManager) return _fileSystemBackupManager; + throw Exception('Unknown backup manager'); } void changePassword(String password) { emit( - state.copyWith( - testBackupPassword: password, - errTestingBackup: '', - ), + state.copyWith(testBackupPassword: password, errTestingBackup: ''), ); } @@ -142,8 +137,10 @@ class BackupSettingsCubit extends Cubit { ); } - Future deleteBackup([String? path]) async { - if (state.backupType == BackupType.fileSystem) { + Future deleteBackup({required Type manager, String? path}) async { + final backupManager = _getBackupManager(manager); + + if (backupManager is FileSystemBackupManager) { if (_filePicker == null) return; final (file, error) = await _filePicker.pickFile(); @@ -157,9 +154,8 @@ class BackupSettingsCubit extends Cubit { return; } - final (deleted, err) = await _backupManager.removeEncryptedBackup( - path: path!, - ); + final (deleted, err) = + await backupManager.removeEncryptedBackup(path: path!); if (err != null) { _handleSaveError('Failed to delete backup: ${err.message}'); @@ -171,17 +167,16 @@ class BackupSettingsCubit extends Cubit { /// Fetches and processes backup data from either filesystem or Google Drive /// [forceRefresh] - If true, forces reload of backup data even if cached - Future fetchBackup({bool forceRefresh = false}) async { + Future fetchBackup({ + required Type manager, + bool forceRefresh = false, + }) async { try { - _emitSafe(state.copyWith(loadingBackups: true)); + final backupManager = _getBackupManager(manager); - final (_, connectErr) = await _backupManager.connect(); - if (connectErr != null) { - _handleLoadError(connectErr.message); - return; - } + _emitSafe(state.copyWith(loadingBackups: true)); - if (state.backupType == BackupType.fileSystem) { + if (backupManager is FileSystemBackupManager) { if (_filePicker == null) return; final (file, error) = await _filePicker.pickFile(); @@ -191,7 +186,7 @@ class BackupSettingsCubit extends Cubit { } final fileContent = await file.readAsString(); - final (loadedBackup, err) = _backupManager.loadEncryptedBackup( + final (loadedBackup, err) = backupManager.loadEncryptedBackup( file: fileContent, ); @@ -210,77 +205,81 @@ class BackupSettingsCubit extends Cubit { return; } - // Google Drive fetch logic - if (!forceRefresh && state.loadedBackups.isNotEmpty) { - emit(state.copyWith(loadingBackups: false)); - return; - } + if (backupManager is GoogleDriveBackupManager) { + final (_, connectErr) = await backupManager.connect(); + if (connectErr != null) { + _handleLoadError(connectErr.message); + return; + } - final (availableBackups, err) = - await (_backupManager as GoogleDriveBackupManager) - .loadAllEncryptedBackupFiles(); + if (!forceRefresh && state.loadedBackups.isNotEmpty) { + emit(state.copyWith(loadingBackups: false)); + return; + } - if (err != null) { - debugPrint('Error loading backups: ${err.message}'); - _handleLoadError("Failed to get backup files"); - return; - } + final (availableBackups, err) = + await backupManager.loadAllEncryptedBackupFiles(); - if (availableBackups == null || availableBackups.isEmpty) { - _handleLoadError("No backup files found"); - return; - } + if (err != null) { + debugPrint('Error loading backups: ${err.message}'); + _handleLoadError("Failed to get backup files"); + return; + } - // Get latest backup by creation time - final latestBackup = availableBackups.reduce((a, b) { - final aTime = a.createdTime; - final bTime = b.createdTime; - if (aTime == null) return b; - if (bTime == null) return a; - return aTime.compareTo(bTime) > 0 ? a : b; - }); - - final backupId = latestBackup.name?.split('_').last.split('.').first; - if (backupId == null) { - _handleLoadError("Corrupted backup file name"); - return; - } + if (availableBackups == null || availableBackups.isEmpty) { + _handleLoadError("No backup files found"); + return; + } - final (loadedBackupMetaData, mediaErr) = - await (_backupManager as GoogleDriveBackupManager).fetchMediaStream( - file: latestBackup, - ); + // Get latest backup by creation time + final latestBackup = availableBackups.reduce((a, b) { + final aTime = a.createdTime; + final bTime = b.createdTime; + if (aTime == null) return b; + if (bTime == null) return a; + return aTime.compareTo(bTime) > 0 ? a : b; + }); + + final backupId = latestBackup.name?.split('_').last.split('.').first; + if (backupId == null) { + _handleLoadError("Corrupted backup file name"); + return; + } - if (mediaErr != null || loadedBackupMetaData == null) { - debugPrint('Error loading backup data: ${mediaErr?.message}'); - _handleLoadError("Failed to load backup data"); - return; - } + final (loadedBackupMetaData, mediaErr) = + await backupManager.fetchMediaStream(file: latestBackup); - final (backup, decodeErr) = _backupManager.loadEncryptedBackup( - file: utf8.decode(loadedBackupMetaData), - ); + if (mediaErr != null || loadedBackupMetaData == null) { + debugPrint('Error loading backup data: ${mediaErr?.message}'); + _handleLoadError("Failed to load backup data"); + return; + } - if (decodeErr != null || backup == null) { - debugPrint('Error decoding backup: ${decodeErr?.message}'); - _handleLoadError("Corrupted backup file"); - return; - } + final (backup, decodeErr) = backupManager.loadEncryptedBackup( + file: utf8.decode(loadedBackupMetaData), + ); - final backupMap = backup.toMap(); - backupMap.addAll({ - 'source': 'drive', - 'filename': latestBackup.name, - }); + if (decodeErr != null || backup == null) { + debugPrint('Error decoding backup: ${decodeErr?.message}'); + _handleLoadError("Corrupted backup file"); + return; + } - _emitSafe( - state.copyWith( - loadingBackups: false, - latestRecoveredBackup: backupMap, - lastBackupAttempt: DateTime.now(), - errorLoadingBackups: '', - ), - ); + final backupMap = backup.toMap(); + backupMap.addAll({ + 'source': 'drive', + 'filename': latestBackup.name, + }); + + _emitSafe( + state.copyWith( + loadingBackups: false, + latestRecoveredBackup: backupMap, + lastBackupAttempt: DateTime.now(), + errorLoadingBackups: '', + ), + ); + } } catch (e) { debugPrint('Fetch backup error: $e'); _handleLoadError('Failed to fetch backup: $e'); @@ -416,7 +415,9 @@ class BackupSettingsCubit extends Cubit { /// Saves encrypted backup to selected location (filesystem or Google Drive) /// Handles backup key derivation and encryption process - Future saveBackup() async { + Future saveBackup({required Type manager}) async { + final backupManager = _getBackupManager(manager); + if (!_canStartBackup()) { _handleSaveError('Please wait before attempting another backup'); return; @@ -428,7 +429,7 @@ class BackupSettingsCubit extends Cubit { // Get the file path first if needed for filesystem backup String? savePath; - if (state.backupType == BackupType.fileSystem) { + if (backupManager is FileSystemBackupManager) { // For filesystem backup, get directory first final (path, pickErr) = await _filePicker?.getDirectoryPath() ?? (null, Err('File picker not initialized')); @@ -437,9 +438,9 @@ class BackupSettingsCubit extends Cubit { return; } savePath = path; - } else { + } else if (backupManager is GoogleDriveBackupManager) { // For other backup types, connect normally - final (result, connectErr) = await _backupManager.connect(); + final (result, connectErr) = await backupManager.connect(); if (connectErr != null) { _handleSaveError(connectErr.message); return; @@ -461,7 +462,7 @@ class BackupSettingsCubit extends Cubit { } // Create encrypted backup - final (result, err) = await _backupManager.createEncryptedBackup( + final (result, err) = await backupManager.createEncryptedBackup( wallets: backups, mnemonic: mainSeed.mnemonic.split(' '), network: mainSeed.network.toString().toLowerCase(), @@ -473,11 +474,11 @@ class BackupSettingsCubit extends Cubit { } // Save backup to selected location - final backupFolder = state.backupType == BackupType.fileSystem + final backupFolder = backupManager is FileSystemBackupManager ? savePath ?? defaultBackupPath : 'appDataFolder'; - final (filePath, saveErr) = await _backupManager.saveEncryptedBackup( + final (filePath, saveErr) = await backupManager.saveEncryptedBackup( backup: result.backup, backupFolder: backupFolder, ); diff --git a/lib/recoverbull/bloc/backup_settings_state.dart b/lib/recoverbull/bloc/backup_settings_state.dart index 82d27c174..f9fd042c9 100644 --- a/lib/recoverbull/bloc/backup_settings_state.dart +++ b/lib/recoverbull/bloc/backup_settings_state.dart @@ -3,8 +3,6 @@ import 'package:freezed_annotation/freezed_annotation.dart'; part 'backup_settings_state.freezed.dart'; -enum BackupType { fileSystem, googleDrive, iCloud } - @freezed class BackupSettingsState with _$BackupSettingsState { const factory BackupSettingsState({ @@ -30,7 +28,6 @@ class BackupSettingsState with _$BackupSettingsState { @Default('') String backupKey, @Default({}) Map latestRecoveredBackup, @Default(null) DateTime? lastBackupAttempt, - @Default(BackupType.fileSystem) BackupType backupType, }) = _BackupSettingsState; const BackupSettingsState._(); diff --git a/lib/recoverbull/encrypted_vault_backup.dart b/lib/recoverbull/encrypted_vault_backup.dart index e3ee5747c..a82267c70 100644 --- a/lib/recoverbull/encrypted_vault_backup.dart +++ b/lib/recoverbull/encrypted_vault_backup.dart @@ -1,5 +1,7 @@ import 'dart:io'; +import 'package:bb_mobile/_pkg/recoverbull/google_drive.dart'; +import 'package:bb_mobile/_pkg/recoverbull/local.dart'; import 'package:bb_mobile/_ui/app_bar.dart'; import 'package:bb_mobile/_ui/components/button.dart'; import 'package:bb_mobile/_ui/components/text.dart'; @@ -21,39 +23,38 @@ import 'package:intl/intl.dart'; const double _kSpacing = 15.0; enum BackupProvider { - googleDrive('Google Drive', 'Easy', Icons.add_to_drive_rounded, - 'Your Google account information is never collected by Bull Bitcoin. It stays within the app and is not shared to our organization or to any third party.'), - iCloud('Apple iCloud', 'Easy', CupertinoIcons.cloud_upload, ''), - custom('Custom location', 'Private', Icons.folder, ''); + googleDrive( + GoogleDriveBackupManager, + 'Google Drive', + 'Easy', + Icons.add_to_drive_rounded, + 'Your Google account information is never collected by Bull Bitcoin. It stays within the app and is not shared to our organization or to any third party.', + ), + iCloud(Null, 'Apple iCloud', 'Easy', CupertinoIcons.cloud_upload, ''), + custom( + FileSystemBackupManager, + 'Custom location', + 'Private', + Icons.folder, + '', + ); final String title; final String description; final IconData icon; final String disclaimer; + final Type manager; - const BackupProvider(this.title, this.description, this.icon, this.disclaimer); + const BackupProvider( + this.manager, this.title, this.description, this.icon, this.disclaimer); Future handleBackup(BackupSettingsCubit cubit) async { - cubit.setBackupType(_getBackupType()); - await cubit.saveBackup(); + await cubit.saveBackup(manager: manager); } Future handleRecover(BackupSettingsCubit cubit) async { - cubit.setBackupType(_getBackupType()); - await cubit.fetchBackup(); - } - - BackupType _getBackupType() { - switch (this) { - case BackupProvider.googleDrive: - return BackupType.googleDrive; - case BackupProvider.iCloud: - return BackupType.iCloud; - case BackupProvider.custom: - return BackupType.fileSystem; - } + await cubit.fetchBackup(manager: manager); } - } class EncryptedVaultBackupPage extends StatefulWidget { @@ -87,7 +88,7 @@ class _EncryptedVaultBackupPageState extends State { _showDisclaimer(context, provider.disclaimer); await Future.delayed(const Duration(seconds: 3)); } - + await provider.handleBackup(_cubit); } diff --git a/lib/recoverbull/keychain_page.dart b/lib/recoverbull/keychain_page.dart index 5cdaa4b66..8cac51c2c 100644 --- a/lib/recoverbull/keychain_page.dart +++ b/lib/recoverbull/keychain_page.dart @@ -864,11 +864,13 @@ class _DeleteButton extends StatelessWidget with _ButtonLogicMixin { borderRadius: BorderRadius.circular(8), ), ), - child: Text('Delete', - style: context.font.bodyMedium!.copyWith( - color: Colors.white, - fontWeight: FontWeight.w900, - )), + child: Text( + 'Delete', + style: context.font.bodyMedium!.copyWith( + color: Colors.white, + fontWeight: FontWeight.w900, + ), + ), ), ], ), @@ -897,7 +899,6 @@ class _DialogBase extends StatelessWidget { required this.message, required this.buttonText, required this.onButtonPressed, - this.iconColor, }); final IconData icon; @@ -905,7 +906,6 @@ class _DialogBase extends StatelessWidget { final String message; final String buttonText; final VoidCallback onButtonPressed; - final Color? iconColor; @override Widget build(BuildContext context) { @@ -919,7 +919,7 @@ class _DialogBase extends StatelessWidget { children: [ Icon( icon, - color: iconColor ?? context.colour.primary, + color: context.colour.primary, size: 48, ), const Gap(_kGapMedium), From 8d5e252870a063bfe126a086307aeb2bc90f8450 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Tue, 4 Mar 2025 14:36:07 -0500 Subject: [PATCH 397/401] refactor: update latestRecoveredBackup type to BullBackup in BackupSettingsState --- lib/recoverbull/bloc/backup_settings_state.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/recoverbull/bloc/backup_settings_state.dart b/lib/recoverbull/bloc/backup_settings_state.dart index f9fd042c9..6fdd5bc1d 100644 --- a/lib/recoverbull/bloc/backup_settings_state.dart +++ b/lib/recoverbull/bloc/backup_settings_state.dart @@ -1,5 +1,6 @@ import 'package:bb_mobile/_model/wallet_sensitive_data.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:recoverbull/recoverbull.dart'; part 'backup_settings_state.freezed.dart'; @@ -26,7 +27,7 @@ class BackupSettingsState with _$BackupSettingsState { @Default('') String backupFolderPath, @Default('') String backupSalt, @Default('') String backupKey, - @Default({}) Map latestRecoveredBackup, + BullBackup? latestRecoveredBackup, @Default(null) DateTime? lastBackupAttempt, }) = _BackupSettingsState; From 2d67c9281177c24deaeeaf973ab22f669f5e7800 Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Tue, 4 Mar 2025 14:36:25 -0500 Subject: [PATCH 398/401] refactor: simplify backup manager retrieval and update latestRecoveredBackup assignment --- .../bloc/backup_settings_cubit.dart | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/lib/recoverbull/bloc/backup_settings_cubit.dart b/lib/recoverbull/bloc/backup_settings_cubit.dart index b08df8387..6c58b9ee6 100644 --- a/lib/recoverbull/bloc/backup_settings_cubit.dart +++ b/lib/recoverbull/bloc/backup_settings_cubit.dart @@ -98,8 +98,11 @@ class BackupSettingsCubit extends Cubit { IRecoverbullManager _getBackupManager(Type type) { if (type == GoogleDriveBackupManager) return _googleDriveBackupManager; - if (type == FileSystemBackupManager) return _fileSystemBackupManager; - throw Exception('Unknown backup manager'); + if (type == FileSystemBackupManager) { + return _fileSystemBackupManager; + } else { + return _fileSystemBackupManager; + } } void changePassword(String password) { @@ -198,7 +201,7 @@ class BackupSettingsCubit extends Cubit { _emitSafe( state.copyWith( loadingBackups: false, - latestRecoveredBackup: loadedBackup.toMap(), + latestRecoveredBackup: loadedBackup, lastBackupAttempt: DateTime.now(), ), ); @@ -265,16 +268,10 @@ class BackupSettingsCubit extends Cubit { return; } - final backupMap = backup.toMap(); - backupMap.addAll({ - 'source': 'drive', - 'filename': latestBackup.name, - }); - _emitSafe( state.copyWith( loadingBackups: false, - latestRecoveredBackup: backupMap, + latestRecoveredBackup: backup, lastBackupAttempt: DateTime.now(), errorLoadingBackups: '', ), @@ -323,7 +320,6 @@ class BackupSettingsCubit extends Cubit { errorLoadingBackups: '', ), ); - if (backupKey.isEmpty) { _handleLoadError('Backup key is missing'); return; From 2029201f8d7bab70a9de1d97e0bf94a4f2e4a11f Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Tue, 4 Mar 2025 14:36:54 -0500 Subject: [PATCH 399/401] refactor: update recoveredBackup type to BullBackup and adjust null checks --- lib/recoverbull/backup_key.dart | 19 ++++++------- lib/recoverbull/encrypted_vault_backup.dart | 30 +++++++++------------ lib/recoverbull/keychain_page.dart | 10 +++---- lib/routes.dart | 8 +++--- 4 files changed, 29 insertions(+), 38 deletions(-) diff --git a/lib/recoverbull/backup_key.dart b/lib/recoverbull/backup_key.dart index b2e3680b7..6dee32975 100644 --- a/lib/recoverbull/backup_key.dart +++ b/lib/recoverbull/backup_key.dart @@ -16,6 +16,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:gap/gap.dart'; import 'package:go_router/go_router.dart'; import 'package:intl/intl.dart'; +import 'package:recoverbull/recoverbull.dart'; class BackupKeyPage extends StatefulWidget { final String wallet; @@ -108,7 +109,7 @@ class _BackupKeyPageState extends State { _cubit.clearError(); return; } - if (state.latestRecoveredBackup.isNotEmpty) { + if (state.latestRecoveredBackup != null) { context.push( '/wallet-settings/backup-settings/key/options', extra: ('', state.latestRecoveredBackup), @@ -141,11 +142,11 @@ class _BackupKeyPageState extends State { class BackupKeyOptionsPage extends StatefulWidget { const BackupKeyOptionsPage({ super.key, - required this.recoveredBackup, + this.recoveredBackup, required this.backupKey, }); - final Map recoveredBackup; + final BullBackup? recoveredBackup; final String backupKey; @override @@ -244,10 +245,7 @@ class _BackupKeyInfoPage extends State { @override Widget build(BuildContext context) { final recoveryFile = widget.recoveredBackup; - if (recoveryFile.isEmpty || - recoveryFile['id'] == null || - recoveryFile['ciphertext'] == null || - recoveryFile['salt'] == null) { + if (recoveryFile == null) { return Scaffold( appBar: AppBar( elevation: 0, @@ -311,7 +309,7 @@ class _BackupKeyInfoPage extends State { _buildInfoText( context, 'Backup ID:', - '${recoveryFile['id']}', + recoveryFile.id, ), const Gap(8), _buildInfoText( @@ -319,7 +317,7 @@ class _BackupKeyInfoPage extends State { 'Created at:', DateFormat('MMM dd, yyyy HH:mm:ss').format( DateTime.fromMillisecondsSinceEpoch( - recoveryFile['created_at'] as int, + recoveryFile.createdAt, ).toLocal(), ), ), @@ -361,8 +359,7 @@ class _BackupKeyInfoPage extends State { onPressed: () => context .read() .recoverBackupKeyFromMnemonic( - widget.recoveredBackup['path'] - as String?, + widget.recoveredBackup?.path, ), fontSize: 12, label: keyState.keyServerUp diff --git a/lib/recoverbull/encrypted_vault_backup.dart b/lib/recoverbull/encrypted_vault_backup.dart index a82267c70..6e8153f00 100644 --- a/lib/recoverbull/encrypted_vault_backup.dart +++ b/lib/recoverbull/encrypted_vault_backup.dart @@ -19,6 +19,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:gap/gap.dart'; import 'package:go_router/go_router.dart'; import 'package:intl/intl.dart'; +import 'package:recoverbull/recoverbull.dart'; const double _kSpacing = 15.0; @@ -126,7 +127,12 @@ class _EncryptedVaultBackupPageState extends State { '/wallet-settings/backup-settings/keychain', extra: ( state.backupKey, - {'id': state.backupId, 'salt': state.backupSalt}, + BullBackup( + createdAt: DateTime.now().toUtc().millisecondsSinceEpoch, + id: state.backupId, + ciphertext: '', + salt: state.backupSalt, + ), KeyChainPageState.enter.name.toLowerCase() ), ); @@ -377,7 +383,7 @@ class _EncryptedVaultRecoverPageState extends State { _backupSettingsCubit.clearError(); return; } - if (state.latestRecoveredBackup.isNotEmpty) { + if (state.latestRecoveredBackup != null) { context.push( '/wallet-settings/backup-settings/recover-options/encrypted/info', extra: state.latestRecoveredBackup, @@ -413,7 +419,7 @@ class RecoveredBackupInfoPage extends StatefulWidget { required this.recoveredBackup, }); - final Map recoveredBackup; + final BullBackup? recoveredBackup; @override State createState() => @@ -482,19 +488,7 @@ class _RecoveredBackupInfoPageState extends State { @override Widget build(BuildContext context) { final recoveryFile = widget.recoveredBackup; - if (recoveryFile.isEmpty) { - return Scaffold( - appBar: AppBar( - elevation: 0, - automaticallyImplyLeading: false, - centerTitle: true, - flexibleSpace: BBAppBar(text: '', onBack: () => context.pop()), - ), - body: _buildErrorView(context), - ); - } else if (recoveryFile['id'] == null || - recoveryFile['ciphertext'] == null || - recoveryFile['salt'] == null) { + if (recoveryFile == null) { return Scaffold( appBar: AppBar( elevation: 0, @@ -552,7 +546,7 @@ class _RecoveredBackupInfoPageState extends State { .copyWith(fontWeight: FontWeight.bold), ), TextSpan( - text: '${recoveryFile['id']}', + text: recoveryFile.id, style: context.font.bodyMedium! .copyWith(fontWeight: FontWeight.bold), ), @@ -571,7 +565,7 @@ class _RecoveredBackupInfoPageState extends State { ), TextSpan( text: - ' ${DateFormat('MMM dd, yyyy HH:mm:ss').format(DateTime.fromMillisecondsSinceEpoch(recoveryFile['created_at'] as int).toLocal())}', + ' ${DateFormat('MMM dd, yyyy HH:mm:ss').format(DateTime.fromMillisecondsSinceEpoch(recoveryFile.createdAt).toLocal())}', style: context.font.bodyMedium! .copyWith(fontWeight: FontWeight.bold), ), diff --git a/lib/recoverbull/keychain_page.dart b/lib/recoverbull/keychain_page.dart index 8cac51c2c..a6a2a7a8d 100644 --- a/lib/recoverbull/keychain_page.dart +++ b/lib/recoverbull/keychain_page.dart @@ -17,6 +17,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:gap/gap.dart'; import 'package:go_router/go_router.dart'; +import 'package:recoverbull/recoverbull.dart'; /// Common constants const _kGapSmall = 8.0; @@ -35,7 +36,7 @@ class KeychainBackupPage extends StatelessWidget { final String? backupKey; final KeyChainPageState _pState; - final Map backup; + final BullBackup backup; @override Widget build(BuildContext context) { @@ -45,9 +46,9 @@ class KeychainBackupPage extends StatelessWidget { create: (context) => KeychainCubit() ..setChainState( _pState, - backup['id'] as String? ?? '', + backup.id, backupKey, - backup['salt'] as String? ?? '', + backup.salt, ) ..keyServerStatus(), ), @@ -66,7 +67,7 @@ class _Screen extends StatelessWidget { const _Screen({this.backupKey, required this.backup, required this.pState}); final String? backupKey; - final Map backup; + final BullBackup backup; final KeyChainPageState pState; @override Widget build(BuildContext context) { @@ -161,7 +162,6 @@ class _Screen extends StatelessWidget { if (state.keySecretState == KeySecretState.recovered && !state.loading && !state.hasError && - backup.isNotEmpty && state.backupKey.isNotEmpty) { if (state.pageState == KeyChainPageState.download) { context.push( diff --git a/lib/routes.dart b/lib/routes.dart index dea03c9eb..b32bc8609 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -44,6 +44,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; +import 'package:recoverbull/recoverbull.dart'; final navigatorKey = GlobalKey(); @@ -260,8 +261,7 @@ GoRouter setupRouter() => GoRouter( GoRoute( path: 'info', builder: (context, state) { - final recoveredBackup = - state.extra! as Map; + final recoveredBackup = state.extra as BullBackup?; return RecoveredBackupInfoPage( recoveredBackup: recoveredBackup, ); @@ -275,7 +275,7 @@ GoRouter setupRouter() => GoRouter( path: 'keychain', builder: (context, state) { final (backupKey, backup, pState) = - state.extra! as (String?, Map, String); + state.extra! as (String?, BullBackup, String); return KeychainBackupPage( backupKey: backupKey, @@ -296,7 +296,7 @@ GoRouter setupRouter() => GoRouter( path: 'options', builder: (context, state) { final (backupKey, recoveredBackup) = - state.extra! as (String, Map); + state.extra! as (String, BullBackup?); return BackupKeyOptionsPage( recoveredBackup: recoveredBackup, backupKey: backupKey, From db8d46bd5f4e9f8a484ba3c027a6b37d88ac9bce Mon Sep 17 00:00:00 2001 From: StaxoLotl Date: Tue, 4 Mar 2025 15:44:17 -0500 Subject: [PATCH 400/401] refactor: update recoverBackup method to accept BullBackup type --- lib/recoverbull/bloc/backup_settings_cubit.dart | 11 ++++++----- lib/recoverbull/keychain_page.dart | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/recoverbull/bloc/backup_settings_cubit.dart b/lib/recoverbull/bloc/backup_settings_cubit.dart index 6c58b9ee6..c67b28d2f 100644 --- a/lib/recoverbull/bloc/backup_settings_cubit.dart +++ b/lib/recoverbull/bloc/backup_settings_cubit.dart @@ -312,7 +312,7 @@ class BackupSettingsCubit extends Cubit { /// Recovers wallet data from encrypted backup /// [encrypted] - Encrypted backup data /// [backupKey] - Key used to decrypt the backup - Future recoverBackup(String encrypted, String backupKey) async { + Future recoverBackup(BullBackup encrypted, String backupKey) async { _emitSafe( state.copyWith( loadingBackups: true, @@ -325,16 +325,17 @@ class BackupSettingsCubit extends Cubit { return; } - if (!BullBackup.isValid(encrypted)) { + if (encrypted.ciphertext.isEmpty || + encrypted.salt.isEmpty || + encrypted.id.isEmpty || + encrypted.path == null) { _handleLoadError('Invalid backup format'); return; } - final backup = BullBackup.fromJson(encrypted); - final (backups, decryptErr) = await _fileSystemBackupManager.restoreEncryptedBackup( - backup: backup, + backup: encrypted, backupKey: HEX.decode(backupKey), ); diff --git a/lib/recoverbull/keychain_page.dart b/lib/recoverbull/keychain_page.dart index a6a2a7a8d..2390f5452 100644 --- a/lib/recoverbull/keychain_page.dart +++ b/lib/recoverbull/keychain_page.dart @@ -170,7 +170,7 @@ class _Screen extends StatelessWidget { ); } else { context.read().recoverBackup( - jsonEncode(backup), + backup, state.backupKey, ); } From 70eb2708979fb30fe132a99018aae2ef7dc2f2b0 Mon Sep 17 00:00:00 2001 From: ethicnology Date: Thu, 6 Mar 2025 09:17:29 -0500 Subject: [PATCH 401/401] refactor: remove infos warning --- lib/recoverbull/backup_key.dart | 6 ++- lib/recoverbull/backup_settings.dart | 2 +- lib/recoverbull/bloc/keychain_cubit.dart | 21 +++++++---- lib/recoverbull/encrypted_vault_backup.dart | 9 ++++- lib/recoverbull/keychain_page.dart | 2 - lib/routes.dart | 41 +++++++++++---------- 6 files changed, 47 insertions(+), 34 deletions(-) diff --git a/lib/recoverbull/backup_key.dart b/lib/recoverbull/backup_key.dart index 6dee32975..358e7d7b7 100644 --- a/lib/recoverbull/backup_key.dart +++ b/lib/recoverbull/backup_key.dart @@ -61,8 +61,10 @@ class _BackupKeyPageState extends State { padding: const EdgeInsets.all(20), child: Column( children: [ - const BBText.titleLarge('Where is your latest backup?', - isBold: true), + const BBText.titleLarge( + 'Where is your latest backup?', + isBold: true, + ), const Gap(20), ...BackupProvider.values.map( (provider) => Padding( diff --git a/lib/recoverbull/backup_settings.dart b/lib/recoverbull/backup_settings.dart index 43b0051c9..0849a7ece 100644 --- a/lib/recoverbull/backup_settings.dart +++ b/lib/recoverbull/backup_settings.dart @@ -389,7 +389,7 @@ class _RecoverOptionsScreenState extends State { ), context.push( '/wallet-settings/backup-settings/recover-options/encrypted', - ) + ), }, ), const Gap(20), diff --git a/lib/recoverbull/bloc/keychain_cubit.dart b/lib/recoverbull/bloc/keychain_cubit.dart index a348f7f30..a2b14b244 100644 --- a/lib/recoverbull/bloc/keychain_cubit.dart +++ b/lib/recoverbull/bloc/keychain_cubit.dart @@ -36,12 +36,14 @@ class KeychainCubit extends Cubit { Future keyServerStatus() async { if (state.isInCooldown) { - emit(state.copyWith( - keyServerUp: false, - error: - 'Rate limited. Please wait ${state.remainingCooldownSeconds} seconds.', - loading: false, - )); + emit( + state.copyWith( + keyServerUp: false, + error: + 'Rate limited. Please wait ${state.remainingCooldownSeconds} seconds.', + loading: false, + ), + ); return; } @@ -53,11 +55,14 @@ class KeychainCubit extends Cubit { emit(state.copyWith(keyServerUp: true, loading: false)); } catch (e) { debugPrint('Server status check failed: $e'); - emit(state.copyWith( + emit( + state.copyWith( keyServerUp: false, loading: false, error: - 'Unable to reach key server. This could be due to network issues or the server may be temporarily unavailable.')); + 'Unable to reach key server. This could be due to network issues or the server may be temporarily unavailable.', + ), + ); } } } diff --git a/lib/recoverbull/encrypted_vault_backup.dart b/lib/recoverbull/encrypted_vault_backup.dart index 6e8153f00..1436977d3 100644 --- a/lib/recoverbull/encrypted_vault_backup.dart +++ b/lib/recoverbull/encrypted_vault_backup.dart @@ -47,7 +47,12 @@ enum BackupProvider { final Type manager; const BackupProvider( - this.manager, this.title, this.description, this.icon, this.disclaimer); + this.manager, + this.title, + this.description, + this.icon, + this.disclaimer, + ); Future handleBackup(BackupSettingsCubit cubit) async { await cubit.saveBackup(manager: manager); @@ -596,7 +601,7 @@ class _RecoveredBackupInfoPageState extends State { ), ), }, - ) + ), ], ), ), diff --git a/lib/recoverbull/keychain_page.dart b/lib/recoverbull/keychain_page.dart index 2390f5452..40eda5153 100644 --- a/lib/recoverbull/keychain_page.dart +++ b/lib/recoverbull/keychain_page.dart @@ -1,5 +1,3 @@ -import 'dart:convert'; - import 'package:bb_mobile/_ui/app_bar.dart'; import 'package:bb_mobile/_ui/components/button.dart'; import 'package:bb_mobile/_ui/components/text.dart'; diff --git a/lib/routes.dart b/lib/routes.dart index b32bc8609..66165b3e2 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -255,7 +255,8 @@ GoRouter setupRouter() => GoRouter( return EncryptedVaultRecoverPage(canPop: extra); } return EncryptedVaultRecoverPage( - wallet: extra as String?); + wallet: extra as String?, + ); }, routes: [ GoRoute( @@ -285,24 +286,26 @@ GoRouter setupRouter() => GoRouter( }, ), GoRoute( - path: 'key', - builder: (context, state) { - return BackupKeyPage( - wallet: state.extra! as String, - ); - }, - routes: [ - GoRoute( - path: 'options', - builder: (context, state) { - final (backupKey, recoveredBackup) = - state.extra! as (String, BullBackup?); - return BackupKeyOptionsPage( - recoveredBackup: recoveredBackup, - backupKey: backupKey, - ); - }) - ]), + path: 'key', + builder: (context, state) { + return BackupKeyPage( + wallet: state.extra! as String, + ); + }, + routes: [ + GoRoute( + path: 'options', + builder: (context, state) { + final (backupKey, recoveredBackup) = + state.extra! as (String, BullBackup?); + return BackupKeyOptionsPage( + recoveredBackup: recoveredBackup, + backupKey: backupKey, + ); + }, + ), + ], + ), ], ), ],