fix: save hive issue

This commit is contained in:
Serhii 2025-06-22 21:01:00 +03:00
parent 66c49f795b
commit 8dc3bb649d
27 changed files with 1411 additions and 1295 deletions

View file

@ -30,7 +30,6 @@ class AuthService with Store {
Routes.modify2FAPage, Routes.modify2FAPage,
Routes.newWallet, Routes.newWallet,
Routes.newWalletType, Routes.newWalletType,
Routes.addressBookAddContact,
Routes.restoreOptions, Routes.restoreOptions,
]; ];

View file

@ -32,12 +32,12 @@ import 'package:cake_wallet/entities/parse_address_from_domain.dart';
import 'package:cake_wallet/entities/parsed_address.dart'; import 'package:cake_wallet/entities/parsed_address.dart';
import 'package:cake_wallet/exchange/provider/trocador_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/trocador_exchange_provider.dart';
import 'package:cake_wallet/haven/cw_haven.dart'; import 'package:cake_wallet/haven/cw_haven.dart';
import 'package:cake_wallet/src/screens/address_book/contact_page.dart';
import 'package:cake_wallet/src/screens/address_book/edit_address_page.dart'; import 'package:cake_wallet/src/screens/address_book/edit_address_page.dart';
import 'package:cake_wallet/src/screens/address_book/edit_addresses_page.dart'; import 'package:cake_wallet/src/screens/address_book/edit_contact_group_page.dart';
import 'package:cake_wallet/src/screens/address_book/edit_contact_page.dart'; 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_group_page.dart';
import 'package:cake_wallet/src/screens/address_book/edit_new_contact_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/contact/contact_list_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/monero_background_sync.dart';
import 'package:cake_wallet/src/screens/dev/moneroc_call_profiler.dart'; import 'package:cake_wallet/src/screens/dev/moneroc_call_profiler.dart';
@ -977,48 +977,66 @@ Future<void> setup({
getIt.registerFactoryParam<AnimatedURPage, String, void>((String urQr, _) => getIt.registerFactoryParam<AnimatedURPage, String, void>((String urQr, _) =>
AnimatedURPage(getIt.get<AnimatedURModel>(), urQr: urQr)); AnimatedURPage(getIt.get<AnimatedURModel>(), urQr: urQr));
getIt.registerFactoryParam<ContactViewModel, ContactRecord?, List<dynamic>?>( getIt.registerFactoryParam<ContactViewModel, AddressEditRequest?, void>(
(contact, initialContactParams) => ContactViewModel( (req, _) => ContactViewModel(_contactSource,getIt<AppStore>().wallet!, request: req),
_contactSource,
contact: contact,
initialParams: initialContactParams ?? <dynamic>[],
),
); );
getIt.registerFactoryParam<ContactListViewModel, CryptoCurrency?, void>( getIt.registerFactoryParam<ContactListViewModel, CryptoCurrency?, void>(
(CryptoCurrency? cur, _) => (cur, _) => ContactListViewModel(
ContactListViewModel(_contactSource, _walletInfoSource, getIt _contactSource,
.get<AppStore>() _walletInfoSource,
.wallet!, cur, getIt.get<SettingsStore>())); getIt<AppStore>().wallet!,
cur,
getIt.registerFactoryParam<ContactListPage, CryptoCurrency?, void>((CryptoCurrency? cur, _) => getIt<SettingsStore>(),
ContactListPage(getIt.get<ContactListViewModel>(param1: cur), getIt.get<AuthService>())); ),
getIt.registerFactoryParam<ContactPage, ContactRecord?, void>(
(ContactRecord? contact, _) => ContactPage(getIt.get<ContactViewModel>(param1: contact)));
getIt.registerFactoryParam<EditNewContactGroupPage, ParsedAddress, void>(
(ParsedAddress parsedAddress, _) => EditNewContactGroupPage(selectedParsedAddress: parsedAddress,contacts: _contactSource));
getIt.registerFactoryParam<EditAddressesPage, ContactRecord, void>(
(ContactRecord contact, _) => EditAddressesPage(contactViewModel: getIt.get<ContactViewModel>(param1: contact)));
getIt.registerFactoryParam<EditAddressPage, List<dynamic>, void>(
(List<dynamic> args, _) {
final contact = args.first as ContactRecord?;
final currency = args[1] as CryptoCurrency?;
final label = args[2] as String?;
return EditAddressPage(
contactViewModel: getIt.get<ContactViewModel>(param1: contact, param2: [currency, label]),
); );
});
getIt.registerFactoryParam<EditNewContactPage, ContactRecord, void>( getIt.registerFactoryParam<ContactListPage, CryptoCurrency?, void>(
(ContactRecord contact, _) => EditNewContactPage(contactViewModel: getIt.get<ContactViewModel>(param1: contact))); (cur, _) => ContactListPage(
getIt.get<ContactListViewModel>(param1: cur),
getIt<AuthService>(),
),
);
getIt.registerFactoryParam<EditContactPage, ContactRecord, void>( getIt.registerFactoryParam<EditContactPage, ContactRecord, void>(
(ContactRecord contact, _) => EditContactPage(contactViewModel: getIt.get<ContactViewModel>(param1: contact))); (contact, _) => EditContactPage(
contactViewModel: getIt.get<ContactViewModel>(
param1: AddressEditRequest.contact(contact),
),
),
);
getIt.registerFactoryParam<EditAddressPage, AddressEditRequest, void>(
(request, _) => EditAddressPage(
contactViewModel: getIt.get<ContactViewModel>(param1: request),
),
);
getIt.registerFactoryParam<EditContactGroupPage, ContactViewModel, void>(
(vm, _) => EditContactGroupPage(contactViewModel: vm),
);
getIt.registerFactoryParam<EditNewContactGroupPage, ParsedAddress, ContactRecord?>(
(parsedAddress, record) {
final vm = getIt<ContactViewModel>(
param1: AddressEditRequest.contact(record),
);
return EditNewContactGroupPage(
selectedParsedAddress: parsedAddress,
contactViewModel : vm,
);
},
);
getIt.registerFactoryParam<EditNewContactPage, ContactRecord?, void>(
(contact, _) => EditNewContactPage(
contactViewModel: getIt.get<ContactViewModel>(param1: AddressEditRequest.contact(contact)),
),
);
getIt.registerFactory(() => AddressListPage(getIt.get<WalletAddressListViewModel>())); getIt.registerFactory(() => AddressListPage(getIt.get<WalletAddressListViewModel>()));

View file

@ -1,3 +1,5 @@
import 'dart:convert';
import 'package:cake_wallet/entities/parsed_address.dart'; import 'package:cake_wallet/entities/parsed_address.dart';
import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/hive_type_ids.dart'; import 'package:cw_core/hive_type_ids.dart';
@ -10,21 +12,152 @@ part 'contact.g.dart';
class Contact extends HiveObject with Keyable { class Contact extends HiveObject with Keyable {
Contact({ Contact({
required this.name, required this.name,
required this.parsedAddresses, this.address = '',
required this.manualAddresses, CryptoCurrency? type,
Map<String, Map<int, Map<String, String>>> parsedByHandle = const {},
Map<int, Map<String, String>> manualAddresses = const {},
Map<String, String> extraBlobs = const {},
AddressSource source = AddressSource.notParsed, AddressSource source = AddressSource.notParsed,
this.handle = '', this.handle = '',
this.imagePath = '', this.imagePath = '',
this.profileName = '', this.profileName = '',
this.description = '', this.description = '',
DateTime? lastChange, DateTime? lastChange,
}) : sourceRaw = source.raw, }) : raw = type?.raw ?? 0,
_parsedJson = _encode(parsedByHandle),
_manualJson = _encode(manualAddresses),
extraJsonBlobs = extraBlobs,
sourceRaw = source.raw,
lastChange = lastChange ?? DateTime.now(); lastChange = lastChange ?? DateTime.now();
static const typeId = CONTACT_TYPE_ID; static const typeId = CONTACT_TYPE_ID;
static const boxName = 'Contacts'; static const boxName = 'Contacts';
@HiveField(0, defaultValue: '')
String name;
@HiveField(1, defaultValue: '')
String address;
@HiveField(2, defaultValue: 0)
int raw;
@HiveField(3)
DateTime lastChange;
@HiveField(4, defaultValue: '')
String _parsedJson;
@HiveField(5, defaultValue: '')
String _manualJson;
@HiveField(6, defaultValue: '')
String handle;
@HiveField(7, defaultValue: '')
String imagePath;
@HiveField(8, defaultValue: '')
String profileName;
@HiveField(9, defaultValue: '')
String description;
@HiveField(10, defaultValue: 0)
int sourceRaw;
@HiveField(11, defaultValue: {})
Map<String, String> extraJsonBlobs;
AddressSource get source => AddressSourceIndex.fromRaw(sourceRaw);
set source(AddressSource v) => sourceRaw = v.raw;
CryptoCurrency get type => CryptoCurrency.deserialize(raw: raw);
set type(CryptoCurrency v) => raw = v.raw;
Map<String, Map<int, Map<String, String>>> get parsedByHandle => _decodeParsed(_parsedJson);
set parsedByHandle(Map<String, Map<int, Map<String, String>>> v) => _parsedJson = _encode(v);
Map<int, Map<String, String>> get manualAddresses => _decodeManual(_manualJson);
set manualAddresses(Map<int, Map<String, String>> v) => _manualJson = _encode(v);
Map<CryptoCurrency, Map<String, String>> get manualByCurrency => manualAddresses.map(
(k, v) => MapEntry(CryptoCurrency.deserialize(raw: k), v),
);
Map<CryptoCurrency, Map<String, String>> get parsedByCurrency {
final out = <CryptoCurrency, Map<String, String>>{};
for (final block in parsedByHandle.values) {
block.forEach((curRaw, lblMap) {
final cur = CryptoCurrency.deserialize(raw: curRaw);
out.putIfAbsent(cur, () => {})..addAll(lblMap);
});
}
return out;
}
@override
dynamic get keyIndex => key;
@override
bool operator ==(Object o) => o is Contact && o.key == key;
@override
int get hashCode => key.hashCode;
static String _encode(Object value) => jsonEncode(_stringifyKeys(value));
static dynamic _stringifyKeys(dynamic obj) {
if (obj is Map) {
return obj.map(
(k, v) => MapEntry(k.toString(), _stringifyKeys(v)),
);
}
if (obj is Iterable) return obj.map(_stringifyKeys).toList();
return obj;
}
static Map<String, Map<int, Map<String, String>>> _decodeParsed(String s) {
if (s.isEmpty) return {};
final Map<String, dynamic> data = jsonDecode(s) as Map<String, dynamic>;
return data.map((handle, byCur) {
final inner = (byCur as Map<String, dynamic>).map((curRaw, lblMap) {
final int cur = int.parse(curRaw);
final labels = (lblMap as Map).cast<String, String>();
return MapEntry(cur, labels);
});
return MapEntry(handle, inner);
});
}
static Map<int, Map<String, String>> _decodeManual(String s) {
if (s.isEmpty) return {};
final Map<String, dynamic> data = jsonDecode(s) as Map<String, dynamic>;
return data.map((curRaw, lblMap) {
final int cur = int.parse(curRaw);
final labels = (lblMap as Map).cast<String, String>();
return MapEntry(cur, labels);
});
}
factory Contact.fromParsed(ParsedAddress p, {String? localImage}) { factory Contact.fromParsed(ParsedAddress p, {String? localImage}) {
final manual = <int, Map<String, String>>{};
p.manualAddressByCurrencyMap?.forEach(
(cur, addr) => manual[cur.raw] = {cur.title: addr},
);
final parsed = <String, Map<int, Map<String, String>>>{};
if (p.parsedAddressByCurrencyMap.isNotEmpty) {
final hKey = '${p.addressSource.label}-${p.handle}';
parsed[hKey] = {
for (final e in p.parsedAddressByCurrencyMap.entries) e.key.raw: {e.key.title: e.value}
};
}
return Contact( return Contact(
name: p.profileName.isNotEmpty ? p.profileName : p.handle, name: p.profileName.isNotEmpty ? p.profileName : p.handle,
profileName: p.profileName, profileName: p.profileName,
@ -32,88 +165,8 @@ class Contact extends HiveObject with Keyable {
description: p.description, description: p.description,
source: p.addressSource, source: p.addressSource,
imagePath: localImage ?? '', imagePath: localImage ?? '',
parsedAddresses: { manualAddresses: manual,
if (p.parsedAddressByCurrencyMap.isNotEmpty) parsedByHandle: parsed,
for (final e in p.parsedAddressByCurrencyMap.entries) e.key.raw: {e.key.title: e.value}
},
manualAddresses: {
if (p.manualAddressByCurrencyMap != null && p.manualAddressByCurrencyMap!.isNotEmpty)
for (final e in p.manualAddressByCurrencyMap!.entries) e.key.raw: {e.key.title: e.value}
},
); );
} }
@HiveField(0, defaultValue: '')
String name;
@HiveField(1, defaultValue: {})
Map<int, Map<String, String>> parsedAddresses;
@HiveField(2, defaultValue: 0)
int sourceRaw;
@HiveField(3, defaultValue: '')
String handle;
@HiveField(4, defaultValue: '')
String imagePath;
@HiveField(5, defaultValue: '')
String profileName;
@HiveField(6, defaultValue: '')
String description;
@HiveField(7)
DateTime lastChange;
@HiveField(8, defaultValue: {})
Map<int, Map<String, String>> manualAddresses;
void setAddress(
{required CryptoCurrency currency,
required String label,
required String address,
bool isManual = false}) {
final target = isManual ? manualAddresses : parsedAddresses;
target.putIfAbsent(currency.raw, () => <String, String>{});
final inner = target[currency.raw]!;
final baseLabel = (label.trim().isEmpty ? currency.title : label).trim();
final uniqueLabel = _getUniqueLabel(baseLabel, inner);
inner[uniqueLabel] = address;
lastChange = DateTime.now();
save();
}
Map<CryptoCurrency, Map<String, String>> get parsedByCurrency =>
parsedAddresses.map((k, v) => MapEntry(CryptoCurrency.deserialize(raw: k), v));
Map<CryptoCurrency, Map<String, String>> get manualByCurrency =>
manualAddresses.map((k, v) => MapEntry(CryptoCurrency.deserialize(raw: k), v));
AddressSource get source => AddressSourceIndex.fromRaw(sourceRaw);
set source(AddressSource source) => sourceRaw = source.raw;
@override
dynamic get keyIndex => key;
String _getUniqueLabel(String base, Map<String, String> byLabel) {
if (!byLabel.containsKey(base)) return base;
var i = 1;
while (byLabel.containsKey('$base $i')) i++;
return base + '_' + '$i';
}
@override
bool operator ==(Object other) => other is Contact && other.key == key;
@override
int get hashCode => key.hashCode;
} }

View file

@ -14,117 +14,143 @@ part 'contact_record.g.dart';
class ContactRecord = ContactRecordBase with _$ContactRecord; class ContactRecord = ContactRecordBase with _$ContactRecord;
abstract class ContactRecordBase extends Record<Contact> with Store implements ContactBase { abstract class ContactRecordBase extends Record<Contact> with Store implements ContactBase {
ContactRecordBase(Box<Contact> source, Contact original) ContactRecordBase(Box<Contact> box, Contact original)
: name = original.name, : name = original.name,
handle = original.handle, handle = original.handle,
profileName = original.profileName, profileName = original.profileName,
description = original.description, description = original.description,
imagePath = original.imagePath, imagePath = original.imagePath,
sourceType = original.source, sourceType = original.source,
parsedAddresses = ObservableMap.of(original.parsedByCurrency), manual = ObservableMap.of(original.manualByCurrency),
manualAddresses = ObservableMap.of(original.manualByCurrency), parsedBlocks = ObservableMap.of({
super(source, original); for (final h in original.parsedByHandle.entries)
h.key: {
for (final cur in h.value.entries)
CryptoCurrency.deserialize(raw: cur.key): Map<String, String>.of(cur.value)
}
}),
super(box, original);
@observable @observable
String name; String name, handle, profileName, description, imagePath;
@observable
String handle;
@observable
String profileName;
@observable
String description;
@observable
String imagePath;
@observable @observable
AddressSource sourceType; AddressSource sourceType;
String address = ''; @observable
ObservableMap<CryptoCurrency, Map<String, String>> manual;
CryptoCurrency type = CryptoCurrency.btc;
@observable @observable
ObservableMap<CryptoCurrency, Map<String, String>> parsedAddresses; ObservableMap<String, Map<CryptoCurrency, Map<String, String>>> parsedBlocks;
@observable @computed
ObservableMap<CryptoCurrency, Map<String, String>> manualAddresses; Map<CryptoCurrency, Map<String, String>> get parsedByCurrency {
final out = <CryptoCurrency, Map<String, String>>{};
@override parsedBlocks.forEach((_, byCur) {
void toBind(Contact original) { byCur.forEach((cur, lbl) => out.putIfAbsent(cur, () => {})..addAll(lbl));
reaction((_) => name, (v) => original.name = v);
reaction((_) => handle, (v) => original.handle = v);
reaction((_) => profileName, (v) => original.profileName = v);
reaction((_) => description, (v) => original.description = v);
reaction((_) => imagePath, (v) => original.imagePath = v);
reaction((_) => sourceType, (v) => original.source = v);
bool _different(Map<String, String>? inner, String lbl, String addr) =>
inner == null || inner[lbl] != addr;
reaction((_) => Map.of(parsedAddresses), (_) {
parsedAddresses.forEach((cur, byLabel) {
byLabel.forEach((lbl, addr) {
final inner = original.parsedAddresses[cur.raw];
if (_different(inner, lbl, addr)) {
original.setAddress(
currency: cur,
label: lbl,
address: addr,
isManual: false,
);
}
});
});
});
reaction((_) => Map.of(manualAddresses), (_) {
manualAddresses.forEach((cur, byLabel) {
byLabel.forEach((lbl, addr) {
final inner = original.manualAddresses[cur.raw];
if (_different(inner, lbl, addr)) {
original.setAddress(
currency: cur,
label: lbl,
address: addr,
isManual: true,
);
}
});
});
});
}
@override
void fromBind(Contact original) {
name = original.name;
handle = original.handle;
profileName = original.profileName;
description = original.description;
imagePath = original.imagePath;
sourceType = original.source;
parsedAddresses = ObservableMap.of({
for (final e in original.parsedByCurrency.entries) e.key: Map<String, String>.of(e.value)
});
manualAddresses = ObservableMap.of({
for (final e in original.manualByCurrency.entries) e.key: Map<String, String>.of(e.value)
}); });
return out;
} }
@computed @computed
File? get avatarFile => imagePath.isEmpty ? null : File(imagePath); File? get avatarFile => imagePath.isEmpty ? null : File(imagePath);
@computed @computed
ImageProvider get avatarProvider { ImageProvider get avatarProvider => (avatarFile?.existsSync() ?? false)
final f = avatarFile; ? FileImage(avatarFile!)
return (f != null && f.existsSync())
? FileImage(f)
: const AssetImage('assets/images/profile.png'); : const AssetImage('assets/images/profile.png');
@override
void toBind(Contact c) {
reaction((_) => name, (v) => c.name = v);
reaction((_) => handle, (v) => c.handle = v);
reaction((_) => profileName, (v) => c.profileName = v);
reaction((_) => description, (v) => c.description = v);
reaction((_) => imagePath, (v) => c.imagePath = v);
reaction((_) => sourceType, (v) => c.source = v);
}
@override
void fromBind(Contact c) {
name = c.name;
handle = c.handle;
profileName = c.profileName;
description = c.description;
imagePath = c.imagePath;
sourceType = c.source;
} }
@action @action
void setParsedAddress(CryptoCurrency cur, String label, String addr) { void setManualAddress(CryptoCurrency cur, String label, String addr) {
final oldInner = parsedAddresses[cur] ?? {}; manual.putIfAbsent(cur, () => {})[label] = addr;
parsedAddresses[cur] = {...oldInner, label: addr}; _flushManual();
} }
@action
void removeManualAddress(CryptoCurrency cur, String label) {
final map = manual[cur];
if (map == null) return;
map.remove(label);
if (map.isEmpty) manual.remove(cur);
_flushManual();
}
@action
void setParsedAddress(String blockKey, CryptoCurrency cur, String label, String addr) {
final block = parsedBlocks.putIfAbsent(blockKey, () => {});
block.putIfAbsent(cur, () => {})[label] = addr;
parsedBlocks[blockKey] = {for (final e in block.entries) e.key: Map.of(e.value)};
_flushParsed();
}
@action
void removeParsedAddress(String blockKey, CryptoCurrency? cur, String? label) {
final block = parsedBlocks[blockKey];
if (block == null) return;
if (cur == null) {
parsedBlocks.remove(blockKey);
_flushParsed();
return;
}
final map = block[cur];
if (map == null) return;
if (label == null) {
block.remove(cur);
} else {
map.remove(label);
if (map.isEmpty) block.remove(cur);
}
if (block.isEmpty) {
parsedBlocks.remove(blockKey);
} else {
parsedBlocks[blockKey] = {for (final e in block.entries) e.key: Map.of(e.value)};
}
_flushParsed();
}
void _flushManual() {
original
..manualAddresses = {
for (final e in manual.entries) e.key.raw: Map<String, String>.of(e.value)
}
..lastChange = DateTime.now();
}
void _flushParsed() {
original
..parsedByHandle = {
for (final h in parsedBlocks.entries)
h.key: {for (final cur in h.value.entries) cur.key.raw: Map<String, String>.of(cur.value)}
}
..lastChange = DateTime.now();
}
@override
String address = '';
@override
CryptoCurrency type = CryptoCurrency.btc;
} }

View file

@ -432,7 +432,10 @@ Future<void> ios_migrate_address_book(Box<Contact> contactSource) async {
final address = _item["address"] as String; final address = _item["address"] as String;
final name = _item["name"] as String; final name = _item["name"] as String;
return Contact(parsedAddresses: {5 : {'label':address} //TODO fix this hardcoded value return Contact(parsedByHandle: {'handle':
{
5: {'label': address} //TODO fix this hardcoded value
}
}, manualAddresses: {}, name: name); }, manualAddresses: {}, name: name);
}); });

