feat: add toggle supported handles

This commit is contained in:
Serhii 2025-06-23 14:07:23 +03:00
parent 8dc3bb649d
commit a298a28f56
12 changed files with 305 additions and 97 deletions

View file

@ -38,6 +38,7 @@ import 'package:cake_wallet/src/screens/address_book/edit_contact_page.dart';
import 'package:cake_wallet/src/screens/address_book/edit_new_contact_group_page.dart';
import 'package:cake_wallet/src/screens/address_book/edit_new_contact_page.dart';
import 'package:cake_wallet/src/screens/address_book/entities/address_edit_request.dart';
import 'package:cake_wallet/src/screens/address_book/supported_handles_page.dart';
import 'package:cake_wallet/src/screens/contact/contact_list_page.dart';
import 'package:cake_wallet/src/screens/dev/monero_background_sync.dart';
import 'package:cake_wallet/src/screens/dev/moneroc_call_profiler.dart';
@ -978,7 +979,7 @@ Future<void> setup({
AnimatedURPage(getIt.get<AnimatedURModel>(), urQr: urQr));
getIt.registerFactoryParam<ContactViewModel, AddressEditRequest?, void>(
(req, _) => ContactViewModel(_contactSource,getIt<AppStore>().wallet!, request: req),
(req, _) => ContactViewModel(_contactSource,getIt<AppStore>().wallet!,getIt<SettingsStore>(), request: req,),
);
getIt.registerFactoryParam<ContactListViewModel, CryptoCurrency?, void>(
@ -1012,6 +1013,9 @@ Future<void> setup({
),
);
getIt.registerFactory<SupportedHandlesPage>(
() => SupportedHandlesPage(contactViewModel: getIt<ContactViewModel>()),
);
getIt.registerFactoryParam<EditContactGroupPage, ContactViewModel, void>(
(vm, _) => EditContactGroupPage(contactViewModel: vm),

View file

@ -200,8 +200,7 @@ class AddressResolverService {
LookupEntry(
source: AddressSource.fio,
currencies: AddressSource.fio.supportedCurrencies,
applies: (q) => !q.startsWith('@') && q.contains('@') && !q.contains('.'),
// TODO: Add condition for FIO lookups
applies: (q) => settingsStore.lookupsFio && !q.startsWith('@') && q.contains('@') && !q.contains('.'),
// FIO handle example: username@domain
run: _lookupFio,
),
@ -215,8 +214,7 @@ class AddressResolverService {
LookupEntry(
source: AddressSource.thorChain,
currencies: AddressSource.thorChain.supportedCurrencies,
applies: (q) => true,
// ThorChain handles can be any string //TODO: Add condition for ThorChain lookups
applies: (q) => settingsStore.lookupsThorChain && q.isNotEmpty,
run: _lookupThorChain,
),
LookupEntry(
@ -238,7 +236,7 @@ class AddressResolverService {
LookupEntry(
source: AddressSource.bip353,
currencies: AddressSource.bip353.supportedCurrencies,
applies: (q) => true, //TODO: Add condition for BIP-353 lookups
applies: (q) => settingsStore.lookupsBip353,
run: _lookupsBip353,
),
LookupEntry(
@ -262,8 +260,8 @@ class AddressResolverService {
LookupEntry(
source: AddressSource.nostr,
currencies: [CryptoCurrency.btc],
applies: (q) => isEmailFormat(q),
// Nostr handle example: name@domain //TODO: Add condition for Nostr lookups
applies: (q) => settingsStore.lookupsNostr && isEmailFormat(q),
// Nostr handle example: name@domain
run: _lookupsNostr,
),
];

View file

@ -84,6 +84,10 @@ class PreferencesKey {
static const lookupsOpenAlias = 'looks_up_open_alias';
static const lookupsENS = 'looks_up_ens';
static const lookupsWellKnown = 'looks_up_well_known';
static const lookupsFio = 'looks_up_fio';
static const lookupsNostr = 'looks_up_nostr';
static const lookupsThorChain = 'looks_up_thor_chain';
static const lookupsBip353 = 'looks_up_bip353';
static const usePayjoin = 'use_payjoin';
static const showPayjoinCard = 'show_payjoin_card';
static const showCameraConsent = 'show_camera_consent';

View file

@ -2,21 +2,39 @@ import 'package:cake_wallet/entities/parsed_address.dart';
import 'package:cake_wallet/src/screens/address_book/widgets/handles_list_widget.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/themes/utils/custom_theme_colors.dart';
import 'package:cake_wallet/view_model/contact_list/contact_view_model.dart';
import 'package:flutter/material.dart';
class SupportedHandlesPage extends BasePage {
SupportedHandlesPage();
SupportedHandlesPage({required this.contactViewModel});
final ContactViewModel contactViewModel;
@override
String? get title => 'Supported Handles';
@override
Widget body(BuildContext context) {
final fillColor = currentTheme.isDark
final fill = currentTheme.isDark
? CustomThemeColors.backgroundGradientColorDark
: CustomThemeColors.backgroundGradientColorLight;
return HandlesListWidget(items: supportedSources, fillColor: fillColor);
final selectedInit = <AddressSource>[
for (final src in supportedSources)
if (contactViewModel.lookupMap[src.label]!.$1()) src
];
return HandlesListWidget(
items: supportedSources,
fillColor: fill,
initiallySelected: selectedInit,
onSelectionChanged: (sel) {
for (final src in supportedSources) {
final pair = contactViewModel.lookupMap[src.label]!;
pair.$2(sel.contains(src));
}
},
);
}
}

View file

@ -1,46 +1,85 @@
import 'package:flutter/material.dart';
import 'package:cake_wallet/entities/parsed_address.dart';
import 'package:cake_wallet/utils/image_utill.dart';
import 'package:flutter/material.dart';
class HandlesListWidget extends StatelessWidget {
const HandlesListWidget({
super.key,
required this.items,
required this.fillColor,
});
class HandlesListWidget extends StatefulWidget {
const HandlesListWidget(
{super.key,
required this.items,
required this.fillColor,
this.initiallySelected = const [],
this.onSelectionChanged});
final List<AddressSource> items;
final Color fillColor;
final List<AddressSource> initiallySelected;
final ValueChanged<Set<AddressSource>>? onSelectionChanged;
@override
State<HandlesListWidget> createState() => _HandlesListWidgetState();
}
class _HandlesListWidgetState extends State<HandlesListWidget> {
late final Set<AddressSource> _selected = widget.initiallySelected.toSet();
void _toggle(AddressSource src) {
setState(() {
if (_selected.contains(src)) {
_selected.remove(src);
} else {
_selected.add(src);
}
});
widget.onSelectionChanged?.call(_selected);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final iconColor = theme.colorScheme.primary;
return ListView.separated(
shrinkWrap: true,
primary: false,
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
itemCount: items.length,
separatorBuilder: (context, index) => const SizedBox(height: 6),
itemBuilder: (context, index) {
final src = items[index];
itemCount: widget.items.length,
separatorBuilder: (_, __) => const SizedBox(height: 6),
itemBuilder: (ctx, i) {
final src = widget.items[i];
final isSelected = _selected.contains(src);
return ListTile(
title: Text(src.label, style: Theme
.of(context)
.textTheme
.bodyMedium),
trailing: Text(src.alias, style: Theme
.of(context)
.textTheme
.bodyMedium),
tileColor: fillColor,
title: Text(src.label, style: theme.textTheme.bodyMedium),
trailing: Text(src.alias, style: theme.textTheme.bodyMedium),
tileColor: widget.fillColor,
splashColor: Colors.transparent,
dense: true,
visualDensity: VisualDensity(horizontal: 0, vertical: -3),
contentPadding: EdgeInsets.symmetric(horizontal: 12),
leading: ImageUtil.getImageFromPath(imagePath: src.iconPath, height: 24, width: 24),
shape: RoundedRectangleBorder(
borderRadius: const BorderRadius.all(Radius.circular(12)),
visualDensity: const VisualDensity(horizontal: 0, vertical: -3),
contentPadding: const EdgeInsets.symmetric(horizontal: 12),
leading: Row(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: 24,
height: 24,
child: isSelected
? Icon(Icons.check_circle, size: 20, color: iconColor)
: Icon(Icons.circle_outlined, size: 20, color: iconColor),
),
const SizedBox(width: 6),
ImageUtil.getImageFromPath(
imagePath: src.iconPath,
height: 24,
width: 24,
),
],
),
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12))),
onTap: () => _toggle(src),
);
},
);
}
}
}

View file

@ -325,33 +325,29 @@ class _ContactListBodyState extends State<ContactListBody> {
shrinkWrap: true,
itemBuilder: (context, index) {
final contact = contacts[index];
return Padding(
key: ValueKey(contact.key),
padding: const EdgeInsets.only(bottom: 8),
child: ContactAddressesExpansionTile(
key: Key(contact.key.toString()),
contentPadding: EdgeInsets.symmetric(horizontal: 8),
manualByCurrency: contact.manual,
fillColor: Theme.of(context).colorScheme.surfaceContainer,
title: _buildContactTitle(
context: context,
return ContactAddressesExpansionTile(
key: Key(contact.key.toString()),
contentPadding: EdgeInsets.symmetric(horizontal: 8),
manualByCurrency: contact.manual,
fillColor: Theme.of(context).colorScheme.surfaceContainer,
title: _buildContactTitle(
context: context,
contact: contact,
contactListViewModel: widget.contactListViewModel),
onEditPressed: (cur, lbl) async {
await _showAddressBookBottomSheet(
context: context,
contactListViewModel: widget.contactListViewModel,
initialRoute: Routes.editAddressPage,
initialArgs: AddressEditRequest.address(
contact: contact,
contactListViewModel: widget.contactListViewModel),
onEditPressed: (cur, lbl) async {
await _showAddressBookBottomSheet(
context: context,
contactListViewModel: widget.contactListViewModel,
initialRoute: Routes.editAddressPage,
initialArgs: AddressEditRequest.address(
contact: contact,
currency: cur,
label: lbl,
kindIsManual: true,
),
);
},
onCopyPressed: (addr) => Clipboard.setData(ClipboardData(text: addr)),
),
currency: cur,
label: lbl,
kindIsManual: true,
),
);
},
onCopyPressed: (addr) => Clipboard.setData(ClipboardData(text: addr)),
);
},
),

View file

@ -53,8 +53,22 @@ class DomainLookupsPage extends BasePage {
title: 'Zano Aliases',
value: _privacySettingsViewModel.lookupsZanoAlias,
onValueChange: (_, bool value) => _privacySettingsViewModel.setLookupsZanoAlias(value)),
//if (!isHaven) it does not work correctly
SettingsSwitcherCell(
title: 'FIO',
value: _privacySettingsViewModel.lookupsFio,
onValueChange: (_, bool value) => _privacySettingsViewModel.setLookupsFio(value)),
SettingsSwitcherCell(
title: 'Nostr',
value: _privacySettingsViewModel.lookupsNostr,
onValueChange: (_, bool value) => _privacySettingsViewModel.setLookupsNostr(value)),
SettingsSwitcherCell(
title: 'ThorChain',
value: _privacySettingsViewModel.lookupsThorChain,
onValueChange: (_, bool value) => _privacySettingsViewModel.setLookupsThorChain(value)),
SettingsSwitcherCell(
title: 'BIP-353',
value: _privacySettingsViewModel.lookupsBip353,
onValueChange: (_, bool value) => _privacySettingsViewModel.setLookupsBip353(value)),
],
),
);

View file

@ -10,6 +10,7 @@ class FilteredList extends StatefulWidget {
this.canReorder = true,
this.shrinkWrap = false,
this.physics,
this.itemPadding = const EdgeInsets.symmetric(vertical: 4),
});
final ObservableList<dynamic> list;
@ -18,39 +19,51 @@ class FilteredList extends StatefulWidget {
final bool canReorder;
final bool shrinkWrap;
final ScrollPhysics? physics;
final EdgeInsets itemPadding;
@override
FilteredListState createState() => FilteredListState();
}
class FilteredListState extends State<FilteredList> {
Widget _buildPaddedItem(BuildContext ctx, int index) {
return Padding(
key: ValueKey(widget.list[index]),
padding: widget.itemPadding,
child: widget.itemBuilder(ctx, index),
);
}
@override
Widget build(BuildContext context) {
if (widget.canReorder) {
return Observer(
builder: (_) => ReorderableListView.builder(
shrinkWrap: widget.shrinkWrap,
physics: widget.physics ?? const BouncingScrollPhysics(),
itemBuilder: widget.itemBuilder,
itemCount: widget.list.length,
onReorder: (int oldIndex, int newIndex) {
if (oldIndex < newIndex) {
newIndex -= 1;
}
final dynamic item = widget.list.removeAt(oldIndex);
widget.list.insert(newIndex, item);
widget.updateFunction();
},
),
);
} else {
if (!widget.canReorder) {
return Observer(
builder: (_) => ListView.builder(
shrinkWrap: widget.shrinkWrap,
physics: widget.physics ?? const BouncingScrollPhysics(),
itemBuilder: widget.itemBuilder,
itemCount: widget.list.length,
itemBuilder: _buildPaddedItem,
),
);
}
return Observer(
builder: (_) => ReorderableListView.builder(
shrinkWrap: widget.shrinkWrap,
physics: widget.physics ?? const BouncingScrollPhysics(),
itemCount: widget.list.length,
itemBuilder: _buildPaddedItem,
onReorder: (oldIndex, newIndex) {
if (oldIndex < newIndex) newIndex -= 1;
final item = widget.list.removeAt(oldIndex);
widget.list.insert(newIndex, item);
widget.updateFunction();
},
proxyDecorator: (child, _, __) => Material(
color: Colors.transparent,
child: child,
),
),
);
}
}

View file

@ -13,10 +13,10 @@ import 'package:cake_wallet/src/screens/address_book/edit_new_contact_page.dart'
import 'package:cake_wallet/src/screens/address_book/entities/address_edit_request.dart';
import 'package:cake_wallet/src/screens/address_book/supported_handles_page.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:cake_wallet/view_model/contact_list/contact_view_model.dart';
import 'package:flutter/material.dart';
class AddressBookBottomSheet extends StatelessWidget {
const AddressBookBottomSheet({
super.key,
@ -29,10 +29,8 @@ class AddressBookBottomSheet extends StatelessWidget {
final String? initialRoute;
final Object? initialArgs;
@override
Widget build(BuildContext context) {
return ClipRRect(
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
child: Material(
@ -47,8 +45,8 @@ class AddressBookBottomSheet extends StatelessWidget {
),
child: _AddContactNavigator(
onHandlerSearch: onHandlerSearch,
initialRoute : initialRoute ?? Navigator.defaultRouteName,
initialArgs : initialArgs,
initialRoute: initialRoute ?? Navigator.defaultRouteName,
initialArgs: initialArgs,
),
),
],
@ -57,6 +55,7 @@ class AddressBookBottomSheet extends StatelessWidget {
);
}
}
Widget _buildDragHandle(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(top: 16),
@ -106,20 +105,19 @@ class _AddContactNavigator extends StatelessWidget {
Route<dynamic> _routeFor(String name, Object? args) {
late final Widget page;
switch (name) {
case Routes.supportedHandlesPage:
page = SupportedHandlesPage();
page = getIt<SupportedHandlesPage>();
break;
case Routes.newContactWelcomePage:
final list = args as List<dynamic>;
final onSearch = list[0] as Future<List<ParsedAddress>> Function(String);
final list = args as List<dynamic>;
final onSearch = list[0] as Future<List<ParsedAddress>> Function(String);
final handleOnly = list.length > 1 && list[1] == true;
final contact = list.length > 2 ? list[2] as ContactRecord? : null;
final contact = list.length > 2 ? list[2] as ContactRecord? : null;
page = NewContactWelcomePage(
onSearch : onSearch,
handleOnly : handleOnly,
onSearch: onSearch,
handleOnly: handleOnly,
existingContact: contact,
);
break;

View file

@ -117,6 +117,10 @@ abstract class SettingsStoreBase with Store {
required this.lookupsOpenAlias,
required this.lookupsENS,
required this.lookupsWellKnown,
required this.lookupsFio,
required this.lookupsNostr,
required this.lookupsThorChain,
required this.lookupsBip353,
required this.usePayjoin,
required this.showPayjoinCard,
required this.customBitcoinFeeRate,
@ -481,6 +485,26 @@ abstract class SettingsStoreBase with Store {
(bool looksUpWellKnown) =>
_sharedPreferences.setBool(PreferencesKey.lookupsWellKnown, looksUpWellKnown));
reaction(
(_) => lookupsFio,
(bool looksUpWellKnown) =>
_sharedPreferences.setBool(PreferencesKey.lookupsWellKnown, looksUpWellKnown));
reaction(
(_) => lookupsNostr,
(bool looksUpWellKnown) =>
_sharedPreferences.setBool(PreferencesKey.lookupsWellKnown, looksUpWellKnown));
reaction(
(_) => lookupsThorChain,
(bool looksUpWellKnown) =>
_sharedPreferences.setBool(PreferencesKey.lookupsWellKnown, looksUpWellKnown));
reaction(
(_) => lookupsBip353,
(bool looksUpWellKnown) =>
_sharedPreferences.setBool(PreferencesKey.lookupsWellKnown, looksUpWellKnown));
reaction(
(_) => usePayjoin,
(bool usePayjoin) =>
@ -809,6 +833,18 @@ abstract class SettingsStoreBase with Store {
@observable
bool lookupsWellKnown;
@observable
bool lookupsFio;
@observable
bool lookupsNostr;
@observable
bool lookupsThorChain;
@observable
bool lookupsBip353;
@observable
bool usePayjoin;
@ -1012,6 +1048,10 @@ abstract class SettingsStoreBase with Store {
final lookupsOpenAlias = sharedPreferences.getBool(PreferencesKey.lookupsOpenAlias) ?? true;
final lookupsENS = sharedPreferences.getBool(PreferencesKey.lookupsENS) ?? true;
final lookupsWellKnown = sharedPreferences.getBool(PreferencesKey.lookupsWellKnown) ?? true;
final lookupsFio = sharedPreferences.getBool(PreferencesKey.lookupsFio) ?? true;
final lookupsNostr = sharedPreferences.getBool(PreferencesKey.lookupsNostr) ?? true;
final lookupsThorChain = sharedPreferences.getBool(PreferencesKey.lookupsThorChain) ?? true;
final lookupsBip353 = sharedPreferences.getBool(PreferencesKey.lookupsBip353) ?? true;
final usePayjoin = sharedPreferences.getBool(PreferencesKey.usePayjoin) ?? false;
final showPayjoinCard = sharedPreferences.getBool(PreferencesKey.showPayjoinCard) ?? true;
final customBitcoinFeeRate = sharedPreferences.getInt(PreferencesKey.customBitcoinFeeRate) ?? 1;
@ -1316,6 +1356,10 @@ abstract class SettingsStoreBase with Store {
lookupsOpenAlias: lookupsOpenAlias,
lookupsENS: lookupsENS,
lookupsWellKnown: lookupsWellKnown,
lookupsFio: lookupsFio,
lookupsNostr: lookupsNostr,
lookupsThorChain: lookupsThorChain,
lookupsBip353: lookupsBip353,
usePayjoin: usePayjoin,
showPayjoinCard: showPayjoinCard,
customBitcoinFeeRate: customBitcoinFeeRate,

View file

@ -6,6 +6,7 @@ import 'package:cake_wallet/entities/contact_record.dart';
import 'package:cake_wallet/entities/parsed_address.dart';
import 'package:cake_wallet/src/screens/address_book/entities/address_edit_request.dart';
import 'package:cake_wallet/src/screens/address_book/entities/user_handles.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:flutter/material.dart';
@ -25,7 +26,8 @@ class ContactViewModel = _ContactViewModel with _$ContactViewModel;
abstract class _ContactViewModel with Store {
_ContactViewModel(
this.box,
this.wallet, {
this.wallet,
this.settingsStore, {
AddressEditRequest? request,
}) : mode = request?.mode == EditMode.manualAddressAdd ||
request?.mode == EditMode.manualAddressEdit
@ -64,6 +66,7 @@ abstract class _ContactViewModel with Store {
final Box<Contact> box;
final WalletBase wallet;
final SettingsStore? settingsStore;
ContactRecord? record;
@observable
@ -269,4 +272,57 @@ abstract class _ContactViewModel with Store {
_originalAddress = address.trim();
_originalHandleKey = blockKey ?? _defaultHandleKey();
}
late final Map<String, (bool Function(), void Function(bool))> lookupMap = settingsStore != null
? {
AddressSource.twitter.label: (
() => settingsStore!.lookupsTwitter,
(v) => settingsStore!.lookupsTwitter = v
),
AddressSource.zanoAlias.label: (
() => settingsStore!.lookupsZanoAlias,
(v) => settingsStore!.lookupsZanoAlias = v
),
AddressSource.mastodon.label: (
() => settingsStore!.lookupsMastodon,
(v) => settingsStore!.lookupsMastodon = v
),
AddressSource.yatRecord.label: (
() => settingsStore!.lookupsYatService,
(v) => settingsStore!.lookupsYatService = v
),
AddressSource.unstoppableDomains.label: (
() => settingsStore!.lookupsUnstoppableDomains,
(v) => settingsStore!.lookupsUnstoppableDomains = v
),
AddressSource.openAlias.label: (
() => settingsStore!.lookupsOpenAlias,
(v) => settingsStore!.lookupsOpenAlias = v
),
AddressSource.ens.label: (
() => settingsStore!.lookupsENS,
(v) => settingsStore!.lookupsENS = v
),
AddressSource.wellKnown.label: (
() => settingsStore!.lookupsWellKnown,
(v) => settingsStore!.lookupsWellKnown = v
),
AddressSource.fio.label: (
() => settingsStore!.lookupsFio,
(v) => settingsStore!.lookupsFio = v
),
AddressSource.nostr.label: (
() => settingsStore!.lookupsNostr,
(v) => settingsStore!.lookupsNostr = v
),
AddressSource.thorChain.label: (
() => settingsStore!.lookupsThorChain,
(v) => settingsStore!.lookupsThorChain = v
),
AddressSource.bip353.label: (
() => settingsStore!.lookupsBip353,
(v) => settingsStore!.lookupsBip353 = v
),
}
: {};
}

View file

@ -103,6 +103,18 @@ abstract class PrivacySettingsViewModelBase with Store {
@computed
bool get looksUpWellKnown => _settingsStore.lookupsWellKnown;
@computed
bool get lookupsFio => _settingsStore.lookupsFio;
@computed
bool get lookupsNostr => _settingsStore.lookupsNostr;
@computed
bool get lookupsThorChain => _settingsStore.lookupsThorChain;
@computed
bool get lookupsBip353 => _settingsStore.lookupsBip353;
@computed
bool get usePayjoin => _settingsStore.usePayjoin;
@ -169,6 +181,18 @@ abstract class PrivacySettingsViewModelBase with Store {
ethereum!.updateEtherscanUsageState(_wallet, value);
}
@action
void setLookupsFio(bool value) => _settingsStore.lookupsFio = value;
@action
void setLookupsNostr(bool value) => _settingsStore.lookupsNostr = value;
@action
void setLookupsThorChain(bool value) => _settingsStore.lookupsThorChain = value;
@action
void setLookupsBip353(bool value) => _settingsStore.lookupsBip353 = value;
@action
void setUsePolygonScan(bool value) {
_settingsStore.usePolygonScan = value;