diff --git a/lib/pages/coin_control/utxo_card.dart b/lib/pages/coin_control/utxo_card.dart index 624b41eee9..cb2acc462f 100644 --- a/lib/pages/coin_control/utxo_card.dart +++ b/lib/pages/coin_control/utxo_card.dart @@ -14,6 +14,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../db/isar/main_db.dart'; import '../../models/isar/models/isar_models.dart'; import '../../providers/global/wallets_provider.dart'; +import '../../providers/wallet/address_label_provider.dart'; import '../../themes/stack_colors.dart'; import '../../utilities/amount/amount.dart'; import '../../utilities/amount/amount_formatter.dart'; @@ -110,6 +111,15 @@ class _UtxoCardState extends ConsumerState { if (snapshot.hasData) { utxo = snapshot.data!; } + final addressLabel = utxo.address == null + ? null + : ref.watch( + pAddressLabel(( + walletId: widget.walletId, + address: utxo.address!, + )), + ); + return Row( children: [ ConditionalParent( @@ -179,6 +189,23 @@ class _UtxoCardState extends ConsumerState { ), ], ), + if (addressLabel != null && + addressLabel.value.isNotEmpty) + Row( + children: [ + Flexible( + child: Text( + addressLabel.value, + style: + STextStyles.w500_12(context).copyWith( + color: Theme.of(context) + .extension()! + .textSubtitle1, + ), + ), + ), + ], + ), ], ), ), diff --git a/lib/providers/wallet/address_label_provider.dart b/lib/providers/wallet/address_label_provider.dart new file mode 100644 index 0000000000..57223ad6e7 --- /dev/null +++ b/lib/providers/wallet/address_label_provider.dart @@ -0,0 +1,71 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:isar_community/isar.dart'; + +import '../../models/isar/models/address_label.dart'; +import '../db/main_db_provider.dart'; + +typedef AddressLabelKey = ({String walletId, String address}); + +abstract interface class AddressLabelStore { + AddressLabel? find(AddressLabelKey key); + + Stream> watch(AddressLabelKey key); +} + +class _MainDBAddressLabelStore implements AddressLabelStore { + const _MainDBAddressLabelStore(this.isar); + + final Isar isar; + + QueryBuilder _query( + AddressLabelKey key, + ) => isar.addressLabels.where().addressStringWalletIdEqualTo( + key.address, + key.walletId, + ); + + @override + AddressLabel? find(AddressLabelKey key) => _query(key).findFirstSync(); + + @override + Stream> watch(AddressLabelKey key) => + _query(key).watch(fireImmediately: true); +} + +final addressLabelStoreProvider = Provider((ref) { + return _MainDBAddressLabelStore(ref.watch(mainDBProvider).isar); +}); + +class _AddressLabelWatcher extends ChangeNotifier { + _AddressLabelWatcher(AddressLabelStore store, AddressLabelKey key) + : _value = store.find(key) { + _subscription = store.watch(key).listen((labels) { + _value = labels.firstOrNull; + notifyListeners(); + }); + } + + late final StreamSubscription> _subscription; + AddressLabel? _value; + + AddressLabel? get value => _value; + + @override + void dispose() { + _subscription.cancel(); + super.dispose(); + } +} + +final _addressLabelWatcherProvider = ChangeNotifierProvider.autoDispose + .family<_AddressLabelWatcher, AddressLabelKey>((ref, key) { + return _AddressLabelWatcher(ref.watch(addressLabelStoreProvider), key); + }); + +final pAddressLabel = Provider.autoDispose + .family( + (ref, key) => ref.watch(_addressLabelWatcherProvider(key)).value, + ); diff --git a/test/providers/wallet/address_label_provider_test.dart b/test/providers/wallet/address_label_provider_test.dart new file mode 100644 index 0000000000..2c83cec10d --- /dev/null +++ b/test/providers/wallet/address_label_provider_test.dart @@ -0,0 +1,92 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stackwallet/models/isar/models/address_label.dart'; +import 'package:stackwallet/providers/wallet/address_label_provider.dart'; + +void main() { + late _FakeAddressLabelStore store; + + setUp(() => store = _FakeAddressLabelStore()); + tearDown(() => store.dispose()); + + testWidgets('updates a visible label without another data event', ( + tester, + ) async { + store.value = _label('Original label'); + + await tester.pumpWidget( + ProviderScope( + overrides: [addressLabelStoreProvider.overrideWithValue(store)], + child: const MaterialApp(home: _LabelText()), + ), + ); + expect(find.text('Original label'), findsOneWidget); + + store.emit(_label('Updated label')); + await tester.pump(); + await tester.pump(); + + expect(find.text('Updated label'), findsOneWidget); + expect(find.text('Original label'), findsNothing); + }); + + testWidgets('reacts when a label is created and deleted', (tester) async { + await tester.pumpWidget( + ProviderScope( + overrides: [addressLabelStoreProvider.overrideWithValue(store)], + child: const MaterialApp(home: _LabelText()), + ), + ); + expect(find.text('No label'), findsOneWidget); + + store.emit(_label('New label')); + await tester.pump(); + await tester.pump(); + expect(find.text('New label'), findsOneWidget); + + store.emit(null); + await tester.pump(); + await tester.pump(); + expect(find.text('No label'), findsOneWidget); + }); +} + +AddressLabel _label(String value) => AddressLabel( + walletId: 'wallet', + addressString: 'address', + value: value, + tags: null, +); + +class _FakeAddressLabelStore implements AddressLabelStore { + final _controller = StreamController>.broadcast(); + AddressLabel? value; + + @override + AddressLabel? find(AddressLabelKey key) => value; + + @override + Stream> watch(AddressLabelKey key) => _controller.stream; + + void emit(AddressLabel? label) { + value = label; + _controller.add(label == null ? [] : [label]); + } + + void dispose() => _controller.close(); +} + +class _LabelText extends ConsumerWidget { + const _LabelText(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final label = ref.watch( + pAddressLabel((walletId: 'wallet', address: 'address')), + ); + return Text(label?.value ?? 'No label'); + } +}