View file

@ -86,6 +86,19 @@ extension AddressSourceIndex on AddressSource {
AddressSource.values[raw.clamp(0, AddressSource.values.length - 1)]; AddressSource.values[raw.clamp(0, AddressSource.values.length - 1)];
} }
extension AddressSourceNameParser on AddressSource {
static AddressSource fromLabel(String? text) {
if (text == null || text.trim().isEmpty) {
return AddressSource.notParsed;
}
final needle = text.trim().toLowerCase();
return AddressSource.values.firstWhere(
(src) => src.label.toLowerCase() == needle,
orElse: () => AddressSource.notParsed,
);
}
}
class ParsedAddress { class ParsedAddress {
const ParsedAddress({ const ParsedAddress({
required this.parsedAddressByCurrencyMap, required this.parsedAddressByCurrencyMap,

View file

@ -13,7 +13,6 @@ import 'package:cake_wallet/entities/wallet_edit_page_arguments.dart';
import 'package:cake_wallet/exchange/trade.dart'; import 'package:cake_wallet/exchange/trade.dart';
import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/address_book/contact_page.dart';
import 'package:cake_wallet/src/screens/anonpay_details/anonpay_details_page.dart'; import 'package:cake_wallet/src/screens/anonpay_details/anonpay_details_page.dart';
import 'package:cake_wallet/src/screens/auth/auth_page.dart'; import 'package:cake_wallet/src/screens/auth/auth_page.dart';
import 'package:cake_wallet/src/screens/backup/backup_page.dart'; import 'package:cake_wallet/src/screens/backup/backup_page.dart';
@ -579,11 +578,6 @@ Route<dynamic> createRoute(RouteSettings settings) {
case Routes.pickerWalletAddress: case Routes.pickerWalletAddress:
return MaterialPageRoute<void>(builder: (_) => getIt.get<AddressListPage>()); return MaterialPageRoute<void>(builder: (_) => getIt.get<AddressListPage>());
case Routes.addressBookAddContact:
return handleRouteWithPlatformAwareness(
(context) => getIt.get<ContactPage>(param1: settings.arguments as ContactRecord?),
);
case Routes.showKeys: case Routes.showKeys:
return handleRouteWithPlatformAwareness( return handleRouteWithPlatformAwareness(
(context) => getIt.get<WalletKeysPage>(), (context) => getIt.get<WalletKeysPage>(),

View file

@ -34,10 +34,10 @@ class Routes {
static const addressBook = '/address_book'; static const addressBook = '/address_book';
static const pickerAddressBook = '/picker_address_book'; static const pickerAddressBook = '/picker_address_book';
static const pickerWalletAddress = '/picker_wallet_address'; static const pickerWalletAddress = '/picker_wallet_address';
static const addressBookAddContact = '/address_book_add_contact';
static const newContactWelcomePage = '/new_contact_welcome_page'; static const newContactWelcomePage = '/new_contact_welcome_page';
static const supportedHandlesPage = '/supported_handles_page'; static const supportedHandlesPage = '/supported_handles_page';
static const editNewContactGroupPage = '/edit_new_contact_group_page'; static const editNewContactGroupPage = '/edit_new_contact_group_page';
static const editContactGroupPage = '/edit_contact_group_page';
static const editAddressesPage = '/edit_addresses_page'; static const editAddressesPage = '/edit_addresses_page';
static const editAddressPage = '/edit_address_page'; static const editAddressPage = '/edit_address_page';
static const editNewContactPage = '/edit_new_contact_page'; static const editNewContactPage = '/edit_new_contact_page';

View file

@ -1,192 +0,0 @@
import 'package:cake_wallet/core/address_validator.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cw_core/currency.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:mobx/mobx.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/core/contact_name_validator.dart';
import 'package:cake_wallet/core/execution_state.dart';
import 'package:cake_wallet/view_model/contact_list/contact_view_model.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/widgets/address_text_field.dart';
import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
import 'package:cake_wallet/src/widgets/base_text_form_field.dart';
import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart';
class ContactPage extends BasePage {
ContactPage(this.contactViewModel);
// : _formKey = GlobalKey<FormState>(),
// _nameController = TextEditingController(),
// _addressController = TextEditingController(),
// _currencyTypeController = TextEditingController() {
// _nameController.text = contactViewModel.name;
// _addressController.text = contactViewModel.addresses.values.first;
// _nameController.addListener(() => contactViewModel.name = _nameController.text);
// _addressController.addListener(() => contactViewModel.address = _addressController.text);
//
// autorun((_) => _currencyTypeController.text = contactViewModel.currency?.toString() ?? '');
// }
@override
String get title => S.current.contact;
final ContactViewModel contactViewModel;
// final GlobalKey<FormState> _formKey;
// final TextEditingController _nameController;
// final TextEditingController _currencyTypeController;
// final TextEditingController _addressController;
bool _isEffectsApplied = false;
@override
Widget body(BuildContext context) {
final downArrow = Image.asset(
'assets/images/arrow_bottom_purple_icon.png',
color: Theme.of(context).colorScheme.primary,
height: 8,
);
_setEffects(context);
return Container();
// Observer(
// builder: (_) => ScrollableWithBottomSection(
// contentPadding: EdgeInsets.all(24),
// content: Form(
// key: _formKey,
// child: Column(
// mainAxisSize: MainAxisSize.min,
// children: <Widget>[
// BaseTextFormField(
// controller: _nameController,
// hintText: S.of(context).contact_name,
// validator: ContactNameValidator(),
// ),
// Padding(
// padding: EdgeInsets.only(top: 20),
// child: Container(
// child: InkWell(
// onTap: () => _presentCurrencyPicker(context),
// child: IgnorePointer(
// child: BaseTextFormField(
// controller: _currencyTypeController,
// hintText: S.of(context).settings_currency,
// suffixIcon: Row(
// mainAxisSize: MainAxisSize.min,
// mainAxisAlignment: MainAxisAlignment.end,
// children: <Widget>[
// downArrow,
// SizedBox(width: 16),
// ],
// ),
// ),
// ),
// ),
// ),
// ),
// if (contactViewModel.currency != null)
// Padding(
// padding: EdgeInsets.only(top: 20),
// child: AddressTextField(
// controller: _addressController,
// options: [
// AddressTextFieldOption.paste,
// AddressTextFieldOption.qrCode,
// ],
// buttonColor: Theme.of(context).colorScheme.surfaceContainerHighest,
// iconColor: Theme.of(context).colorScheme.onSurfaceVariant,
// validator: AddressValidator(type: contactViewModel.currency!),
// ),
// )
// ],
// ),
// ),
// bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24),
// bottomSection: Row(
// children: <Widget>[
// Expanded(
// child: PrimaryButton(
// onPressed: () {
// contactViewModel.reset();
// _nameController.text = '';
// _addressController.text = '';
// },
// text: S.of(context).reset,
// color: Theme.of(context).colorScheme.surfaceContainer,
// textColor: Theme.of(context).colorScheme.onSecondaryContainer,
// ),
// ),
// SizedBox(width: 20),
// Expanded(
// child: Observer(
// builder: (_) => PrimaryButton(
// onPressed: () async {
// if (_formKey.currentState != null && !_formKey.currentState!.validate()) {
// return;
// }
//
// await contactViewModel.save();
// },
// text: S.of(context).save,
// color: Theme.of(context).colorScheme.primary,
// textColor: Theme.of(context).colorScheme.onPrimary,
// isDisabled: !contactViewModel.isReady,
// ),
// ),
// )
// ],
// )),
// );
}
void _presentCurrencyPicker(BuildContext context) {
// showPopUp<void>(
// builder: (_) => CurrencyPicker(
// selectedAtIndex: contactViewModel.currency != null
// ? contactViewModel.currencies.indexOf(contactViewModel.currency!)
// : -1,
// items: contactViewModel.currencies,
// title: S.of(context).please_select,
// hintText: S.of(context).search_currency,
// onItemSelected: (Currency item) => contactViewModel.currency = item as CryptoCurrency),
// context: context,
// );
}
void _onContactSavingFailure(BuildContext context, String error) {
showPopUp<void>(
context: context,
builder: (BuildContext context) {
return AlertWithOneAction(
alertTitle: S.current.contact,
alertContent: error,
buttonText: S.of(context).ok,
buttonAction: () => Navigator.of(context).pop(),
);
});
}
void _onContactSavedSuccessfully(BuildContext context) => Navigator.of(context).pop();
void _setEffects(BuildContext context) {
if (_isEffectsApplied) {
return;
}
_isEffectsApplied = true;
reaction((_) => contactViewModel.state, (ExecutionState state) {
if (state is FailureState) {
_onContactSavingFailure(context, state.error);
}
if (state is ExecutedSuccessfullyState) {
_onContactSavedSuccessfully(context);
}
});
}
}

View file

@ -1,11 +1,14 @@
import 'dart:async'; import 'dart:async';
import 'package:cake_wallet/core/address_validator.dart';
import 'package:cake_wallet/entities/contact_record.dart';
import 'package:cake_wallet/entities/parsed_address.dart'; import 'package:cake_wallet/entities/parsed_address.dart';
import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/address_book/widgets/rounded_icon_button.dart';
import 'package:cake_wallet/src/screens/address_book/widgets/standard_text_form_field_widget.dart';
import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart'; import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart';
import 'package:cake_wallet/src/widgets/base_text_form_field.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/themes/core/material_base_theme.dart'; import 'package:cake_wallet/themes/core/material_base_theme.dart';
import 'package:cake_wallet/themes/utils/custom_theme_colors.dart'; import 'package:cake_wallet/themes/utils/custom_theme_colors.dart';
@ -14,24 +17,36 @@ import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/currency.dart'; import 'package:cw_core/currency.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
enum _InputMode { handle, address } enum _InputMode { handle, address }
class NewContactWelcomePage extends BasePage { class NewContactWelcomePage extends BasePage {
NewContactWelcomePage({required this.onSearch}); NewContactWelcomePage({required this.onSearch, this.handleOnly = false, this.existingContact});
final Future<List<ParsedAddress>> Function(String query) onSearch; final Future<List<ParsedAddress>> Function(String query) onSearch;
final bool handleOnly;
final ContactRecord? existingContact;
@override @override
Widget body(BuildContext context) => Widget body(BuildContext context) => NewContactWelcomePageBody(
NewContactWelcomePageBody(currentTheme: currentTheme, onSearch: onSearch); currentTheme: currentTheme,
onSearch: onSearch,
handleOnly: handleOnly,
existingContact: existingContact);
} }
class NewContactWelcomePageBody extends StatefulWidget { class NewContactWelcomePageBody extends StatefulWidget {
const NewContactWelcomePageBody({required this.currentTheme, required this.onSearch}); const NewContactWelcomePageBody(
{required this.currentTheme,
required this.onSearch,
required this.handleOnly,
required this.existingContact});
final MaterialThemeBase currentTheme; final MaterialThemeBase currentTheme;
final Future<List<ParsedAddress>> Function(String query) onSearch; final Future<List<ParsedAddress>> Function(String query) onSearch;
final bool handleOnly;
final ContactRecord? existingContact;
@override @override
State<NewContactWelcomePageBody> createState() => _NewContactWelcomePageBodyState(); State<NewContactWelcomePageBody> createState() => _NewContactWelcomePageBodyState();
@ -130,6 +145,7 @@ class _NewContactWelcomePageBodyState extends State<NewContactWelcomePageBody> {
} }
Widget _segmentedSwitcher(BuildContext ctx) { Widget _segmentedSwitcher(BuildContext ctx) {
if (widget.handleOnly) return const SizedBox.shrink();
final txt = Theme.of(ctx).textTheme.bodyMedium; final txt = Theme.of(ctx).textTheme.bodyMedium;
final seg = (_InputMode m, String label) => ButtonSegment<_InputMode>( final seg = (_InputMode m, String label) => ButtonSegment<_InputMode>(
value: m, value: m,
@ -150,13 +166,8 @@ class _NewContactWelcomePageBodyState extends State<NewContactWelcomePageBody> {
return SizedBox( return SizedBox(
width: double.infinity, width: double.infinity,
child: SegmentedButton<_InputMode>( child: SegmentedButton<_InputMode>(
segments: [ segments: [seg(_InputMode.handle, 'handle'), seg(_InputMode.address, 'address')],
seg(_InputMode.handle, 'handle'), selected: <_InputMode>{_mode},
seg(_InputMode.address, 'address'),
],
selected: <_InputMode>{
_mode
},
showSelectedIcon: false, showSelectedIcon: false,
style: ButtonStyle( style: ButtonStyle(
shape: WidgetStatePropertyAll( shape: WidgetStatePropertyAll(
@ -191,51 +202,40 @@ class _NewContactWelcomePageBodyState extends State<NewContactWelcomePageBody> {
? CustomThemeColors.backgroundGradientColorDark ? CustomThemeColors.backgroundGradientColorDark
: CustomThemeColors.backgroundGradientColorLight; : CustomThemeColors.backgroundGradientColorLight;
final hasDropdown = _results.isNotEmpty || _isSearching; final isHandleMode = widget.handleOnly ? true : _mode == _InputMode.handle;
final border = OutlineInputBorder(
borderRadius: BorderRadius.vertical(
top: const Radius.circular(12),
bottom: hasDropdown ? Radius.zero : const Radius.circular(12),
),
borderSide: BorderSide(color: Theme.of(context).colorScheme.outline),
);
final rounded = BorderRadius.vertical( return StandardTextFormFieldWidget(
top: const Radius.circular(12), focusNode: _focusNode,
bottom: hasDropdown ? Radius.zero : const Radius.circular(12),
);
final noStroke = OutlineInputBorder(borderRadius: rounded, borderSide: BorderSide.none);
final isHandleMode = _mode == _InputMode.handle;
return Theme(
data: Theme.of(context).copyWith(
inputDecorationTheme: InputDecorationTheme(
border: noStroke,
focusedBorder: noStroke,
enabledBorder: noStroke,
disabledBorder: border,
errorBorder: border,
focusedErrorBorder: border,
prefixIconConstraints: const BoxConstraints(minWidth: 0, minHeight: 0),
),
),
child: BaseTextFormField(
key: ValueKey(_mode),
controller: isHandleMode ? _handleCtl : _addressCtl, controller: isHandleMode ? _handleCtl : _addressCtl,
labelText: isHandleMode ? 'Enter handle' : 'Enter address',
fillColor: fillColor,
onChanged: isHandleMode ? _handleChanged : (v) => setState(() => _typedAddress = v.trim()),
prefixIcon: isHandleMode prefixIcon: isHandleMode
? null ? null
: Padding( : Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: _currencyPrefix(context), child: _currencyPrefix(context),
), ),
focusNode: _focusNode, suffixIcon: RoundedIconButton(
fillColor: fillColor, icon: Icons.paste_outlined,
hintText: isHandleMode ? 'Enter handle' : 'Enter address', onPressed: () async {
onChanged: isHandleMode ? _handleChanged : (v) => setState(() => _typedAddress = v.trim()), final data = await Clipboard.getData(Clipboard.kTextPlain);
textStyle: Theme.of(context).textTheme.bodyMedium!, final text = data?.text?.trim() ?? '';
if (text.isEmpty) return;
final isHandleMode = widget.handleOnly || _mode == _InputMode.handle;
if (isHandleMode) {
_handleCtl.text = text;
_handleChanged(text);
} else {
_addressCtl.text = text;
setState(() => _typedAddress = text);
}
},
shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(6))),
), ),
addressValidator: AddressValidator(type: _selectedCurrency),
); );
} }
@ -311,7 +311,7 @@ class _NewContactWelcomePageBodyState extends State<NewContactWelcomePageBody> {
}, },
), ),
), ),
_mode == _InputMode.handle widget.handleOnly || _mode == _InputMode.handle
? InkWell( ? InkWell(
splashFactory: NoSplash.splashFactory, splashFactory: NoSplash.splashFactory,
onTap: () { onTap: () {
@ -346,13 +346,14 @@ class _NewContactWelcomePageBodyState extends State<NewContactWelcomePageBody> {
onPressed: () { onPressed: () {
_focusNode.unfocus(); _focusNode.unfocus();
if (_mode == _InputMode.handle) { if (_mode == _InputMode.handle) {
Navigator.pushNamed(context, Routes.editNewContactGroupPage, Navigator.pushNamed(
arguments: _selected); context,
Routes.editNewContactGroupPage,
arguments: [_selected!, widget.existingContact],
);
_selected = null; _selected = null;
} else { } else {
Navigator.pushNamed(context, final parsed = ParsedAddress(
Routes.editNewContactGroupPage,
arguments: ParsedAddress(
parsedAddressByCurrencyMap: {}, parsedAddressByCurrencyMap: {},
manualAddressByCurrencyMap: { manualAddressByCurrencyMap: {
_selectedCurrency: _typedAddress.trim(), _selectedCurrency: _typedAddress.trim(),
@ -362,7 +363,13 @@ class _NewContactWelcomePageBodyState extends State<NewContactWelcomePageBody> {
profileName: '', profileName: '',
profileImageUrl: 'assets/images/profile.png', profileImageUrl: 'assets/images/profile.png',
description: '', description: '',
)); );
Navigator.pushNamed(
context,
Routes.editNewContactGroupPage,
arguments: [parsed, null],
);
} }
}, },
color: Theme.of(context).colorScheme.primary, color: Theme.of(context).colorScheme.primary,
@ -370,11 +377,12 @@ class _NewContactWelcomePageBodyState extends State<NewContactWelcomePageBody> {
height: 40, height: 40,
textColor: Theme.of(context).colorScheme.onPrimary, textColor: Theme.of(context).colorScheme.onPrimary,
isLoading: false, isLoading: false,
isDisabled: _mode == _InputMode.handle isDisabled: widget.handleOnly
? _selected == null || _isSearching ? _selected == null || _isSearching
: _typedAddress.isEmpty, : (_mode == _InputMode.handle
), ? _selected == null || _isSearching
) : _typedAddress.isEmpty),
)),
], ],
), ),
), ),

View file

@ -1,75 +1,38 @@
import 'package:cake_wallet/core/address_validator.dart'; import 'package:cake_wallet/core/address_validator.dart';
import 'package:cake_wallet/core/execution_state.dart';
import 'package:cake_wallet/entities/parsed_address.dart';
import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/address_book/widgets/addresses_expansion_tile_widget.dart'; import 'package:cake_wallet/src/screens/address_book/widgets/rounded_icon_button.dart';
import 'package:cake_wallet/src/screens/address_book/widgets/standard_text_form_field_widget.dart';
import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart'; import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/themes/core/material_base_theme.dart';
import 'package:cake_wallet/themes/utils/custom_theme_colors.dart'; import 'package:cake_wallet/themes/utils/custom_theme_colors.dart';
import 'package:cake_wallet/utils/address_formatter.dart';
import 'package:cake_wallet/utils/image_utill.dart'; import 'package:cake_wallet/utils/image_utill.dart';
import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/contact_list/contact_view_model.dart'; import 'package:cake_wallet/view_model/contact_list/contact_view_model.dart';
import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/currency.dart'; import 'package:cw_core/currency.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:mobx/mobx.dart';
class EditAddressPage extends BasePage { class EditAddressPage extends BasePage {
EditAddressPage({required this.contactViewModel}) EditAddressPage({required this.contactViewModel})
: _formKey = GlobalKey<FormState>(), : _formKey = GlobalKey<FormState>(),
manualAddress = contactViewModel.manualAddressesByCurrency[contactViewModel.initialCurrency] _labelController = TextEditingController(text: contactViewModel.label),
?[contactViewModel.manualLabel] ?? _addressController = TextEditingController(text: contactViewModel.address) {
'', _labelController.addListener(() => contactViewModel.label = _labelController.text);
_labelController = TextEditingController(), _addressController.addListener(() => contactViewModel.address = _addressController.text);
_manualAddressController = TextEditingController() {
_labelController.text = contactViewModel.manualLabel;
_manualAddressController.text = contactViewModel.isNewAddress ? '' : manualAddress ?? '';
_labelController.addListener(() => contactViewModel.manualLabel = _labelController.text);
_manualAddressController
.addListener(() => contactViewModel.manualAddress = _manualAddressController.text);
} }
@override @override
String get title => contactViewModel.isNewAddress ? 'Add Address' : 'Edit Address'; String get title => contactViewModel.isAddressEdit ? 'Edit Address' : 'Add Address';
final ContactViewModel contactViewModel; final ContactViewModel contactViewModel;
final GlobalKey<FormState> _formKey; final GlobalKey<FormState> _formKey;
final TextEditingController _labelController; final TextEditingController _labelController;
final TextEditingController _manualAddressController; final TextEditingController _addressController;
final String? manualAddress;
Widget _circleIcon(
{required BuildContext context,
required IconData icon,
required VoidCallback onPressed,
ShapeBorder? shape,
double? width,
double? height,
double? iconSize,
Color? fillColor}) {
final colorScheme = Theme.of(context).colorScheme;
return RawMaterialButton(
onPressed: onPressed,
fillColor: fillColor ?? colorScheme.surfaceContainerHighest,
elevation: 0,
constraints: BoxConstraints.tightFor(width: width ?? 24, height: height ?? 24),
padding: EdgeInsets.zero,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
shape: shape ?? const CircleBorder(),
child: Icon(icon, size: iconSize ?? 14, color: colorScheme.onSurface),
);
}
@override @override
Widget body(BuildContext context) { Widget body(BuildContext context) {
final theme = Theme.of(context);
final fillColor = currentTheme.isDark final fillColor = currentTheme.isDark
? CustomThemeColors.backgroundGradientColorDark.withAlpha(100) ? CustomThemeColors.backgroundGradientColorDark.withAlpha(100)
: CustomThemeColors.backgroundGradientColorLight; : CustomThemeColors.backgroundGradientColorLight;
@ -90,7 +53,7 @@ class EditAddressPage extends BasePage {
title: Text(initialCurrency.fullName ?? initialCurrency.name, title: Text(initialCurrency.fullName ?? initialCurrency.name,
style: Theme.of(context).textTheme.bodyMedium), style: Theme.of(context).textTheme.bodyMedium),
trailing: Icon(Icons.keyboard_arrow_down_outlined, trailing: Icon(Icons.keyboard_arrow_down_outlined,
size: 24, color: theme.colorScheme.onSurface), size: 24, color: Theme.of(context).colorScheme.onSurface),
tileColor: fillColor, tileColor: fillColor,
dense: true, dense: true,
visualDensity: VisualDensity(horizontal: 0, vertical: -3), visualDensity: VisualDensity(horizontal: 0, vertical: -3),
@ -100,113 +63,57 @@ class EditAddressPage extends BasePage {
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: const BorderRadius.all(Radius.circular(12)), borderRadius: const BorderRadius.all(Radius.circular(12)),
), ),
onTap: () => _presentCurrencyPicker(context, contactViewModel), onTap: () => _presentCurrencyPicker(context, contactViewModel)),
),
const SizedBox(height: 8), const SizedBox(height: 8),
TextField( StandardTextFormFieldWidget(
controller: _labelController, controller: _labelController,
decoration: InputDecoration(
isDense: true,
isCollapsed: true,
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
labelText: 'Address label', labelText: 'Address label',
labelStyle: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(color: Theme.of(context).hintColor),
hintStyle: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(color: Theme.of(context).hintColor),
fillColor: fillColor, fillColor: fillColor,
border: OutlineInputBorder( suffixIcon: RoundedIconButton(
borderRadius: BorderRadius.all(Radius.circular(15)), icon: Icons.paste_outlined,
borderSide: BorderSide(color: theme.colorScheme.outline)), onPressed: () async {
enabledBorder: OutlineInputBorder( final data = await Clipboard.getData(Clipboard.kTextPlain);
borderRadius: BorderRadius.all(Radius.circular(15)), final text = data?.text ?? '';
borderSide: BorderSide(color: Colors.transparent), if (text.trim().isEmpty) return;
), _labelController.text = text.trim();
suffixIcon: Padding( },
padding: const EdgeInsets.only(right: 10),
child: _circleIcon(
context: context,
icon: Icons.copy_all_outlined,
onPressed: () {},
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(6)))), borderRadius: BorderRadius.all(Radius.circular(6)))),
), addressValidator: (value) {
suffixIconConstraints: const BoxConstraints( if (value == null || value.trim().isEmpty) return 'Label cannot be empty';
minWidth: 34, return null;
maxWidth: 34, },
minHeight: 24,
maxHeight: 24,
)),
style: theme.textTheme.bodyMedium,
onChanged: (value) {},
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
TextFormField( StandardTextFormFieldWidget(
controller: _manualAddressController, controller: _addressController,
decoration: InputDecoration( labelText: S.of(context).address,
isDense: true,
isCollapsed: true,
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
labelText: 'Address',
labelStyle: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(color: Theme.of(context).hintColor),
hintStyle: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(color: Theme.of(context).hintColor),
fillColor: fillColor, fillColor: fillColor,
border: OutlineInputBorder( suffixIcon: RoundedIconButton(
borderRadius: BorderRadius.all(Radius.circular(15)), icon: Icons.paste_outlined,
borderSide: BorderSide(color: theme.colorScheme.outline)), onPressed: () async {
enabledBorder: OutlineInputBorder( final data = await Clipboard.getData(Clipboard.kTextPlain);
borderRadius: BorderRadius.all(Radius.circular(15)), final text = data?.text ?? '';
borderSide: BorderSide(color: Colors.transparent), if (text.trim().isEmpty) return;
), _addressController.text = text.trim();
suffixIcon: Padding( },
padding: const EdgeInsets.only(right: 10),
child: _circleIcon(
context: context,
icon: Icons.copy_all_outlined,
onPressed: () {},
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(6)))), borderRadius: BorderRadius.all(Radius.circular(6)))),
), addressValidator: AddressValidator(type: contactViewModel.currency)),
suffixIconConstraints: const BoxConstraints(
minWidth: 34,
maxWidth: 34,
minHeight: 24,
maxHeight: 24,
)),
style: theme.textTheme.bodyMedium,
onChanged: (value) {},
validator: AddressValidator(type: contactViewModel.currency),
),
], ],
), ),
Padding( Padding(
padding: const EdgeInsets.only(bottom: 24), padding: const EdgeInsets.only(bottom: 24),
child: Row( child: Row(
children: [ children: [
if (!contactViewModel.isNewAddress) if (contactViewModel.isAddressEdit)
Padding( Padding(
padding: const EdgeInsets.only(right: 8), padding: const EdgeInsets.only(right: 8),
child: _circleIcon( child: RoundedIconButton(
context: context,
icon: Icons.delete_outline_rounded, icon: Icons.delete_outline_rounded,
onPressed: () async { onPressed: () async {
contactViewModel.deleteManualAddress( await contactViewModel.deleteCurrentAddress();
initialCurrency, contactViewModel.manualLabel); if (context.mounted) Navigator.of(context, rootNavigator: true).pop();
contactViewModel.updateManualAddress();
await contactViewModel.save();
contactViewModel.reset();
_manualAddressController.clear();
Navigator.of(context, rootNavigator: true).pop();
}, },
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
@ -219,10 +126,7 @@ class EditAddressPage extends BasePage {
Expanded( Expanded(
child: ElevatedButton( child: ElevatedButton(
onPressed: () { onPressed: () {
contactViewModel.reset(); if (context.mounted) Navigator.of(context, rootNavigator: true).pop();
_labelController.clear();
_manualAddressController.clear();
Navigator.of(context, rootNavigator: true).pop();
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: fillColor, backgroundColor: fillColor,
@ -245,12 +149,12 @@ class EditAddressPage extends BasePage {
if (_formKey.currentState != null && !_formKey.currentState!.validate()) { if (_formKey.currentState != null && !_formKey.currentState!.validate()) {
return; return;
} }
contactViewModel.updateManualAddress(); if (contactViewModel.mode == ContactEditMode.manualAddress) {
await contactViewModel.save(); await contactViewModel.saveManualAddress();
} else {
if (context.mounted && contactViewModel.state is! FailureState) { await contactViewModel.saveParsedAddress();
Navigator.of(context, rootNavigator: true).pop();
} }
if (context.mounted) Navigator.of(context, rootNavigator: true).pop();
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.primary, backgroundColor: Theme.of(context).colorScheme.primary,

View file

@ -1,184 +0,0 @@
import 'package:cake_wallet/entities/parsed_address.dart';
import 'package:cake_wallet/src/screens/address_book/widgets/addresses_expansion_tile_widget.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/themes/utils/custom_theme_colors.dart';
import 'package:cake_wallet/utils/address_formatter.dart';
import 'package:cake_wallet/utils/image_utill.dart';
import 'package:cake_wallet/view_model/contact_list/contact_view_model.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart';
class EditAddressesPage extends BasePage {
EditAddressesPage({required this.contactViewModel});
final ContactViewModel contactViewModel;
Widget _circleIcon(BuildContext context, IconData icon, VoidCallback onPressed) {
final colorScheme = Theme.of(context).colorScheme;
return RawMaterialButton(
onPressed: onPressed,
fillColor: colorScheme.surfaceContainerHighest,
elevation: 0,
constraints: const BoxConstraints.tightFor(width: 24, height: 24),
padding: EdgeInsets.zero,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
shape: const CircleBorder(),
child: Icon(icon, size: 14, color: colorScheme.onSurface),
);
}
@override
Widget leading(BuildContext context) {
return SizedBox(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
_circleIcon(context, Icons.favorite_border_outlined, () {}),
const SizedBox(width: 8),
_circleIcon(context, Icons.refresh_sharp, () {}),
],
),
);
}
@override
Widget middle(BuildContext context) {
return Center(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(5),
child: Image(
width: 24,
height: 24,
image: contactViewModel.avatarProvider,
fit: BoxFit.cover,
),
),
const SizedBox(width: 12),
Text(
contactViewModel.name,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontSize: 18,
fontWeight: FontWeight.w600,
color: titleColor(context),
),
),
],
),
);
}
@override
Widget trailing(BuildContext context) {
return SizedBox(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
_circleIcon(context, Icons.add, () {}),
const SizedBox(width: 8),
_circleIcon(context, Icons.edit, () {}),
],
),
);
}
@override
Widget body(BuildContext context) {
final theme = Theme.of(context);
final fillColor = currentTheme.isDark
? CustomThemeColors.backgroundGradientColorDark.withAlpha(100)
: CustomThemeColors.backgroundGradientColorLight;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// ContactAddressesExpansionTile(
// key: ValueKey(contactViewModel.name),
// manualByCurrency: contact.manualAddresses,
// 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: [contact, cur, lbl],
// );
// },
// onCopyPressed: (addr) => Clipboard.setData(ClipboardData(text: addr)),
// ),
//
//
// ContactAddressesExpansionTile(
// key: ValueKey(contactViewModel.name),
// title: Text('Manual Addresses'),
// fillColor: fillColor,
// addresses: contactViewModel.manualAddressesByCurrency),
const SizedBox(height: 8),
Container(
width: double.infinity,
padding: const EdgeInsets.fromLTRB(8, 4, 8, 1),
decoration: BoxDecoration(
color: fillColor,
borderRadius: const BorderRadius.all(Radius.circular(12)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
ImageUtil.getImageFromPath(
imagePath: contactViewModel.sourceType.iconPath, height: 24, width: 24),
const SizedBox(width: 6),
Expanded(
child: Text(
contactViewModel.sourceType.label + ' - ' + contactViewModel.handle,
style: theme.textTheme.labelSmall?.copyWith(
fontSize: 12,
color: theme.colorScheme.onSurfaceVariant,
),
),
),
const SizedBox(width: 6),
_circleIcon(context, Icons.edit, () {}),
],
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text('Addresses detected:', style: theme.textTheme.bodyMedium),
const SizedBox(width: 8),
Expanded(
child: Wrap(
spacing: 8,
children: contactViewModel.parsedAddressesByCurrency.keys
.map((currency) => currency.iconPath != null
? Image.asset(currency.iconPath!, height: 24, width: 24)
: const SizedBox.shrink())
.toList(),
),
),
],
),
),
],
),
),
],
),
);
}
}

View file

@ -0,0 +1,264 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/address_book/widgets/rounded_icon_button.dart';
import 'package:cake_wallet/src/screens/address_book/widgets/standard_text_form_field_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/utils/image_utill.dart';
import 'package:cake_wallet/view_model/contact_list/contact_view_model.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
class EditContactGroupPage extends BasePage {
EditContactGroupPage({
required this.contactViewModel,
}) : _formKey = GlobalKey<FormState>(),
_groupLabelCtl = TextEditingController(text: contactViewModel.name) {
_groupLabelCtl.addListener(() {
contactViewModel.name = _groupLabelCtl.text;
});
}
final ContactViewModel contactViewModel;
final GlobalKey<FormState> _formKey;
final TextEditingController _groupLabelCtl;
@override
String? get title => 'Edit Contact';
@override
Widget body(BuildContext context) {
final theme = Theme.of(context);
final fillColor = currentTheme.isDark
? CustomThemeColors.backgroundGradientColorDark
: CustomThemeColors.backgroundGradientColorLight;
return LayoutBuilder(builder: (context, constraints) {
return SingleChildScrollView(
reverse: true,
child: ConstrainedBox(
constraints: BoxConstraints(minHeight: constraints.maxHeight),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
children: [
IntrinsicHeight(
child: Padding(
padding: const EdgeInsets.only(top: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
onTap: () {
//edit avatar
},
child: ConstrainedBox(
constraints: const BoxConstraints(
minWidth: 44,
maxWidth: 44,
minHeight: 44,
maxHeight: 44,
),
child: Container(
decoration: BoxDecoration(
color: fillColor,
borderRadius: const BorderRadius.all(Radius.circular(12)),
),
child: Padding(
padding: const EdgeInsets.fromLTRB(8, 4, 8, 1),
child: Column(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(5),
child: Image(
width: 24,
height: 24,
image: contactViewModel.avatar,
fit: BoxFit.cover,
),
),
const SizedBox(height: 1),
Text('Icon',
style: theme.textTheme.labelSmall?.copyWith(
fontSize: 8,
color: theme.colorScheme.onSurfaceVariant,
)),
],
),
),
),
),
),
const SizedBox(width: 6),
Expanded(
child: Form(
key: _formKey,
child: StandardTextFormFieldWidget(
controller: _groupLabelCtl,
labelText: 'Address group name',
fillColor: fillColor,
addressValidator: (value) {
// final text = value?.trim() ?? '';
// if (text.isEmpty) return 'Name cannot be empty';
//
// final clash = contactViewModel.box.values.any(
// (c) =>
// c.name.toLowerCase() == text.toLowerCase() &&
// c.key != contactViewModel.contactRecord?.original.key,
// );
// return clash ? 'Group with this name already exists' : null;
},
),
),
),
],
),
),
),
contactViewModel.userHandles.isEmpty
? Padding(
padding: const EdgeInsets.only(top: 16),
child: Text(
'No alias services found',
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
)
: Observer(builder: (_) {
final userHandlesList = contactViewModel.userHandles.toList();
return Column(
children: [
const SizedBox(height: 24),
Align(
alignment: Alignment.centerLeft,
child: Text(
'Alias Services',
style: Theme.of(context).textTheme.bodyMedium,
),
),
const SizedBox(height: 4),
ListView.separated(
shrinkWrap: true,
primary: false,
padding: const EdgeInsets.symmetric(vertical: 8),
itemCount: userHandlesList.length,
separatorBuilder: (context, index) => const SizedBox(height: 6),
itemBuilder: (context, index) {
final item = userHandlesList[index];
return ListTile(
title: Text(item.src?.label ?? '',
style: Theme.of(context).textTheme.labelSmall?.copyWith(
color:
Theme.of(context).colorScheme.onSurfaceVariant,
)),
subtitle: Text(item.label,
style: Theme.of(context).textTheme.bodySmall),
trailing: RoundedIconButton(
icon: Icons.delete_outline_rounded,
onPressed: () {
contactViewModel.deleteParsedBlock(item.handleKey);
},
iconSize: 20,
width: 28,
height: 28),
tileColor: fillColor,
dense: true,
visualDensity: VisualDensity(horizontal: 0, vertical: -3),
contentPadding: EdgeInsets.symmetric(horizontal: 12),
leading: ImageUtil.getImageFromPath(
imagePath: item.src?.iconPath ?? '',
height: 24,
width: 24),
shape: RoundedRectangleBorder(
borderRadius: const BorderRadius.all(Radius.circular(12)),
),
);
},
),
],
);
}),
],
),
Padding(
padding: const EdgeInsets.only(bottom: 24),
child: Row(
children: [
Padding(
padding: const EdgeInsets.only(right: 8),
child: RoundedIconButton(
icon: Icons.delete_outline_rounded,
onPressed: () async {
await contactViewModel.deleteContact();
if (context.mounted) Navigator.of(context, rootNavigator: true).pop();
},
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
width: 40,
height: 40,
iconSize: 30,
fillColor: Theme.of(context).colorScheme.errorContainer),
),
Expanded(
child: ElevatedButton(
onPressed: () {
if (context.mounted) Navigator.of(context).pop();
},
style: ElevatedButton.styleFrom(
backgroundColor: fillColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: Text(
S.of(context).cancel,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSecondaryContainer,
),
),
),
),
const SizedBox(width: 8),
Expanded(
child: ElevatedButton(
onPressed: () async {
if (_formKey.currentState != null &&
!_formKey.currentState!.validate()) {
return;
}
contactViewModel.name = _groupLabelCtl.text;
await contactViewModel.saveContactInfo();
if (context.mounted) Navigator.of(context).pop();
},
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.primary,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: Text(
S.of(context).save,
maxLines: 1,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onPrimary,
),
),
),
),
],
),
),
],
),
),
),
);
});
}
}

View file

@ -1,46 +1,28 @@
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/entities/parse_address_from_domain.dart';
import 'package:cake_wallet/entities/parsed_address.dart';
import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/address_book/entities/address_edit_request.dart';
import 'package:cake_wallet/src/screens/address_book/widgets/addresses_expansion_tile_widget.dart'; import 'package:cake_wallet/src/screens/address_book/widgets/addresses_expansion_tile_widget.dart';
import 'package:cake_wallet/src/screens/address_book/widgets/rounded_icon_button.dart';
import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/themes/utils/custom_theme_colors.dart'; import 'package:cake_wallet/themes/utils/custom_theme_colors.dart';
import 'package:cake_wallet/utils/image_utill.dart'; import 'package:cake_wallet/utils/image_utill.dart';
import 'package:cake_wallet/view_model/contact_list/contact_view_model.dart'; import 'package:cake_wallet/view_model/contact_list/contact_view_model.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
class EditContactPage extends BasePage { class EditContactPage extends BasePage {
EditContactPage({required this.contactViewModel}); EditContactPage({required this.contactViewModel});
final ContactViewModel contactViewModel; final ContactViewModel contactViewModel;
Widget _circleIcon(BuildContext context, IconData icon, VoidCallback onPressed) {
final colorScheme = Theme.of(context).colorScheme;
return RawMaterialButton(
onPressed: onPressed,
fillColor: colorScheme.surfaceContainerHighest,
elevation: 0,
constraints: const BoxConstraints.tightFor(width: 24, height: 24),
padding: EdgeInsets.zero,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
shape: const CircleBorder(),
child: Icon(icon, size: 14, color: colorScheme.onSurface),
);
}
@override
Widget leading(BuildContext context) {
return SizedBox(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
_circleIcon(context, Icons.favorite_border_outlined, () {}),
],
),
);
}
@override @override
Widget middle(BuildContext context) { Widget middle(BuildContext context) {
return Center( return Observer(
builder: (_) => Center(
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@ -49,10 +31,8 @@ class EditContactPage extends BasePage {
child: Image( child: Image(
width: 24, width: 24,
height: 24, height: 24,
image: contactViewModel.avatarProvider, image: contactViewModel.avatar,
fit: BoxFit.cover, fit: BoxFit.cover)),
),
),
const SizedBox(width: 12), const SizedBox(width: 12),
Text( Text(
contactViewModel.name, contactViewModel.name,
@ -64,18 +44,37 @@ class EditContactPage extends BasePage {
), ),
], ],
), ),
),
); );
} }
@override @override
Widget trailing(BuildContext context) { Widget trailing(BuildContext context) {
final onHandlerSearch = (query) async {
final address = await getIt
.get<AddressResolverService>()
.resolve(query: query as String, wallet: contactViewModel.wallet);
return address;
};
return SizedBox( return SizedBox(
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
_circleIcon(context, Icons.add, () {}), RoundedIconButton(icon: Icons.add, onPressed: () {
Navigator.pushNamed(
context,
Routes.newContactWelcomePage,
arguments: [onHandlerSearch, true, contactViewModel.record],
);
}),
const SizedBox(width: 8), const SizedBox(width: 8),
_circleIcon(context, Icons.edit, () {}), RoundedIconButton(icon: Icons.edit, onPressed: () {
Navigator.pushNamed(
context,
Routes.editContactGroupPage,
arguments: contactViewModel,
);
}),
], ],
), ),
); );
@ -98,32 +97,46 @@ class EditContactPage extends BasePage {
key: ValueKey(contactViewModel.name), key: ValueKey(contactViewModel.name),
title: Text('Manual Addresses'), title: Text('Manual Addresses'),
fillColor: fillColor, fillColor: fillColor,
manualByCurrency: contactViewModel.manualAddressesByCurrency, manualByCurrency: contactViewModel.manual,
onCopyPressed: (address) async => await Clipboard.setData(ClipboardData(text: address)), onCopyPressed: (address) async =>
await Clipboard.setData(ClipboardData(text: address)),
onEditPressed: (cur, lbl) { onEditPressed: (cur, lbl) {
Navigator.pushNamed( Navigator.pushNamed(
context, context,
Routes.editAddressPage, Routes.editAddressPage,
arguments: [ arguments: AddressEditRequest.address(
contactViewModel.contactRecord, contact: contactViewModel.record,
cur, currency: cur,
lbl, label: lbl,
], kindIsManual: true,
),
); );
}, },
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
ContactAddressesExpansionTile( ...contactViewModel.parsedBlocks.entries.map((entry) {
key: ValueKey(contactViewModel.name + '_Parsed'), final String handle = entry.key;
final stringSrc = handle.split('-').first;
final Map<CryptoCurrency, Map<String, String>> byCurrency = entry.value;
final src = AddressSourceNameParser.fromLabel(stringSrc);
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: ContactAddressesExpansionTile(
key: ValueKey('${contactViewModel.name}'),
title: Row( title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
ImageUtil.getImageFromPath( ImageUtil.getImageFromPath(
imagePath: contactViewModel.sourceType.iconPath, height: 24, width: 24), imagePath: src.iconPath,
height: 24,
width: 24,
),
const SizedBox(width: 6), const SizedBox(width: 6),
Expanded( Expanded(
child: Text( child: Text(
contactViewModel.sourceType.label + ' - ' + contactViewModel.handle, handle,
style: theme.textTheme.labelSmall?.copyWith( style: theme.textTheme.labelSmall?.copyWith(
fontSize: 12, fontSize: 12,
color: theme.colorScheme.onSurfaceVariant, color: theme.colorScheme.onSurfaceVariant,
@ -133,20 +146,25 @@ class EditContactPage extends BasePage {
], ],
), ),
fillColor: fillColor, fillColor: fillColor,
manualByCurrency: contactViewModel.parsedAddressesByCurrency, manualByCurrency: byCurrency,
onCopyPressed: (address) async => await Clipboard.setData(ClipboardData(text: address)), onCopyPressed: (addr) => Clipboard.setData(ClipboardData(text: addr)),
onEditPressed: (cur, lbl) { onEditPressed: (cur, lbl) {
Navigator.pushNamed( Navigator.pushNamed(
context, context,
Routes.editAddressPage, Routes.editAddressPage,
arguments: [ arguments: AddressEditRequest.address(
contactViewModel.contactRecord, contact: contactViewModel.record,
cur, currency: cur,
lbl, label: lbl,
], kindIsManual: false,
handle: handle,
handleKey: entry.key,
),
); );
}, },
), ),
);
}).toList(),
], ],
), ),
), ),

View file

@ -6,20 +6,20 @@ import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/themes/utils/custom_theme_colors.dart'; import 'package:cake_wallet/themes/utils/custom_theme_colors.dart';
import 'package:cake_wallet/utils/image_utill.dart'; import 'package:cake_wallet/utils/image_utill.dart';
import 'package:cake_wallet/view_model/contact_list/contact_view_model.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
class EditNewContactGroupPage extends BasePage { class EditNewContactGroupPage extends BasePage {
EditNewContactGroupPage({ EditNewContactGroupPage({
required this.selectedParsedAddress, required this.selectedParsedAddress,
required this.contacts, required this.contactViewModel,
}) : _formKey = GlobalKey<FormState>(), }) : _formKey = GlobalKey<FormState>(),
_groupLabelCtl = TextEditingController( _groupLabelCtl = TextEditingController(
text: selectedParsedAddress.profileName ?? '', text: selectedParsedAddress.profileName ?? '',
); );
final ParsedAddress selectedParsedAddress; final ParsedAddress selectedParsedAddress;
final Box<Contact> contacts; final ContactViewModel contactViewModel;
final GlobalKey<FormState> _formKey; final GlobalKey<FormState> _formKey;
final TextEditingController _groupLabelCtl; final TextEditingController _groupLabelCtl;
@ -127,7 +127,7 @@ class EditNewContactGroupPage extends BasePage {
final text = value?.trim() ?? ''; final text = value?.trim() ?? '';
if (text.isEmpty) return 'Name cannot be empty'; if (text.isEmpty) return 'Name cannot be empty';
final clash = contacts.values.any( final clash = contactViewModel.box.values.any(
(c) => c.name.toLowerCase() == text.toLowerCase(), (c) => c.name.toLowerCase() == text.toLowerCase(),
); );
return clash ? 'Group with this name already exists' : null; return clash ? 'Group with this name already exists' : null;
@ -149,22 +149,46 @@ class EditNewContactGroupPage extends BasePage {
height: 40, height: 40,
onPressed: () async { onPressed: () async {
if (!(_formKey.currentState?.validate() ?? false)) return; if (!(_formKey.currentState?.validate() ?? false)) return;
final localImg =
await ImageUtil.saveAvatarLocally(selectedParsedAddress.profileImageUrl);
final contact = Contact.fromParsed( if (contactViewModel.record != null) {
selectedParsedAddress.copyWith(profileName: _groupLabelCtl.text.trim()), final record = contactViewModel.record!;
localImage: localImg); final handleKey =
contacts.add(contact); '${selectedParsedAddress.addressSource.label}-${selectedParsedAddress.handle}'
final contactRecord = ContactRecord( .trim();
contacts,
contact, selectedParsedAddress.parsedAddressByCurrencyMap.forEach((cur, addr) {
record.setParsedAddress(
handleKey,
cur,
cur.title,
addr.trim(),
); );
});
if (context.mounted) { if (context.mounted) {
Navigator.of(context).pushNamed( Navigator.of(context, rootNavigator: true).pop();
Routes.editNewContactPage, }
arguments: [contactRecord], } else {
final localImg = await ImageUtil.saveAvatarLocally(
selectedParsedAddress.profileImageUrl);
final newContact = Contact.fromParsed(
selectedParsedAddress.copyWith(
profileName: _groupLabelCtl.text.trim(),
),
localImage: localImg,
); );
contactViewModel.box.add(newContact);
final record = ContactRecord(contactViewModel.box, newContact);
if (context.mounted) {
Navigator.pushNamed(
context,
Routes.editNewContactPage,
arguments: record,
);
}
} }
}, },
color: Theme.of(context).colorScheme.primary, color: Theme.of(context).colorScheme.primary,

View file

@ -1,14 +1,11 @@
import 'package:cake_wallet/entities/parsed_address.dart';
import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/address_book/entities/address_edit_request.dart';
import 'package:cake_wallet/src/screens/address_book/widgets/addresses_expansion_tile_widget.dart'; import 'package:cake_wallet/src/screens/address_book/widgets/addresses_expansion_tile_widget.dart';
import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/themes/utils/custom_theme_colors.dart'; import 'package:cake_wallet/themes/utils/custom_theme_colors.dart';
import 'package:cake_wallet/utils/address_formatter.dart';
import 'package:cake_wallet/utils/image_utill.dart'; import 'package:cake_wallet/utils/image_utill.dart';
import 'package:cake_wallet/view_model/contact_list/contact_view_model.dart'; import 'package:cake_wallet/view_model/contact_list/contact_view_model.dart';
import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@ -56,7 +53,7 @@ class EditNewContactPage extends BasePage {
child: Image( child: Image(
width: 24, width: 24,
height: 24, height: 24,
image: contactViewModel.avatarProvider, image: contactViewModel.avatar,
fit: BoxFit.cover, fit: BoxFit.cover,
), ),
), ),
@ -141,9 +138,11 @@ class EditNewContactPage extends BasePage {
Expanded( Expanded(
child: Wrap( child: Wrap(
spacing: 8, spacing: 8,
children: contactViewModel.parsedAddressesByCurrency.keys children: contactViewModel.parsedBlocks.values
.map((currency) => currency.iconPath != null .expand((map) => map.keys)
? Image.asset(currency.iconPath!, height: 24, width: 24) .toSet()
.map((CryptoCurrency cur) => cur.iconPath != null
? Image.asset(cur.iconPath!, height: 24, width: 24)
: const SizedBox.shrink()) : const SizedBox.shrink())
.toList(), .toList(),
), ),
@ -159,17 +158,18 @@ class EditNewContactPage extends BasePage {
key: ValueKey(contactViewModel.name), key: ValueKey(contactViewModel.name),
title: Text('Manual Addresses'), title: Text('Manual Addresses'),
fillColor: fillColor, fillColor: fillColor,
manualByCurrency: contactViewModel.manualAddressesByCurrency, manualByCurrency: contactViewModel.manual,
onCopyPressed: (address) async => await Clipboard.setData(ClipboardData(text: address)), onCopyPressed: (address) async => await Clipboard.setData(ClipboardData(text: address)),
onEditPressed: (cur, lbl) { onEditPressed: (cur, lbl) {
Navigator.pushNamed( Navigator.pushNamed(
context, context,
Routes.editAddressPage, Routes.editAddressPage,
arguments: [ arguments: AddressEditRequest.address(
contactViewModel.contactRecord, contact: contactViewModel.record,
cur, currency: cur,
lbl, label: lbl,
], kindIsManual: false,
)
); );
}, },
), ),

View file

@ -0,0 +1,52 @@
import 'package:cake_wallet/entities/contact_record.dart';
import 'package:cw_core/crypto_currency.dart';
enum EditMode {
contactFields,
manualAddressAdd,
manualAddressEdit,
parsedAddressAdd,
parsedAddressEdit,
}
class AddressEditRequest {
factory AddressEditRequest.contact(ContactRecord? c) => AddressEditRequest._(
contact: c,
mode: EditMode.contactFields,
);
factory AddressEditRequest.address({
required ContactRecord? contact,
required CryptoCurrency currency,
String? label,
required bool kindIsManual,
final String? handle,
String? handleKey,
}) =>
AddressEditRequest._(
contact: contact,
currency: currency,
label: label,
kindIsManual: kindIsManual,
handleKey: handleKey,
mode: label == null
? (kindIsManual ? EditMode.manualAddressAdd : EditMode.parsedAddressAdd)
: (kindIsManual ? EditMode.manualAddressEdit : EditMode.parsedAddressEdit),
);
const AddressEditRequest._({
this.contact,
this.currency,
this.label,
this.kindIsManual = false,
this.handleKey,
required this.mode,
});
final ContactRecord? contact;
final CryptoCurrency? currency;
final String? label;
final bool kindIsManual;
final EditMode mode;
final String? handleKey;
}

View file

@ -0,0 +1,23 @@
import 'package:cake_wallet/entities/parsed_address.dart';
class UserHandles {
factory UserHandles({required String handleKey}) {
assert(handleKey.isNotEmpty, 'handleKey cannot be empty');
final dash = handleKey.indexOf('-');
final prefix = dash == -1 ? handleKey : handleKey.substring(0, dash);
final label =
dash == -1 || dash == handleKey.length - 1 ? handleKey : handleKey.substring(dash + 1);
final src = AddressSourceNameParser.fromLabel(prefix);
return UserHandles._(handleKey, label, src);
}
const UserHandles._(this.handleKey, this.label, this.src);
final String handleKey;
final String label;
final AddressSource? src;
}

View file

@ -1,7 +1,7 @@
import 'package:cake_wallet/entities/parsed_address.dart'; 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/src/screens/base_page.dart';
import 'package:cake_wallet/themes/utils/custom_theme_colors.dart'; import 'package:cake_wallet/themes/utils/custom_theme_colors.dart';
import 'package:cake_wallet/utils/image_utill.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class SupportedHandlesPage extends BasePage { class SupportedHandlesPage extends BasePage {
@ -20,44 +20,4 @@ class SupportedHandlesPage extends BasePage {
} }
} }
class HandlesListWidget extends StatelessWidget {
const HandlesListWidget({
super.key,
required this.items,
required this.fillColor,
});
final List<AddressSource> items;
final Color fillColor;
@override
Widget build(BuildContext context) {
return ListView.separated(
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];
return ListTile(
title: Text(src.label, style: Theme
.of(context)
.textTheme
.bodyMedium),
trailing: Text(src.alias, style: Theme
.of(context)
.textTheme
.bodyMedium),
tileColor: fillColor,
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)),
),
onTap: () {},
);
},
);
}
}

View file

@ -0,0 +1,46 @@
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,
});
final List<AddressSource> items;
final Color fillColor;
@override
Widget build(BuildContext context) {
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];
return ListTile(
title: Text(src.label, style: Theme
.of(context)
.textTheme
.bodyMedium),
trailing: Text(src.alias, style: Theme
.of(context)
.textTheme
.bodyMedium),
tileColor: fillColor,
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)),
),
);
},
);
}
}

View file

@ -0,0 +1,35 @@
import 'package:flutter/material.dart';
class RoundedIconButton extends StatelessWidget {
const RoundedIconButton(
{required this.icon,
required this.onPressed,
this.shape,
this.width,
this.height,
this.iconSize,
this.fillColor});
final IconData icon;
final VoidCallback onPressed;
final ShapeBorder? shape;
final double? width;
final double? height;
final double? iconSize;
final Color? fillColor;
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return RawMaterialButton(
onPressed: onPressed,
fillColor: fillColor ?? colorScheme.surfaceContainerHighest,
elevation: 0,
constraints: BoxConstraints.tightFor(width: width ?? 24, height: height ?? 24),
padding: EdgeInsets.zero,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
shape: shape ?? const CircleBorder(),
child: Icon(icon, size: iconSize ?? 14, color: colorScheme.onSurface),
);
}
}

View file

@ -0,0 +1,63 @@
import 'package:flutter/material.dart';
class StandardTextFormFieldWidget extends StatelessWidget {
const StandardTextFormFieldWidget({
super.key,
required this.controller,
required this.labelText,
required this.fillColor,
required this.addressValidator,
this.focusNode,
this.suffixIcon,
this.prefixIcon,
this.suffix,
this.onChanged,
});
final TextEditingController controller;
final String labelText;
final Color fillColor;
final String? Function(String?)? addressValidator;
final FocusNode? focusNode;
final Widget? suffixIcon;
final Widget? prefixIcon;
final Widget? suffix;
final void Function(String)? onChanged;
@override
Widget build(BuildContext context) {
return TextFormField(
controller: controller,
focusNode: focusNode,
decoration: InputDecoration(
isDense: true,
isCollapsed: true,
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
labelText: labelText,
labelStyle:
Theme.of(context).textTheme.bodyMedium!.copyWith(color: Theme.of(context).hintColor),
hintStyle:
Theme.of(context).textTheme.bodyMedium!.copyWith(color: Theme.of(context).hintColor),
fillColor: fillColor,
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(15)),
borderSide: BorderSide(color: Theme.of(context).colorScheme.outline)),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(15)),
borderSide: BorderSide(color: Colors.transparent),
),
suffixIcon: Padding(padding: const EdgeInsets.only(right: 10), child: suffixIcon),
suffix: suffix,
prefixIcon: prefixIcon,
suffixIconConstraints: const BoxConstraints(
minWidth: 34,
maxWidth: 34,
minHeight: 24,
maxHeight: 24,
)),
style: Theme.of(context).textTheme.bodyMedium,
onChanged: onChanged,
validator: addressValidator,
);
}
}

View file

@ -6,7 +6,9 @@ import 'package:cake_wallet/entities/parse_address_from_domain.dart';
import 'package:cake_wallet/entities/wallet_list_order_types.dart'; import 'package:cake_wallet/entities/wallet_list_order_types.dart';
import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/address_book/entities/address_edit_request.dart';
import 'package:cake_wallet/src/screens/address_book/widgets/addresses_expansion_tile_widget.dart'; import 'package:cake_wallet/src/screens/address_book/widgets/addresses_expansion_tile_widget.dart';
import 'package:cake_wallet/src/screens/address_book/widgets/rounded_icon_button.dart';
import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/filter_list_widget.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/filter_list_widget.dart';
import 'package:cake_wallet/src/screens/wallet_list/filtered_list.dart'; import 'package:cake_wallet/src/screens/wallet_list/filtered_list.dart';
@ -63,19 +65,8 @@ class ContactListPage extends BasePage {
// FIX-ME: Style // FIX-ME: Style
//shape: CircleBorder(), //shape: CircleBorder(),
onPressed: () async { onPressed: () async {
if (contactListViewModel.shouldRequireTOTP2FAForAddingContacts) {
authService.authenticateAction(
context,
route: Routes.addressBookAddContact,
conditionToDetermineIfToUse2FA:
contactListViewModel.shouldRequireTOTP2FAForAddingContacts,
);
} else {
//await Navigator.of(context).pushNamed(Routes.addressBookAddContact); //TODO remove old flow
await _showAddressBookBottomSheet( await _showAddressBookBottomSheet(
context: context, contactListViewModel: contactListViewModel); context: context, contactListViewModel: contactListViewModel);
}
}, },
child: Offstage(), child: Offstage(),
), ),
@ -340,7 +331,7 @@ class _ContactListBodyState extends State<ContactListBody> {
child: ContactAddressesExpansionTile( child: ContactAddressesExpansionTile(
key: Key(contact.key.toString()), key: Key(contact.key.toString()),
contentPadding: EdgeInsets.symmetric(horizontal: 8), contentPadding: EdgeInsets.symmetric(horizontal: 8),
manualByCurrency: contact.manualAddresses, manualByCurrency: contact.manual,
fillColor: Theme.of(context).colorScheme.surfaceContainer, fillColor: Theme.of(context).colorScheme.surfaceContainer,
title: _buildContactTitle( title: _buildContactTitle(
context: context, context: context,
@ -351,7 +342,12 @@ class _ContactListBodyState extends State<ContactListBody> {
context: context, context: context,
contactListViewModel: widget.contactListViewModel, contactListViewModel: widget.contactListViewModel,
initialRoute: Routes.editAddressPage, initialRoute: Routes.editAddressPage,
initialArgs: [contact, cur, lbl], initialArgs: AddressEditRequest.address(
contact: contact,
currency: cur,
label: lbl,
kindIsManual: true,
),
); );
}, },
onCopyPressed: (addr) => Clipboard.setData(ClipboardData(text: addr)), onCopyPressed: (addr) => Clipboard.setData(ClipboardData(text: addr)),
@ -394,17 +390,20 @@ class _ContactListBodyState extends State<ContactListBody> {
Row( Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
_circleIcon( RoundedIconButton(
context: context,
icon: Icons.add, icon: Icons.add,
onPressed: () async => await _showAddressBookBottomSheet( onPressed: () async => await _showAddressBookBottomSheet(
context: context, context: context,
contactListViewModel: contactListViewModel, contactListViewModel: contactListViewModel,
initialRoute: Routes.editAddressPage, initialRoute: Routes.editAddressPage,
initialArgs: [contact, null, null])), initialArgs: AddressEditRequest.address(
contact: contact,
currency: walletTypeToCryptoCurrency(widget.contactListViewModel.wallet.type),
label: null,
kindIsManual: true,
))),
const SizedBox(width: 8), const SizedBox(width: 8),
_circleIcon( RoundedIconButton(
context: context,
icon: Icons.edit, icon: Icons.edit,
onPressed: () async => await _showAddressBookBottomSheet( onPressed: () async => await _showAddressBookBottomSheet(
context: context, context: context,
@ -464,24 +463,6 @@ class _ContactListBodyState extends State<ContactListBody> {
), ),
); );
} }
Widget _circleIcon(
{required BuildContext context,
required IconData icon,
required VoidCallback onPressed,
ShapeBorder? shape}) {
final colorScheme = Theme.of(context).colorScheme;
return RawMaterialButton(
onPressed: onPressed,
fillColor: colorScheme.surfaceContainerHighest,
elevation: 0,
constraints: const BoxConstraints.tightFor(width: 24, height: 24),
padding: EdgeInsets.zero,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
shape: shape ?? const CircleBorder(),
child: Icon(icon, size: 14, color: colorScheme.onSurface),
);
}
} }
Future<void> _showAddressBookBottomSheet( Future<void> _showAddressBookBottomSheet(

View file

@ -612,64 +612,13 @@ class SendPage extends BasePage {
// newContactAddress = null; // newContactAddress = null;
// } // }
bool showContactSheet = (newContactAddress != null && sendViewModel.showAddressBookPopup); // bool showContactSheet = (newContactAddress != null && sendViewModel.showAddressBookPopup);
await showModalBottomSheet<void>( await showModalBottomSheet<void>(
context: context, context: context,
isDismissible: false, isDismissible: false,
builder: (BuildContext bottomSheetContext) { builder: (BuildContext bottomSheetContext) {
return showContactSheet && sendViewModel.ocpRequest == null return InfoBottomSheet(
? InfoBottomSheet(
currentTheme: currentTheme,
footerType: FooterType.doubleActionButton,
titleText: S.of(bottomSheetContext).transaction_sent,
contentImage: 'assets/images/contact.png',
contentImageColor: Theme.of(context).colorScheme.onSurface,
content: S.of(bottomSheetContext).add_contact_to_address_book,
actionPanel: Padding(
padding: const EdgeInsets.only(left: 34.0),
child: Row(
children: [
SimpleCheckbox(
onChanged: (value) =>
sendViewModel.setShowAddressBookPopup(!value)),
const SizedBox(width: 8),
Text(
'Dont ask me next time',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14,
fontFamily: 'Lato',
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.titleLarge!.color,
decoration: TextDecoration.none,
),
),
],
),
),
doubleActionLeftButtonText: 'No',
doubleActionRightButtonText: 'Yes',
onLeftActionButtonPressed: () {
Navigator.of(bottomSheetContext).pop();
if (context.mounted) {
Navigator.of(context)
.pushNamedAndRemoveUntil(Routes.dashboard, (route) => false);
}
RequestReviewHandler.requestReview();
newContactAddress = null;
},
onRightActionButtonPressed: () {
Navigator.of(bottomSheetContext).pop();
RequestReviewHandler.requestReview();
if (context.mounted) {
Navigator.of(context).pushNamed(Routes.addressBookAddContact,
arguments: newContactAddress);
}
newContactAddress = null;
},
)
: InfoBottomSheet(
currentTheme: currentTheme, currentTheme: currentTheme,
footerType: FooterType.singleActionButton, footerType: FooterType.singleActionButton,
titleText: S.of(bottomSheetContext).transaction_sent, titleText: S.of(bottomSheetContext).transaction_sent,

View file

@ -4,13 +4,16 @@ import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/entities/contact_record.dart'; import 'package:cake_wallet/entities/contact_record.dart';
import 'package:cake_wallet/entities/parsed_address.dart'; import 'package:cake_wallet/entities/parsed_address.dart';
import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/address_book/contact_welcome_page.dart';
import 'package:cake_wallet/src/screens/address_book/edit_address_page.dart'; import 'package:cake_wallet/src/screens/address_book/edit_address_page.dart';
import 'package:cake_wallet/src/screens/address_book/edit_addresses_page.dart'; import 'package:cake_wallet/src/screens/address_book/edit_contact_group_page.dart';
import 'package:cake_wallet/src/screens/address_book/edit_contact_page.dart'; 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_group_page.dart';
import 'package:cake_wallet/src/screens/address_book/edit_new_contact_page.dart'; import 'package:cake_wallet/src/screens/address_book/edit_new_contact_page.dart';
import 'package:cake_wallet/src/screens/address_book/new_contact_welcome_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/address_book/supported_handles_page.dart';
import 'package:cake_wallet/view_model/contact_list/contact_view_model.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -108,24 +111,40 @@ class _AddContactNavigator extends StatelessWidget {
case Routes.supportedHandlesPage: case Routes.supportedHandlesPage:
page = SupportedHandlesPage(); page = SupportedHandlesPage();
break; break;
case Routes.newContactWelcomePage:
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;
page = NewContactWelcomePage(
onSearch : onSearch,
handleOnly : handleOnly,
existingContact: contact,
);
break;
case Routes.editNewContactGroupPage: case Routes.editNewContactGroupPage:
page = getIt<EditNewContactGroupPage>(param1: args as ParsedAddress); final list = args as List<dynamic>;
page = getIt<EditNewContactGroupPage>(
param1: list[0] as ParsedAddress,
param2: list.length > 1 ? list[1] as ContactRecord? : null,
);
break;
case Routes.editContactGroupPage:
final vm = args as ContactViewModel;
page = getIt<EditContactGroupPage>(param1: vm);
break; break;
case Routes.editNewContactPage: case Routes.editNewContactPage:
final list = args as List<dynamic>; page = getIt<EditNewContactPage>(param1: args as ContactRecord);
page = getIt<EditNewContactPage>(param1: list.first as ContactRecord?);
break;
case Routes.editAddressesPage:
page = getIt<EditAddressesPage>(param1: args as ContactRecord);
break; break;
case Routes.editContactPage: case Routes.editContactPage:
page = getIt<EditContactPage>(param1: args as ContactRecord); page = getIt<EditContactPage>(param1: args as ContactRecord);
break; break;
case Routes.editAddressPage: case Routes.editAddressPage:
page = getIt<EditAddressPage>(param1: args as List<dynamic>); page = getIt<EditAddressPage>(param1: args as AddressEditRequest);
break; break;
default: default:
page = NewContactWelcomePage(onSearch: onHandlerSearch); page = NewContactWelcomePage(onSearch: onHandlerSearch, handleOnly: false);
} }
return MaterialPageRoute( return MaterialPageRoute(

View file

@ -1,237 +1,272 @@
import 'dart:io'; import 'dart:io';
import 'package:cake_wallet/core/execution_state.dart';
import 'package:cake_wallet/entities/contact.dart';
import 'package:cake_wallet/entities/contact_record.dart'; import 'package:cake_wallet/entities/contact_record.dart';
import 'package:cake_wallet/entities/parsed_address.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:cw_core/crypto_currency.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:mobx/mobx.dart'; import 'package:mobx/mobx.dart';
import 'package:cake_wallet/core/execution_state.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/entities/contact.dart';
import 'package:cw_core/crypto_currency.dart';
part 'contact_view_model.g.dart'; part 'contact_view_model.g.dart';
class ContactViewModel = ContactViewModelBase with _$ContactViewModel; enum ContactEditMode {
contactInfo,
manualAddress,
parsedAddress,
}
abstract class ContactViewModelBase with Store { class ContactViewModel = _ContactViewModel with _$ContactViewModel;
ContactViewModelBase(this._box, {ContactRecord? contact, required List<dynamic>? initialParams,})
: state = InitialExecutionState(), abstract class _ContactViewModel with Store {
_ContactViewModel(
this.box,
this.wallet, {
AddressEditRequest? request,
}) : mode = request?.mode == EditMode.manualAddressAdd ||
request?.mode == EditMode.manualAddressEdit
? ContactEditMode.manualAddress
: request?.mode == EditMode.parsedAddressAdd ||
request?.mode == EditMode.parsedAddressEdit
? ContactEditMode.parsedAddress
: ContactEditMode.contactInfo,
record = request?.contact,
currencies = CryptoCurrency.all, currencies = CryptoCurrency.all,
contactRecord = contact, state = InitialExecutionState(),
name = contact?.name ?? '', name = request?.contact?.name ?? '',
handle = contact?.handle ?? '', handle = request?.contact?.handle ?? '',
profileName = contact?.profileName ?? '', profileName = request?.contact?.profileName ?? '',
description = contact?.description ?? '', description = request?.contact?.description ?? '',
imagePath = contact?.imagePath ?? '', imagePath = request?.contact?.imagePath ?? '',
sourceType = contact?.sourceType ?? AddressSource.notParsed, sourceType = request?.contact?.sourceType ?? AddressSource.notParsed,
currency = request?.currency ?? CryptoCurrency.xmr,
label = request?.label ?? '',
address = '',
handleKey = request?.handleKey ?? '' {
_initMapsFromRecord();
currency = (initialParams != null && if (request?.label != null && record != null) {
initialParams.isNotEmpty && currency = request!.currency!;
initialParams[0] is CryptoCurrency) label = request.label!;
? initialParams[0] as CryptoCurrency address = _targetMap[currency]?[label] ?? '';
: CryptoCurrency.xmr,
initialCurrency = (initialParams != null && _rememberOriginal(
initialParams.isNotEmpty && blockKey: mode == ContactEditMode.parsedAddress
initialParams[0] is CryptoCurrency) ? (request.handleKey ?? _defaultHandleKey())
? initialParams[0] as CryptoCurrency
: null, : null,
manualLabel = (initialParams != null && );
initialParams.length > 1 && }
initialParams[1] is String)
? initialParams[1] as String
: '',
isNewAddress = !(initialParams != null &&
initialParams.isNotEmpty &&
initialParams[0] is CryptoCurrency) {
const _emptyParsed = <CryptoCurrency, Map<String, String>>{};
const _emptyManual = <CryptoCurrency, Map<String, String>>{};
final parsedRaw = contact?.parsedAddresses ?? _emptyParsed;
final manualRaw = contact?.manualAddresses ?? _emptyManual;
parsedAddressesByCurrency = ObservableMap.of({
for (final e in parsedRaw.entries)
e.key: Map<String, String>.of(e.value)
});
manualAddressesByCurrency = ObservableMap.of({
for (final e in manualRaw.entries)
e.key: Map<String, String>.of(e.value)
});
} }
final Box<Contact> box;
final WalletBase wallet;
ContactRecord? record;
@observable @observable
ExecutionState state; ExecutionState state;
@observable @observable
String name; String name, handle, profileName, description, imagePath;
@observable
String handle;
@observable
String profileName;
@observable
String description;
@observable
String imagePath;
@observable @observable
AddressSource sourceType; AddressSource sourceType;
@observable @observable
CryptoCurrency currency; CryptoCurrency currency;
@observable @observable
String manualAddress = ''; String label, address, handleKey;
@observable
String manualLabel = '';
@observable @observable
ObservableMap<CryptoCurrency, Map<String, String>> parsedAddressesByCurrency = ObservableMap< ObservableMap<CryptoCurrency, Map<String, String>> manual = ObservableMap();
CryptoCurrency,
Map<String, String>>();
@observable @observable
ObservableMap<CryptoCurrency, Map<String, String>> manualAddressesByCurrency = ObservableMap< ObservableMap<CryptoCurrency, Map<String, String>> parsed = ObservableMap();
CryptoCurrency, @observable
Map<String, String>>(); ObservableMap<String, Map<CryptoCurrency, Map<String, String>>> parsedBlocks = ObservableMap();
final Box<Contact> _box; final ContactEditMode mode;
final ContactRecord? contactRecord;
final List<CryptoCurrency> currencies; final List<CryptoCurrency> currencies;
late final bool isNewAddress;
CryptoCurrency? initialCurrency; CryptoCurrency? _originalCur;
String? _originalLabel, _originalAddress, _originalHandleKey;
@computed @computed
bool get isReady => bool get isReady => name.trim().isNotEmpty || manual.isNotEmpty || parsed.isNotEmpty;
name
.trim()
.isNotEmpty && parsedAddressesByCurrency.isNotEmpty;
ImageProvider get avatarProvider { @computed
final file = avatarFile; List<UserHandles> get userHandles =>
return (file != null && file.existsSync()) parsedBlocks.keys.map((k) => UserHandles(handleKey: k)).toList();
? FileImage(file)
: const AssetImage('assets/images/profile.png'); @computed
ImageProvider get avatar => imagePath.isEmpty
? const AssetImage('assets/images/profile.png')
: FileImage(File(imagePath));
bool get isAddressEdit =>
mode != ContactEditMode.contactInfo && record != null && (_originalLabel ?? '').isNotEmpty;
ObservableMap<CryptoCurrency, Map<String, String>> get _targetMap =>
mode == ContactEditMode.manualAddress
? manual
: parsed[currency] != null
? parsed
: manual;
@action
Future<void> saveContactInfo() async {
if (record != null) {
record!
..name = name.trim()
..handle = handle.trim()
..profileName = profileName.trim()
..description = description.trim()
..imagePath = imagePath
..sourceType = sourceType;
record!.original..lastChange = DateTime.now();
await record!.original.save();
state = ExecutedSuccessfullyState();
return;
}
final newContact = Contact(
name: name.trim(),
address: '',
)
..handle = handle.trim()
..profileName = profileName.trim()
..description = description.trim()
..imagePath = imagePath
..source = sourceType
..lastChange = DateTime.now();
await box.put(newContact.key, newContact);
record = ContactRecord(box, newContact);
state = ExecutedSuccessfullyState();
} }
@action @action
void updateManualAddress() { Future<void> saveManualAddress() async {
if (manualAddress _ensureRecord();
.trim()
.isEmpty) return;
final inner = manualAddressesByCurrency.putIfAbsent(currency, () => {}); final map = manual.putIfAbsent(currency, () => {});
final base = manualLabel final oldLabel = isAddressEdit ? _originalLabel : null;
.trim() final newLabel = label.trim().isEmpty ? currency.title : label.trim();
.isEmpty ? currency.title : manualLabel.trim(); final newAddress = address.trim();
var label = base; if (oldLabel != null && oldLabel != newLabel) map.remove(oldLabel);
var i = 1; map[newLabel] = newAddress;
while (inner.containsKey(label)) { manual[currency] = Map.of(map);
label = '$base $i';
i++;
}
inner[label] = manualAddress.trim(); record!.setManualAddress(currency, newLabel, newAddress);
_rememberOriginal();
manualAddressesByCurrency[currency] = Map<String, String>.of(inner); state = ExecutedSuccessfullyState();
} }
@action @action
Future<void> pickAvatar(String localPath) async { Future<void> saveParsedAddress() async {
imagePath = localPath; _ensureRecord();
final blockKey = handleKey.trim().isEmpty ? _defaultHandleKey() : handleKey.trim();
final block = parsedBlocks.putIfAbsent(blockKey, () => {});
final map = block.putIfAbsent(currency, () => {});
final oldLabel = isAddressEdit ? _originalLabel : null;
final newLabel = label.trim().isEmpty ? currency.title : label.trim();
final newAddress = address.trim();
if (oldLabel != null && oldLabel != newLabel) map.remove(oldLabel);
map[newLabel] = newAddress;
parsedBlocks[blockKey] = {for (final e in block.entries) e.key: Map.of(e.value)};
record!.setParsedAddress(blockKey, currency, newLabel, newAddress);
_rememberOriginal(blockKey: blockKey);
state = ExecutedSuccessfullyState();
} }
void deleteManualAddress(CryptoCurrency cur, String label) { @action
final inner = manualAddressesByCurrency[cur]; Future<void> deleteCurrentAddress() async {
if (inner == null) return; if (!isAddressEdit) return;
inner.remove(label); _ensureRecord();
manualAddressesByCurrency[cur] = Map<String, String>.of(inner);
if (mode == ContactEditMode.manualAddress) {
final map = manual[_originalCur]!;
map.remove(_originalLabel);
if (map.isEmpty) manual.remove(_originalCur);
manual[_originalCur!] = Map.of(map);
record!.removeManualAddress(_originalCur!, _originalLabel!);
} else {
final block = parsedBlocks[_originalHandleKey]!;
final curMap = block[_originalCur]!;
curMap.remove(_originalLabel);
if (curMap.isEmpty) block.remove(_originalCur);
if (block.isEmpty)
parsedBlocks.remove(_originalHandleKey);
else
parsedBlocks[_originalHandleKey!] = {for (final e in block.entries) e.key: Map.of(e.value)};
record!.removeParsedAddress(_originalHandleKey!, _originalCur!, _originalLabel!);
}
state = ExecutedSuccessfullyState();
}
@action
Future<void> deleteParsedBlock(String handleKey) async {
if (!parsedBlocks.containsKey(handleKey)) return;
parsedBlocks.remove(handleKey);
record!.removeParsedAddress(handleKey, null, null);
state = ExecutedSuccessfullyState();
}
@action
Future<void> deleteContact() async {
if (record == null) return;
await record!.original.delete();
record = null;
reset();
state = ExecutedSuccessfullyState();
} }
@action @action
void reset() { void reset() {
name = ''; name = handle = profileName = description = imagePath = '';
handle = ''; label = address = handleKey = '';
profileName = ''; currency = CryptoCurrency.xmr;
description = ''; manual.clear();
imagePath = ''; parsed.clear();
parsedAddressesByCurrency.clear(); parsedBlocks.clear();
manualAddressesByCurrency.clear(); _originalCur = null;
_originalLabel = null;
_originalAddress = null;
_originalHandleKey = null;
state = InitialExecutionState();
} }
Future<void> save() async { void _initMapsFromRecord() {
if (record == null) return;
try { manual = ObservableMap.of(record!.manual);
state = IsExecutingState(); parsed = ObservableMap.of(record!.parsedByCurrency);
parsedBlocks = ObservableMap.of(record!.parsedBlocks);
final clash = _box.values.any(
(c) => c.name == name && c.key != contactRecord?.original.key,
);
if (clash) {
state = FailureState(S.current.contact_name_exists);
return;
} }
if (contactRecord != null && contactRecord!.original.isInBox) { void _ensureRecord() {
if (record != null) return;
final contact = contactRecord!.original; final newContact = Contact(name: name.trim().isEmpty ? 'No name' : name, address: '');
box.put(newContact.key, newContact);
contact record = ContactRecord(box, newContact);
..name = name
..handle = handle
..profileName = profileName
..description = description
..imagePath = imagePath
..source = sourceType;
contact.parsedAddresses
..clear()
..addAll({
for (final e in parsedAddressesByCurrency.entries)
e.key.raw: Map<String, String>.of(e.value)
});
contact.manualAddresses
..clear()
..addAll({
for (final e in manualAddressesByCurrency.entries)
e.key.raw: Map<String, String>.of(e.value)
});
await contact.save();
contactRecord!
..parsedAddresses = ObservableMap.of(contact.parsedByCurrency)
..manualAddresses = ObservableMap.of(contact.manualByCurrency);
} else {
final newContact = Contact(
name: name,
parsedAddresses: {
for (final e in parsedAddressesByCurrency.entries)
e.key.raw: Map<String, String>.of(e.value)
},
manualAddresses: {
for (final e in manualAddressesByCurrency.entries)
e.key.raw: Map<String, String>.of(e.value)
},
source: sourceType,
handle: handle,
profileName: profileName,
description: description,
imagePath: imagePath,
lastChange: DateTime.now(),
);
await _box.add(newContact);
}
state = ExecutedSuccessfullyState();
} catch (e, st) {
debugPrintStack(label: 'save() failed', stackTrace: st);
state = FailureState(e.toString());
}
} }
File? get avatarFile => imagePath.isEmpty ? null : File(imagePath); String _defaultHandleKey() => '${sourceType.label}-${handle}'.trim();
void _rememberOriginal({String? blockKey}) {
_originalCur = currency;
_originalLabel = label.trim().isEmpty ? currency.title : label.trim();
_originalAddress = address.trim();
_originalHandleKey = blockKey ?? _defaultHandleKey();
}
} }

View file

@ -711,7 +711,12 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
contactListViewModel.contactSource, contactListViewModel.contactSource,
Contact( Contact(
name: '', name: '',
parsedAddresses: {3: {'label4':address} parsedByHandle: {
'handle':
{
3: {'label4': address}
}
}, },
manualAddresses: {3: {'label5':address} //TODO : Fix this hardcoded value manualAddresses: {3: {'label5':address} //TODO : Fix this hardcoded value
}, },