Merge a8eb98b612
into 5082dc20f3
BIN
assets/images/add_contact_coins_img.png
Normal file
After Width: | Height: | Size: 23 KiB |
BIN
assets/images/fio.png
Normal file
After Width: | Height: | Size: 7.8 KiB |
10
assets/images/mastodon.svg
Normal file
|
@ -0,0 +1,10 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M22.7872 5.31335C22.4522 2.73468 20.2819 0.702486 17.7091 0.308701C17.275 0.242161 15.6304 0 11.8207 0H11.7923C7.98156 0 7.16402 0.242161 6.72995 0.308701C4.2288 0.691576 1.94467 2.5176 1.3905 5.12682C1.12395 6.41183 1.0955 7.83642 1.14502 9.14324C1.21561 11.0172 1.2293 12.888 1.39366 14.7544C1.50728 15.9941 1.7055 17.224 1.98681 18.4348C2.51359 20.671 4.646 22.5319 6.7352 23.2911C8.972 24.0829 11.3775 24.2143 13.6824 23.6707C13.9359 23.6096 14.1863 23.5387 14.4336 23.458C14.9941 23.2737 15.6504 23.0675 16.133 22.7054C16.1396 22.7003 16.145 22.6937 16.1488 22.6862C16.1527 22.6786 16.1548 22.6702 16.1551 22.6617V20.8531C16.155 20.8452 16.1531 20.8373 16.1497 20.8302C16.1463 20.8231 16.1414 20.8168 16.1353 20.8119C16.1292 20.807 16.1222 20.8035 16.1147 20.8018C16.1072 20.8001 16.0994 20.8001 16.0919 20.8019C14.6152 21.1671 13.102 21.3502 11.5837 21.3473C8.97086 21.3473 8.26815 20.0634 8.0669 19.5289C7.90516 19.0669 7.80246 18.5851 7.76138 18.0956C7.76094 18.0873 7.76241 18.0791 7.76564 18.0716C7.76884 18.0641 7.77373 18.0574 7.7799 18.0522C7.78606 18.047 7.7933 18.0434 7.80108 18.0416C7.80885 18.0399 7.81689 18.04 7.8246 18.0421C9.2767 18.4048 10.7653 18.5879 12.259 18.5875C12.6183 18.5875 12.9765 18.5875 13.3358 18.5777C14.8381 18.5341 16.4216 18.4544 17.8998 18.1556C17.9367 18.1479 17.9735 18.1414 18.0051 18.1316C20.3367 17.6679 22.5555 16.2128 22.7809 12.528C22.7894 12.383 22.8104 11.0085 22.8104 10.858C22.8115 10.3464 22.9695 7.22883 22.7872 5.31335Z" fill="url(#paint0_linear_3451_1074)"/>
|
||||
<path d="M19.0934 8.21051V14.6169H16.6418V8.39923C16.6418 7.09026 16.115 6.42267 15.0436 6.42267C13.8657 6.42267 13.2757 7.21242 13.2757 8.7723V12.1756H10.8388V8.7723C10.8388 7.21242 10.2478 6.42267 9.0699 6.42267C8.00473 6.42267 7.47267 7.09026 7.47267 8.39923V14.6169H5.02209V8.21051C5.02209 6.90154 5.34483 5.86164 5.99033 5.09079C6.65617 4.32176 7.52959 3.92688 8.61368 3.92688C9.86848 3.92688 10.8167 4.42647 11.4488 5.42457L12.0588 6.48485L12.6699 5.42457C13.302 4.42647 14.2502 3.92688 15.5029 3.92688C16.586 3.92688 17.4594 4.32176 18.1273 5.09079C18.7721 5.86091 19.0941 6.90081 19.0934 8.21051Z" fill="white"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_3451_1074" x1="12" y1="0" x2="12" y2="24" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#6364FF"/>
|
||||
<stop offset="1" stop-color="#563ACC"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 2.4 KiB |
BIN
assets/images/nostr.png
Normal file
After Width: | Height: | Size: 1 KiB |
BIN
assets/images/open_alias.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
assets/images/ud.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
assets/images/x_social.png
Normal file
After Width: | Height: | Size: 733 B |
|
@ -42,10 +42,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: async
|
||||
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
|
||||
sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.11.0"
|
||||
version: "2.12.0"
|
||||
blockchain_utils:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -59,10 +59,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: boolean_selector
|
||||
sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
|
||||
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
version: "2.1.2"
|
||||
build:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -140,10 +140,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: characters
|
||||
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
|
||||
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
version: "1.4.0"
|
||||
checked_yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -156,10 +156,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: clock
|
||||
sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf
|
||||
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
version: "1.1.2"
|
||||
code_builder:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -172,10 +172,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: collection
|
||||
sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
|
||||
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.19.0"
|
||||
version: "1.19.1"
|
||||
convert:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -243,10 +243,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: fake_async
|
||||
sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78"
|
||||
sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.1"
|
||||
version: "1.3.2"
|
||||
ffi:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -410,18 +410,18 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker
|
||||
sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06"
|
||||
sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.0.7"
|
||||
version: "10.0.8"
|
||||
leak_tracker_flutter_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_flutter_testing
|
||||
sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379"
|
||||
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.8"
|
||||
version: "3.0.9"
|
||||
leak_tracker_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -450,10 +450,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
|
||||
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.12.16+1"
|
||||
version: "0.12.17"
|
||||
material_color_utilities:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -466,10 +466,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
|
||||
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.15.0"
|
||||
version: "1.16.0"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -532,10 +532,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: path
|
||||
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
|
||||
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.9.0"
|
||||
version: "1.9.1"
|
||||
path_provider:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -715,26 +715,26 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: source_span
|
||||
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
|
||||
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.10.0"
|
||||
version: "1.10.1"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stack_trace
|
||||
sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377"
|
||||
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.12.0"
|
||||
version: "1.12.1"
|
||||
stream_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stream_channel
|
||||
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
|
||||
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
version: "2.1.4"
|
||||
stream_transform:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -747,26 +747,26 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: string_scanner
|
||||
sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3"
|
||||
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
version: "1.4.1"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: term_glyph
|
||||
sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
|
||||
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
version: "1.2.2"
|
||||
test_api:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c"
|
||||
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.3"
|
||||
version: "0.7.4"
|
||||
timing:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -820,10 +820,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b
|
||||
sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "14.3.0"
|
||||
version: "14.3.1"
|
||||
watcher:
|
||||
dependency: "direct overridden"
|
||||
description:
|
||||
|
@ -873,5 +873,5 @@ packages:
|
|||
source: hosted
|
||||
version: "3.1.3"
|
||||
sdks:
|
||||
dart: ">=3.6.0 <4.0.0"
|
||||
dart: ">=3.7.0-0 <4.0.0"
|
||||
flutter: ">=3.24.0"
|
||||
|
|
|
@ -30,7 +30,6 @@ class AuthService with Store {
|
|||
Routes.modify2FAPage,
|
||||
Routes.newWallet,
|
||||
Routes.newWalletType,
|
||||
Routes.addressBookAddContact,
|
||||
Routes.restoreOptions,
|
||||
];
|
||||
|
||||
|
|
80
lib/di.dart
|
@ -29,8 +29,16 @@ import 'package:cake_wallet/entities/contact_record.dart';
|
|||
import 'package:cake_wallet/entities/exchange_api_mode.dart';
|
||||
import 'package:cake_wallet/entities/hardware_wallet/require_hardware_wallet_connection.dart';
|
||||
import 'package:cake_wallet/entities/parse_address_from_domain.dart';
|
||||
import 'package:cake_wallet/entities/parsed_address.dart';
|
||||
import 'package:cake_wallet/exchange/provider/trocador_exchange_provider.dart';
|
||||
import 'package:cake_wallet/haven/cw_haven.dart';
|
||||
import 'package:cake_wallet/src/screens/address_book/edit_address_page.dart';
|
||||
import 'package:cake_wallet/src/screens/address_book/edit_contact_group_page.dart';
|
||||
import 'package:cake_wallet/src/screens/address_book/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/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/contact_list_page.dart';
|
||||
import 'package:cake_wallet/src/screens/dev/monero_background_sync.dart';
|
||||
import 'package:cake_wallet/src/screens/dev/moneroc_call_profiler.dart';
|
||||
import 'package:cake_wallet/src/screens/dev/network_requests.dart';
|
||||
|
@ -92,8 +100,6 @@ import 'package:cake_wallet/src/screens/backup/backup_page.dart';
|
|||
import 'package:cake_wallet/src/screens/backup/edit_backup_password_page.dart';
|
||||
import 'package:cake_wallet/src/screens/buy/buy_webview_page.dart';
|
||||
import 'package:cake_wallet/src/screens/buy/webview_page.dart';
|
||||
import 'package:cake_wallet/src/screens/contact/contact_list_page.dart';
|
||||
import 'package:cake_wallet/src/screens/contact/contact_page.dart';
|
||||
import 'package:cake_wallet/src/screens/dashboard/dashboard_page.dart';
|
||||
import 'package:cake_wallet/src/screens/dashboard/desktop_dashboard_page.dart';
|
||||
import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_sidebar_wrapper.dart';
|
||||
|
@ -974,18 +980,62 @@ Future<void> setup({
|
|||
getIt.registerFactoryParam<AnimatedURPage, String, void>((String urQr, _) =>
|
||||
AnimatedURPage(getIt.get<AnimatedURModel>(), urQr: urQr));
|
||||
|
||||
getIt.registerFactoryParam<ContactViewModel, ContactRecord?, void>(
|
||||
(ContactRecord? contact, _) => ContactViewModel(_contactSource, contact: contact));
|
||||
getIt.registerFactoryParam<ContactViewModel, AddressEditRequest?, void>(
|
||||
(req, _) => ContactViewModel(_contactSource,getIt<AppStore>().wallet!,getIt<SettingsStore>(), request: req,),
|
||||
);
|
||||
|
||||
getIt.registerFactoryParam<ContactListViewModel, CryptoCurrency?, void>(
|
||||
(CryptoCurrency? cur, _) =>
|
||||
ContactListViewModel(_contactSource, _walletInfoSource, cur, getIt.get<SettingsStore>()));
|
||||
(cur, _) => ContactListViewModel(
|
||||
_contactSource,
|
||||
_walletInfoSource,
|
||||
getIt<AppStore>().wallet!,
|
||||
cur,
|
||||
getIt<SettingsStore>(),
|
||||
),
|
||||
);
|
||||
|
||||
getIt.registerFactoryParam<ContactListPage, CryptoCurrency?, void>((CryptoCurrency? cur, _) =>
|
||||
ContactListPage(getIt.get<ContactListViewModel>(param1: cur), getIt.get<AuthService>()));
|
||||
getIt.registerFactoryParam<ContactListPage, CryptoCurrency?, void>(
|
||||
(cur, _) => ContactListPage(
|
||||
getIt.get<ContactListViewModel>(param1: cur),
|
||||
getIt<AuthService>(),
|
||||
),
|
||||
);
|
||||
|
||||
getIt.registerFactoryParam<ContactPage, ContactRecord?, void>(
|
||||
(ContactRecord? contact, _) => ContactPage(getIt.get<ContactViewModel>(param1: contact)));
|
||||
getIt.registerFactoryParam<ContactPage, ContactRecord, void>(
|
||||
(contact, _) => ContactPage(
|
||||
contactViewModel: getIt.get<ContactViewModel>(
|
||||
param1: AddressEditRequest.contact(contact),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
getIt.registerFactoryParam<EditAddressPage, AddressEditRequest, void>(
|
||||
(request, _) => EditAddressPage(
|
||||
contactViewModel: getIt.get<ContactViewModel>(param1: request),
|
||||
),
|
||||
);
|
||||
|
||||
getIt.registerFactory<SupportedHandlesPage>(
|
||||
() => SupportedHandlesPage(contactViewModel: getIt<ContactViewModel>()),
|
||||
);
|
||||
|
||||
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.registerFactory(() => AddressListPage(getIt.get<WalletAddressListViewModel>()));
|
||||
|
||||
|
@ -1368,10 +1418,12 @@ Future<void> setup({
|
|||
|
||||
getIt.registerFactory(() => YatService());
|
||||
|
||||
getIt.registerFactory(() => AddressResolver(
|
||||
yatService: getIt.get<YatService>(),
|
||||
wallet: getIt.get<AppStore>().wallet!,
|
||||
settingsStore: getIt.get<SettingsStore>()));
|
||||
getIt.registerLazySingleton<AddressResolverService>(
|
||||
() => AddressResolverService(
|
||||
yatService: getIt<YatService>(),
|
||||
settingsStore: getIt<SettingsStore>(),
|
||||
),
|
||||
);
|
||||
|
||||
getIt.registerFactoryParam<FullscreenQRPage, QrViewData, void>(
|
||||
(QrViewData viewData, _) => FullscreenQRPage(qrViewData: viewData));
|
||||
|
|
|
@ -82,7 +82,6 @@ class Bip353Record {
|
|||
}
|
||||
|
||||
static Future<String?> pickBip353AddressChoice(
|
||||
BuildContext context,
|
||||
String bip353Name,
|
||||
Map<String, String> addressMap,
|
||||
) async {
|
||||
|
@ -90,13 +89,12 @@ class Bip353Record {
|
|||
return addressMap.values.first;
|
||||
}
|
||||
|
||||
final chosenAddress = await _showAddressChoiceDialog(context, bip353Name, addressMap);
|
||||
final chosenAddress = await _showAddressChoiceDialog(bip353Name, addressMap);
|
||||
|
||||
return chosenAddress;
|
||||
}
|
||||
|
||||
static Future<String?> _showAddressChoiceDialog(
|
||||
BuildContext context,
|
||||
String bip353Name,
|
||||
Map<String, String> addressMap,
|
||||
) async {
|
||||
|
@ -118,24 +116,24 @@ class Bip353Record {
|
|||
|
||||
String? selectedOriginalValue;
|
||||
|
||||
if (context.mounted) {
|
||||
await showPopUp<void>(
|
||||
context: context,
|
||||
builder: (dialogContext) {
|
||||
return AlertWithPickerOption(
|
||||
alertTitle: S.of(context).multiple_addresses_detected + '\n$bip353Name',
|
||||
alertTitleTextSize: 14,
|
||||
alertSubtitle: S.of(context).please_choose_one + ':',
|
||||
options: displayItems,
|
||||
onOptionSelected: (Map<String, String> chosenItem) {
|
||||
selectedOriginalValue = chosenItem['originalValue'];
|
||||
},
|
||||
alertBarrierDismissible: true,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
return selectedOriginalValue;
|
||||
// if (context.mounted) {
|
||||
// await showPopUp<void>(
|
||||
// context: context,
|
||||
// builder: (dialogContext) {
|
||||
// return AlertWithPickerOption(
|
||||
// alertTitle: S.of(context).multiple_addresses_detected + '\n$bip353Name',
|
||||
// alertTitleTextSize: 14,
|
||||
// alertSubtitle: S.of(context).please_choose_one + ':',
|
||||
// options: displayItems,
|
||||
// onOptionSelected: (Map<String, String> chosenItem) {
|
||||
// selectedOriginalValue = chosenItem['originalValue'];
|
||||
// },
|
||||
// alertBarrierDismissible: true,
|
||||
// );
|
||||
// },
|
||||
// );
|
||||
// }
|
||||
return selectedOriginalValue = displayItems.first['originalValue'];
|
||||
}
|
||||
|
||||
static String _truncate(String value, {int front = 6, int back = 6}) {
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:cake_wallet/entities/parsed_address.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/hive_type_ids.dart';
|
||||
import 'package:cw_core/keyable.dart';
|
||||
|
@ -7,12 +10,25 @@ part 'contact.g.dart';
|
|||
|
||||
@HiveType(typeId: Contact.typeId)
|
||||
class Contact extends HiveObject with Keyable {
|
||||
Contact({required this.name, required this.address, CryptoCurrency? type, DateTime? lastChange})
|
||||
: lastChange = lastChange ?? DateTime.now() {
|
||||
if (type != null) {
|
||||
raw = type.raw;
|
||||
}
|
||||
}
|
||||
Contact({
|
||||
required this.name,
|
||||
this.address = '',
|
||||
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,
|
||||
this.handle = '',
|
||||
this.imagePath = '',
|
||||
this.profileName = '',
|
||||
this.description = '',
|
||||
DateTime? lastChange,
|
||||
}) : raw = type?.raw ?? 0,
|
||||
_parsedJson = _encode(parsedByHandle),
|
||||
_manualJson = _encode(manualAddresses),
|
||||
extraJsonBlobs = extraBlobs,
|
||||
sourceRaw = source.raw,
|
||||
lastChange = lastChange ?? DateTime.now();
|
||||
|
||||
static const typeId = CONTACT_TYPE_ID;
|
||||
static const boxName = 'Contacts';
|
||||
|
@ -24,13 +40,66 @@ class Contact extends HiveObject with Keyable {
|
|||
String address;
|
||||
|
||||
@HiveField(2, defaultValue: 0)
|
||||
late int raw;
|
||||
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;
|
||||
|
||||
|
@ -40,5 +109,64 @@ class Contact extends HiveObject with Keyable {
|
|||
@override
|
||||
int get hashCode => key.hashCode;
|
||||
|
||||
void updateCryptoCurrency({required CryptoCurrency currency}) => raw = currency.raw;
|
||||
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}) {
|
||||
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(
|
||||
name: p.profileName.isNotEmpty ? p.profileName : p.handle,
|
||||
profileName: p.profileName,
|
||||
handle: p.handle,
|
||||
description: p.description,
|
||||
source: p.addressSource,
|
||||
imagePath: localImage ?? '',
|
||||
manualAddresses: manual,
|
||||
parsedByHandle: parsed,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:cake_wallet/entities/contact.dart';
|
||||
import 'package:cake_wallet/entities/contact_base.dart';
|
||||
import 'package:cake_wallet/entities/parsed_address.dart';
|
||||
import 'package:cake_wallet/entities/record.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
|
||||
|
@ -10,39 +14,143 @@ part 'contact_record.g.dart';
|
|||
class ContactRecord = ContactRecordBase with _$ContactRecord;
|
||||
|
||||
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,
|
||||
address = original.address,
|
||||
type = original.type,
|
||||
lastChange = original.lastChange,
|
||||
super(source, original);
|
||||
handle = original.handle,
|
||||
profileName = original.profileName,
|
||||
description = original.description,
|
||||
imagePath = original.imagePath,
|
||||
sourceType = original.source,
|
||||
manual = ObservableMap.of(original.manualByCurrency),
|
||||
parsedBlocks = ObservableMap.of({
|
||||
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);
|
||||
|
||||
@override
|
||||
@observable
|
||||
String name;
|
||||
|
||||
@override
|
||||
String name, handle, profileName, description, imagePath;
|
||||
@observable
|
||||
String address;
|
||||
AddressSource sourceType;
|
||||
|
||||
@override
|
||||
@observable
|
||||
CryptoCurrency type;
|
||||
ObservableMap<CryptoCurrency, Map<String, String>> manual;
|
||||
|
||||
DateTime? lastChange;
|
||||
@observable
|
||||
ObservableMap<String, Map<CryptoCurrency, Map<String, String>>> parsedBlocks;
|
||||
|
||||
@computed
|
||||
Map<CryptoCurrency, Map<String, String>> get parsedByCurrency {
|
||||
final out = <CryptoCurrency, Map<String, String>>{};
|
||||
parsedBlocks.forEach((_, byCur) {
|
||||
byCur.forEach((cur, lbl) => out.putIfAbsent(cur, () => {})..addAll(lbl));
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
@computed
|
||||
File? get avatarFile => imagePath.isEmpty ? null : File(imagePath);
|
||||
|
||||
@computed
|
||||
ImageProvider get avatarProvider => (avatarFile?.existsSync() ?? false)
|
||||
? FileImage(avatarFile!)
|
||||
: const AssetImage('assets/images/profile.png');
|
||||
|
||||
@override
|
||||
void toBind(Contact original) {
|
||||
reaction((_) => name, (String name) => original.name = name);
|
||||
reaction((_) => address, (String address) => original.address = address);
|
||||
reaction((_) => type,
|
||||
(CryptoCurrency currency) => original.updateCryptoCurrency(currency: currency));
|
||||
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 original) {
|
||||
name = original.name;
|
||||
address = original.address;
|
||||
type = original.type;
|
||||
void fromBind(Contact c) {
|
||||
name = c.name;
|
||||
handle = c.handle;
|
||||
profileName = c.profileName;
|
||||
description = c.description;
|
||||
imagePath = c.imagePath;
|
||||
sourceType = c.source;
|
||||
}
|
||||
|
||||
@action
|
||||
void setManualAddress(CryptoCurrency cur, String label, String addr) {
|
||||
manual.putIfAbsent(cur, () => {})[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;
|
||||
}
|
||||
|
|
|
@ -432,7 +432,11 @@ Future<void> ios_migrate_address_book(Box<Contact> contactSource) async {
|
|||
final address = _item["address"] as String;
|
||||
final name = _item["name"] as String;
|
||||
|
||||
return Contact(address: address, name: name, type: CryptoCurrency.fromString(type));
|
||||
return Contact(parsedByHandle: {'handle':
|
||||
{
|
||||
5: {'label': address} //TODO fix this hardcoded value
|
||||
}
|
||||
}, manualAddresses: {}, name: name);
|
||||
});
|
||||
|
||||
await contactSource.addAll(contacts);
|
||||
|
|
|
@ -1,33 +1,29 @@
|
|||
import 'package:cake_wallet/core/address_validator.dart';
|
||||
import 'package:cake_wallet/core/yat_service.dart';
|
||||
import 'package:cake_wallet/entities/emoji_string_extension.dart';
|
||||
import 'package:cake_wallet/entities/ens_record.dart';
|
||||
import 'package:cake_wallet/entities/fio_address_provider.dart';
|
||||
import 'package:cake_wallet/entities/openalias_record.dart';
|
||||
import 'package:cake_wallet/entities/parsed_address.dart';
|
||||
import 'package:cake_wallet/entities/unstoppable_domain_address.dart';
|
||||
import 'package:cake_wallet/entities/emoji_string_extension.dart';
|
||||
import 'package:cake_wallet/entities/wellknown_record.dart';
|
||||
import 'package:cake_wallet/entities/zano_alias.dart';
|
||||
import 'package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart';
|
||||
import 'package:cake_wallet/mastodon/mastodon_api.dart';
|
||||
import 'package:cake_wallet/nostr/nostr_api.dart';
|
||||
import 'package:cake_wallet/store/settings_store.dart';
|
||||
import 'package:cake_wallet/twitter/twitter_api.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:cw_core/wallet_base.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:cake_wallet/entities/fio_address_provider.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
import 'bip_353_record.dart';
|
||||
|
||||
class AddressResolver {
|
||||
AddressResolver({required this.yatService, required this.wallet, required this.settingsStore})
|
||||
: walletType = wallet.type;
|
||||
class AddressResolverService {
|
||||
AddressResolverService({required this.yatService, required this.settingsStore}) {
|
||||
_buildLookupTable();
|
||||
}
|
||||
|
||||
final YatService yatService;
|
||||
final WalletType walletType;
|
||||
final WalletBase wallet;
|
||||
final SettingsStore settingsStore;
|
||||
|
||||
static const unstoppableDomains = [
|
||||
|
@ -165,30 +161,146 @@ class AddressResolver {
|
|||
"zone"
|
||||
];
|
||||
|
||||
static String? extractAddressByType(
|
||||
{required String raw,
|
||||
required CryptoCurrency type,
|
||||
bool requireSurroundingWhitespaces = true}) {
|
||||
var addressPattern = AddressValidator.getAddressFromStringPattern(type);
|
||||
late final List<LookupEntry> _lookupTable;
|
||||
|
||||
if (addressPattern == null) {
|
||||
throw Exception('Unexpected token: $type for getAddressFromStringPattern');
|
||||
void _buildLookupTable() {
|
||||
_lookupTable = [
|
||||
LookupEntry(
|
||||
source: AddressSource.twitter,
|
||||
currencies: AddressSource.twitter.supportedCurrencies,
|
||||
applies: (q) => settingsStore.lookupsTwitter && q.startsWith('@'),
|
||||
// x handle example: @username
|
||||
run: _lookupTwitter,
|
||||
),
|
||||
LookupEntry(
|
||||
source: AddressSource.zanoAlias,
|
||||
currencies: AddressSource.zanoAlias.supportedCurrencies,
|
||||
applies: (q) => settingsStore.lookupsZanoAlias && q.startsWith('@'),
|
||||
// zano handle example: @username
|
||||
run: _lookupZano,
|
||||
),
|
||||
LookupEntry(
|
||||
source: AddressSource.mastodon,
|
||||
currencies: AddressSource.mastodon.supportedCurrencies,
|
||||
applies: (q) =>
|
||||
settingsStore.lookupsMastodon &&
|
||||
q.startsWith('@') &&
|
||||
q.contains('@', 1) &&
|
||||
q.contains('.', 1),
|
||||
// Mastodon handle example: @username@hostname.xxx
|
||||
run: _lookupMastodon,
|
||||
),
|
||||
LookupEntry(
|
||||
source: AddressSource.wellKnown,
|
||||
currencies: AddressSource.wellKnown.supportedCurrencies,
|
||||
applies: (q) => settingsStore.lookupsWellKnown && q.contains('.') && q.contains('@'),
|
||||
// .well-known handle example:
|
||||
run: _lookupWellKnown,
|
||||
),
|
||||
LookupEntry(
|
||||
source: AddressSource.fio,
|
||||
currencies: AddressSource.fio.supportedCurrencies,
|
||||
applies: (q) => settingsStore.lookupsFio && !q.startsWith('@') && q.contains('@') && !q.contains('.'),
|
||||
// FIO handle example: username@domain
|
||||
run: _lookupFio,
|
||||
),
|
||||
LookupEntry(
|
||||
source: AddressSource.yatRecord,
|
||||
currencies: AddressSource.yatRecord.supportedCurrencies,
|
||||
applies: (q) => settingsStore.lookupsYatService && q.hasOnlyEmojis,
|
||||
// Yat handle example: 🐶🐾
|
||||
run: _lookupYatService,
|
||||
),
|
||||
LookupEntry(
|
||||
source: AddressSource.thorChain,
|
||||
currencies: AddressSource.thorChain.supportedCurrencies,
|
||||
applies: (q) => settingsStore.lookupsThorChain && q.isNotEmpty,
|
||||
run: _lookupThorChain,
|
||||
),
|
||||
LookupEntry(
|
||||
source: AddressSource.unstoppableDomains,
|
||||
currencies: AddressSource.unstoppableDomains.supportedCurrencies,
|
||||
applies: (q) {
|
||||
if (!settingsStore.lookupsUnstoppableDomains) return false;
|
||||
// Unstoppable Domains handle example: name.crypto
|
||||
final formattedName = OpenaliasRecord.formatDomainName(q);
|
||||
final domainParts = formattedName.split('.');
|
||||
final name = domainParts.last;
|
||||
return domainParts.length > 1 &&
|
||||
domainParts.first.isNotEmpty &&
|
||||
name.isNotEmpty &&
|
||||
unstoppableDomains.any((domain) => name.trim() == domain);
|
||||
},
|
||||
run: _lookupsUnstoppableDomains,
|
||||
),
|
||||
LookupEntry(
|
||||
source: AddressSource.bip353,
|
||||
currencies: AddressSource.bip353.supportedCurrencies,
|
||||
applies: (q) => settingsStore.lookupsBip353,
|
||||
run: _lookupsBip353,
|
||||
),
|
||||
LookupEntry(
|
||||
source: AddressSource.ens,
|
||||
currencies: AddressSource.ens.supportedCurrencies,
|
||||
applies: (q) => settingsStore.lookupsENS && q.endsWith('.eth'),
|
||||
// ENS handle example: name.eth
|
||||
run: _lookupEns,
|
||||
),
|
||||
LookupEntry(
|
||||
source: AddressSource.openAlias,
|
||||
currencies: AddressSource.openAlias.supportedCurrencies,
|
||||
applies: (q) {
|
||||
if (!settingsStore.lookupsOpenAlias) return false;
|
||||
// OpenAlias handle example:
|
||||
final formattedName = OpenaliasRecord.formatDomainName(q);
|
||||
return formattedName.contains(".");
|
||||
},
|
||||
run: _lookupsOpenAlias,
|
||||
),
|
||||
LookupEntry(
|
||||
source: AddressSource.nostr,
|
||||
currencies: [CryptoCurrency.btc],
|
||||
applies: (q) => settingsStore.lookupsNostr && isEmailFormat(q),
|
||||
// Nostr handle example: name@domain
|
||||
run: _lookupsNostr,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
|
||||
static String _cleanInput(String raw) => raw
|
||||
.replaceAll(RegExp(r'[\u2028\u2029]'), '\n')
|
||||
.replaceAll(RegExp(r'<[^>]+>'), ' ');
|
||||
|
||||
static String? extractAddressByType({
|
||||
required String raw,
|
||||
required CryptoCurrency type,
|
||||
}) {
|
||||
final pat = AddressValidator.getAddressFromStringPattern(type);
|
||||
if (pat == null) {
|
||||
throw StateError('Unknown pattern for $type');
|
||||
}
|
||||
|
||||
if (requireSurroundingWhitespaces)
|
||||
addressPattern = "$BEFORE_REGEX$addressPattern$AFTER_REGEX";
|
||||
final text = _cleanInput(raw);
|
||||
|
||||
final match = RegExp(addressPattern, multiLine: true).firstMatch(raw);
|
||||
return match?.group(0)?.replaceAllMapped(RegExp('[^0-9a-zA-Z]|bitcoincash:|nano_|ban_'),
|
||||
(Match match) {
|
||||
String group = match.group(0)!;
|
||||
if (group.startsWith('bitcoincash:') ||
|
||||
group.startsWith('nano_') ||
|
||||
group.startsWith('ban_')) {
|
||||
return group;
|
||||
}
|
||||
return '';
|
||||
final regex = RegExp(r'(?:^|[^0-9A-Za-z])(' + pat + r')',
|
||||
multiLine: true, caseSensitive: false);
|
||||
|
||||
final m = regex.firstMatch(text);
|
||||
if (m == null) return null;
|
||||
|
||||
// 3. Strip BCH / NANO prefixes and punctuation just like before
|
||||
final cleaned = m
|
||||
.group(1)!
|
||||
.replaceAllMapped(RegExp(r'[^0-9a-zA-Z]|bitcoincash:|nano_|ban_'), (m) {
|
||||
final g = m.group(0)!;
|
||||
return (g.startsWith('bitcoincash:') || g.startsWith('nano_') || g.startsWith('ban_'))
|
||||
? g
|
||||
: '';
|
||||
});
|
||||
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
bool isEmailFormat(String address) {
|
||||
|
@ -199,197 +311,363 @@ class AddressResolver {
|
|||
return emailRegex.hasMatch(address);
|
||||
}
|
||||
|
||||
Future<ParsedAddress> resolve(BuildContext context, String text, CryptoCurrency currency) async {
|
||||
final ticker = currency.title;
|
||||
Future<List<ParsedAddress>> resolve({
|
||||
required String query,
|
||||
required WalletBase wallet,
|
||||
CryptoCurrency? currency,
|
||||
}) async {
|
||||
try {
|
||||
// twitter handle example: @username
|
||||
if (text.startsWith('@') && !text.substring(1).contains('@')) {
|
||||
if (currency == CryptoCurrency.zano && settingsStore.lookupsZanoAlias) {
|
||||
final formattedName = text.substring(1);
|
||||
final zanoAddress = await ZanoAlias.fetchZanoAliasAddress(formattedName);
|
||||
if (zanoAddress != null && zanoAddress.isNotEmpty) {
|
||||
return ParsedAddress.zanoAddress(
|
||||
address: zanoAddress,
|
||||
name: text,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (settingsStore.lookupsTwitter) {
|
||||
final formattedName = text.substring(1);
|
||||
final twitterUser = await TwitterApi.lookupUserByName(userName: formattedName);
|
||||
final addressFromBio = extractAddressByType(
|
||||
raw: twitterUser.description,
|
||||
type: CryptoCurrency.fromString(ticker, walletCurrency: wallet.currency));
|
||||
if (addressFromBio != null && addressFromBio.isNotEmpty) {
|
||||
return ParsedAddress.fetchTwitterAddress(
|
||||
address: addressFromBio,
|
||||
name: text,
|
||||
profileImageUrl: twitterUser.profileImageUrl,
|
||||
profileName: twitterUser.name);
|
||||
}
|
||||
final tasks = <Future<ParsedAddress?>>[];
|
||||
|
||||
final pinnedTweet = twitterUser.pinnedTweet?.text;
|
||||
if (pinnedTweet != null) {
|
||||
final addressFromPinnedTweet = extractAddressByType(
|
||||
raw: pinnedTweet,
|
||||
type: CryptoCurrency.fromString(ticker, walletCurrency: wallet.currency));
|
||||
if (addressFromPinnedTweet != null) {
|
||||
return ParsedAddress.fetchTwitterAddress(
|
||||
address: addressFromPinnedTweet,
|
||||
name: text,
|
||||
profileImageUrl: twitterUser.profileImageUrl,
|
||||
profileName: twitterUser.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (final entry in _lookupTable) {
|
||||
if (!supportedSources.contains(entry.source)) continue;
|
||||
if (!entry.applies(query)) continue;
|
||||
|
||||
final coins = currency == null
|
||||
? entry.currencies.toList()
|
||||
: (entry.currencies.contains(currency) ? [currency] : const <CryptoCurrency>[]);
|
||||
|
||||
if (coins.isEmpty) continue;
|
||||
tasks.add(entry.run(query, coins, wallet));
|
||||
}
|
||||
|
||||
// Mastodon example: @username@hostname.xxx
|
||||
if (text.startsWith('@') && text.contains('@', 1) && text.contains('.', 1)) {
|
||||
if (settingsStore.lookupsMastodon) {
|
||||
final subText = text.substring(1);
|
||||
final hostNameIndex = subText.indexOf('@');
|
||||
final hostName = subText.substring(hostNameIndex + 1);
|
||||
final userName = subText.substring(0, hostNameIndex);
|
||||
final results = await Future.wait(tasks);
|
||||
|
||||
final mastodonUser =
|
||||
await MastodonAPI.lookupUserByUserName(userName: userName, apiHost: hostName);
|
||||
|
||||
if (mastodonUser != null) {
|
||||
String? addressFromBio = extractAddressByType(raw: mastodonUser.note, type: currency);
|
||||
|
||||
if (addressFromBio != null && addressFromBio.isNotEmpty) {
|
||||
return ParsedAddress.fetchMastodonAddress(
|
||||
address: addressFromBio,
|
||||
name: text,
|
||||
profileImageUrl: mastodonUser.profileImageUrl,
|
||||
profileName: mastodonUser.username);
|
||||
} else {
|
||||
final pinnedPosts =
|
||||
await MastodonAPI.getPinnedPosts(userId: mastodonUser.id, apiHost: hostName);
|
||||
|
||||
if (pinnedPosts.isNotEmpty) {
|
||||
final userPinnedPostsText = pinnedPosts.map((item) => item.content).join('\n');
|
||||
String? addressFromPinnedPost =
|
||||
extractAddressByType(raw: userPinnedPostsText, type: currency);
|
||||
|
||||
if (addressFromPinnedPost != null && addressFromPinnedPost.isNotEmpty) {
|
||||
return ParsedAddress.fetchMastodonAddress(
|
||||
address: addressFromPinnedPost,
|
||||
name: text,
|
||||
profileImageUrl: mastodonUser.profileImageUrl,
|
||||
profileName: mastodonUser.username);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// .well-known scheme:
|
||||
if (text.contains('.') && text.contains('@')) {
|
||||
if (settingsStore.lookupsWellKnown) {
|
||||
final record =
|
||||
await WellKnownRecord.fetchAddressAndName(formattedName: text, currency: currency);
|
||||
if (record != null) {
|
||||
return ParsedAddress.fetchWellKnownAddress(address: record.address, name: text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!text.startsWith('@') && text.contains('@') && !text.contains('.')) {
|
||||
final bool isFioRegistered = await FioAddressProvider.checkAvail(text);
|
||||
if (isFioRegistered) {
|
||||
final address = await FioAddressProvider.getPubAddress(text, ticker);
|
||||
return ParsedAddress.fetchFioAddress(address: address, name: text);
|
||||
}
|
||||
}
|
||||
if (text.hasOnlyEmojis) {
|
||||
if (settingsStore.lookupsYatService) {
|
||||
if (walletType != WalletType.haven) {
|
||||
final addresses = await yatService.fetchYatAddress(text, ticker);
|
||||
return ParsedAddress.fetchEmojiAddress(addresses: addresses, name: text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final thorChainAddress = await ThorChainExchangeProvider.lookupAddressByName(text);
|
||||
if (thorChainAddress != null && thorChainAddress.isNotEmpty) {
|
||||
String? address =
|
||||
thorChainAddress[ticker] ?? (ticker == 'RUNE' ? thorChainAddress['THOR'] : null);
|
||||
if (address != null) {
|
||||
return ParsedAddress.thorChainAddress(address: address, name: text);
|
||||
}
|
||||
}
|
||||
|
||||
final formattedName = OpenaliasRecord.formatDomainName(text);
|
||||
final domainParts = formattedName.split('.');
|
||||
final name = domainParts.last;
|
||||
|
||||
if (domainParts.length <= 1 || domainParts.first.isEmpty || name.isEmpty) {
|
||||
return ParsedAddress(addresses: [text]);
|
||||
}
|
||||
|
||||
if (unstoppableDomains.any((domain) => name.trim() == domain)) {
|
||||
if (settingsStore.lookupsUnstoppableDomains) {
|
||||
final address = await fetchUnstoppableDomainAddress(text, ticker);
|
||||
if (address.isNotEmpty) {
|
||||
return ParsedAddress.fetchUnstoppableDomainAddress(address: address, name: text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final bip353AddressMap = await Bip353Record.fetchUriByCryptoCurrency(text, ticker);
|
||||
|
||||
if (bip353AddressMap != null && bip353AddressMap.isNotEmpty) {
|
||||
final chosenAddress = await Bip353Record.pickBip353AddressChoice(context, text, bip353AddressMap);
|
||||
if (chosenAddress != null) {
|
||||
return ParsedAddress.fetchBip353AddressAddress(address: chosenAddress, name: text);
|
||||
}
|
||||
}
|
||||
|
||||
if (text.endsWith(".eth")) {
|
||||
if (settingsStore.lookupsENS) {
|
||||
final address = await EnsRecord.fetchEnsAddress(text, wallet: wallet);
|
||||
if (address.isNotEmpty && address != "0x0000000000000000000000000000000000000000") {
|
||||
return ParsedAddress.fetchEnsAddress(name: text, address: address);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (formattedName.contains(".")) {
|
||||
if (settingsStore.lookupsOpenAlias) {
|
||||
final txtRecord = await OpenaliasRecord.lookupOpenAliasRecord(formattedName);
|
||||
|
||||
if (txtRecord != null) {
|
||||
final record = await OpenaliasRecord.fetchAddressAndName(
|
||||
formattedName: formattedName, ticker: ticker.toLowerCase(), txtRecord: txtRecord);
|
||||
return ParsedAddress.fetchOpenAliasAddress(record: record, name: text);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isEmailFormat(text)) {
|
||||
final nostrProfile = await NostrProfileHandler.queryProfile(context, text);
|
||||
if (nostrProfile?.relays != null) {
|
||||
final nostrUserData =
|
||||
await NostrProfileHandler.processRelays(context, nostrProfile!, text);
|
||||
|
||||
if (nostrUserData != null) {
|
||||
String? addressFromBio = extractAddressByType(raw: nostrUserData.about, type: currency);
|
||||
if (addressFromBio != null && addressFromBio.isNotEmpty) {
|
||||
return ParsedAddress.nostrAddress(
|
||||
address: addressFromBio,
|
||||
name: text,
|
||||
profileImageUrl: nostrUserData.picture,
|
||||
profileName: nostrUserData.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return results.whereType<ParsedAddress>().toList();
|
||||
} catch (e) {
|
||||
printV(e.toString());
|
||||
printV('Error resolving address: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
Future<ParsedAddress?> _lookupTwitter(
|
||||
String text, List<CryptoCurrency> currencies, WalletBase wallet) async {
|
||||
final formattedName = text.substring(1);
|
||||
final twitterUser = await TwitterApi.lookupUserByName(userName: formattedName);
|
||||
|
||||
if (twitterUser == null) return null;
|
||||
|
||||
final Map<CryptoCurrency, String> result = {};
|
||||
|
||||
for (final cur in currencies) {
|
||||
final addressFromBio = extractAddressByType(
|
||||
raw: twitterUser.description, type: CryptoCurrency.fromString(cur.title));
|
||||
|
||||
if (addressFromBio != null && addressFromBio.isNotEmpty) {
|
||||
result[cur] = addressFromBio;
|
||||
}
|
||||
}
|
||||
|
||||
return ParsedAddress(addresses: [text]);
|
||||
final pinnedTweet = twitterUser.pinnedTweet?.text;
|
||||
if (pinnedTweet != null) {
|
||||
for (final cur in currencies) {
|
||||
final addressFromPinnedTweet =
|
||||
extractAddressByType(raw: pinnedTweet, type: CryptoCurrency.fromString(cur.title));
|
||||
if (addressFromPinnedTweet != null && addressFromPinnedTweet.isNotEmpty) {
|
||||
result[cur] = addressFromPinnedTweet;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (result.isNotEmpty) {
|
||||
return ParsedAddress(
|
||||
parsedAddressByCurrencyMap: result,
|
||||
addressSource: AddressSource.twitter,
|
||||
handle: text,
|
||||
profileImageUrl: twitterUser.profileImageUrl,
|
||||
profileName: twitterUser.name,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<ParsedAddress?> _lookupZano(
|
||||
String text, List<CryptoCurrency> currencies, WalletBase _) async {
|
||||
final formattedName = text.substring(1);
|
||||
|
||||
final zanoAddress = await ZanoAlias.fetchZanoAliasAddress(formattedName);
|
||||
if (zanoAddress != null && zanoAddress.isNotEmpty) {
|
||||
return ParsedAddress(
|
||||
parsedAddressByCurrencyMap: {CryptoCurrency.zano: zanoAddress},
|
||||
addressSource: AddressSource.zanoAlias,
|
||||
handle: text,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<ParsedAddress?> _lookupMastodon(
|
||||
String text, List<CryptoCurrency> currencies, WalletBase _) async {
|
||||
final subText = text.substring(1);
|
||||
final hostNameIndex = subText.indexOf('@');
|
||||
final hostName = subText.substring(hostNameIndex + 1);
|
||||
final userName = subText.substring(0, hostNameIndex);
|
||||
|
||||
final Map<CryptoCurrency, String> result = {};
|
||||
|
||||
final mastodonUser =
|
||||
await MastodonAPI.lookupUserByUserName(userName: userName, apiHost: hostName);
|
||||
|
||||
if (mastodonUser != null) {
|
||||
for (final cur in currencies) {
|
||||
String? addressFromBio = extractAddressByType(raw: mastodonUser.note, type: cur);
|
||||
if (addressFromBio != null && addressFromBio.isNotEmpty) {
|
||||
result[cur] = addressFromBio;
|
||||
}
|
||||
}
|
||||
|
||||
final pinnedPosts =
|
||||
await MastodonAPI.getPinnedPosts(userId: mastodonUser.id, apiHost: hostName);
|
||||
|
||||
if (pinnedPosts.isNotEmpty) {
|
||||
final userPinnedPostsText = pinnedPosts.map((item) => item.content).join('\n');
|
||||
|
||||
for (final cur in currencies) {
|
||||
String? addressFromPinnedPost = extractAddressByType(raw: userPinnedPostsText, type: cur);
|
||||
if (addressFromPinnedPost != null && addressFromPinnedPost.isNotEmpty) {
|
||||
result[cur] = addressFromPinnedPost;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (result.isNotEmpty) {
|
||||
return ParsedAddress(
|
||||
parsedAddressByCurrencyMap: result,
|
||||
addressSource: AddressSource.mastodon,
|
||||
handle: text,
|
||||
profileImageUrl: mastodonUser.profileImageUrl,
|
||||
profileName: mastodonUser.username,
|
||||
);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<ParsedAddress?> _lookupWellKnown(
|
||||
String text, List<CryptoCurrency> currencies, WalletBase _) async {
|
||||
final Map<CryptoCurrency, String> result = {};
|
||||
|
||||
for (final cur in currencies) {
|
||||
final record = await WellKnownRecord.fetchAddressAndName(formattedName: text, currency: cur);
|
||||
if (record != null) {
|
||||
result[cur] = record.address;
|
||||
}
|
||||
}
|
||||
|
||||
if (result.isNotEmpty) {
|
||||
return ParsedAddress(
|
||||
parsedAddressByCurrencyMap: result,
|
||||
addressSource: AddressSource.wellKnown,
|
||||
handle: text,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<ParsedAddress?> _lookupFio(
|
||||
String text, List<CryptoCurrency> currencies, WalletBase _) async {
|
||||
final Map<CryptoCurrency, String> result = {};
|
||||
final bool isFioRegistered = await FioAddressProvider.checkAvail(text);
|
||||
if (!isFioRegistered) return null;
|
||||
|
||||
for (final cur in currencies) {
|
||||
final address = await FioAddressProvider.getPubAddress(text, cur.title);
|
||||
if (address.isNotEmpty) {
|
||||
result[cur] = address;
|
||||
}
|
||||
}
|
||||
|
||||
if (result.isNotEmpty) {
|
||||
return ParsedAddress(
|
||||
parsedAddressByCurrencyMap: result,
|
||||
addressSource: AddressSource.fio,
|
||||
handle: text,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<ParsedAddress?> _lookupYatService(
|
||||
String text, List<CryptoCurrency> currency, WalletBase _) async {
|
||||
final Map<CryptoCurrency, String> result = {};
|
||||
|
||||
for (final cur in currency) {
|
||||
final addresses = await yatService.fetchYatAddress(text, cur.title);
|
||||
if (addresses.isNotEmpty) {
|
||||
result[cur] = addresses.first.address; //TODO: Handle multiple addresses
|
||||
}
|
||||
if (result.isNotEmpty) {
|
||||
return ParsedAddress(
|
||||
parsedAddressByCurrencyMap: result,
|
||||
addressSource: AddressSource.yatRecord,
|
||||
handle: text,
|
||||
);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<ParsedAddress?> _lookupThorChain(
|
||||
String text, List<CryptoCurrency> currency, WalletBase _) async {
|
||||
final Map<CryptoCurrency, String> result = {};
|
||||
|
||||
final thorChainAddress = await ThorChainExchangeProvider.lookupAddressByName(text);
|
||||
if (thorChainAddress != null && thorChainAddress.isNotEmpty) {
|
||||
for (final cur in currency) {
|
||||
String? address =
|
||||
thorChainAddress[cur.title] ?? (cur.title == 'RUNE' ? thorChainAddress['THOR'] : null);
|
||||
if (address != null && address.isNotEmpty) {
|
||||
result[cur] = address;
|
||||
}
|
||||
}
|
||||
|
||||
if (result.isNotEmpty) {
|
||||
return ParsedAddress(
|
||||
parsedAddressByCurrencyMap: result,
|
||||
addressSource: AddressSource.thorChain,
|
||||
handle: text,
|
||||
);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<ParsedAddress?> _lookupsUnstoppableDomains(
|
||||
String text, List<CryptoCurrency> currency, WalletBase _) async {
|
||||
final Map<CryptoCurrency, String> result = {};
|
||||
for (final cur in currency) {
|
||||
final address = await fetchUnstoppableDomainAddress(text, cur.title);
|
||||
if (address.isNotEmpty) {
|
||||
result[cur] = address;
|
||||
}
|
||||
}
|
||||
|
||||
if (result.isNotEmpty) {
|
||||
return ParsedAddress(
|
||||
parsedAddressByCurrencyMap: result,
|
||||
profileImageUrl: 'assets/images/profile.png',
|
||||
profileName: text,
|
||||
addressSource: AddressSource.unstoppableDomains,
|
||||
handle: text,
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<ParsedAddress?> _lookupsBip353(
|
||||
String text, List<CryptoCurrency> currency, WalletBase _) async {
|
||||
final Map<CryptoCurrency, String> result = {};
|
||||
|
||||
for (final cur in currency) {
|
||||
final bip353AddressMap = await Bip353Record.fetchUriByCryptoCurrency(text, cur.title);
|
||||
if (bip353AddressMap != null && bip353AddressMap.isNotEmpty) {
|
||||
final address = bip353AddressMap['address'];
|
||||
if (address != null && address.isNotEmpty) {
|
||||
result[cur] = address;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (result.isNotEmpty) {
|
||||
return ParsedAddress(
|
||||
parsedAddressByCurrencyMap: result,
|
||||
addressSource: AddressSource.bip353,
|
||||
handle: text,
|
||||
);
|
||||
}
|
||||
|
||||
//
|
||||
// if (bip353AddressMap != null && bip353AddressMap.isNotEmpty) {
|
||||
// final chosenAddress =
|
||||
// await Bip353Record.pickBip353AddressChoice(text, bip353AddressMap); //TODO fix context
|
||||
// if (chosenAddress != null) {
|
||||
// return ParsedAddress.fetchBip353AddressAddress(address: chosenAddress, name: text);
|
||||
// }
|
||||
// }
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<ParsedAddress?> _lookupEns(
|
||||
String text, List<CryptoCurrency> currency, WalletBase wallet) async {
|
||||
final Map<CryptoCurrency, String> result = {};
|
||||
|
||||
for (final cur in currency) {
|
||||
final address = await EnsRecord.fetchEnsAddress(text, wallet: wallet);
|
||||
if (address.isNotEmpty && address != "0x0000000000000000000000000000000000000000") {
|
||||
result[cur] = address;
|
||||
}
|
||||
}
|
||||
|
||||
if (result.isNotEmpty) {
|
||||
return ParsedAddress(
|
||||
parsedAddressByCurrencyMap: result,
|
||||
addressSource: AddressSource.ens,
|
||||
handle: text,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<ParsedAddress?> _lookupsOpenAlias(
|
||||
String text, List<CryptoCurrency> currency, WalletBase _) async {
|
||||
final formattedName = OpenaliasRecord.formatDomainName(text);
|
||||
final txtRecord = await OpenaliasRecord.lookupOpenAliasRecord(formattedName);
|
||||
final Map<CryptoCurrency, String> result = {};
|
||||
|
||||
for (final cur in currency) {
|
||||
if (txtRecord == null) continue;
|
||||
final record = await OpenaliasRecord.fetchAddressAndName(
|
||||
formattedName: formattedName, ticker: cur.title.toLowerCase(), txtRecord: txtRecord);
|
||||
if (record.address.isNotEmpty) {
|
||||
result[cur] = record.address;
|
||||
}
|
||||
}
|
||||
|
||||
if (result.isNotEmpty) {
|
||||
return ParsedAddress(
|
||||
parsedAddressByCurrencyMap: result,
|
||||
addressSource: AddressSource.openAlias,
|
||||
handle: text,
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<ParsedAddress?> _lookupsNostr(
|
||||
String text, List<CryptoCurrency> currency, WalletBase _) async {
|
||||
//TODO implement Nostr lookup logic
|
||||
// final nostrProfile = await NostrProfileHandler.queryProfile(context, text);
|
||||
// if (nostrProfile?.relays != null) {
|
||||
// final nostrUserData =
|
||||
// await NostrProfileHandler.processRelays(context, nostrProfile!, text);
|
||||
//
|
||||
// if (nostrUserData != null) {
|
||||
// String? addressFromBio = extractAddressByType(raw: nostrUserData.about, type: currency);
|
||||
// if (addressFromBio != null && addressFromBio.isNotEmpty) {
|
||||
// return ParsedAddress.nostrAddress(
|
||||
// address: addressFromBio,
|
||||
// name: text,
|
||||
// profileImageUrl: nostrUserData.picture,
|
||||
// profileName: nostrUserData.name);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class LookupEntry {
|
||||
const LookupEntry({
|
||||
required this.source,
|
||||
required this.currencies,
|
||||
required this.applies,
|
||||
required this.run,
|
||||
});
|
||||
|
||||
final AddressSource source;
|
||||
final List<CryptoCurrency> currencies;
|
||||
final bool Function(String query) applies;
|
||||
final Future<ParsedAddress?> Function(
|
||||
String query, List<CryptoCurrency> currencies, WalletBase wallet) run;
|
||||
}
|
||||
|
|
|
@ -1,181 +1,140 @@
|
|||
import 'package:cake_wallet/entities/openalias_record.dart';
|
||||
import 'package:cake_wallet/entities/yat_record.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
|
||||
enum ParseFrom {
|
||||
unstoppableDomains,
|
||||
openAlias,
|
||||
yatRecord,
|
||||
fio,
|
||||
notParsed,
|
||||
twitter,
|
||||
ens,
|
||||
contact,
|
||||
mastodon,
|
||||
nostr,
|
||||
thorChain,
|
||||
wellKnown,
|
||||
zanoAlias,
|
||||
bip353
|
||||
const supportedSources = [
|
||||
AddressSource.twitter,
|
||||
AddressSource.unstoppableDomains,
|
||||
AddressSource.ens,
|
||||
AddressSource.mastodon,
|
||||
];
|
||||
|
||||
enum AddressSource {
|
||||
twitter(
|
||||
label: 'X',
|
||||
iconPath: 'assets/images/x_social.png',
|
||||
alias: '@username',
|
||||
supportedCurrencies: [CryptoCurrency.xmr, CryptoCurrency.btc]),
|
||||
unstoppableDomains(
|
||||
label: 'Unstoppable Domains',
|
||||
iconPath: 'assets/images/ud.png',
|
||||
alias: 'domain.tld',
|
||||
supportedCurrencies: [CryptoCurrency.xmr, CryptoCurrency.btc]),
|
||||
openAlias(
|
||||
label: 'OpenAlias',
|
||||
iconPath: 'assets/images/open_alias.png',
|
||||
alias: 'oa',
|
||||
supportedCurrencies: [CryptoCurrency.xmr, CryptoCurrency.btc]),
|
||||
yatRecord(
|
||||
label: 'Yat',
|
||||
iconPath: 'assets/images/yat_mini_logo.png',
|
||||
alias: '🎂🎂🎂',
|
||||
supportedCurrencies: [CryptoCurrency.xmr, CryptoCurrency.btc]),
|
||||
fio(
|
||||
label: 'FIO',
|
||||
iconPath: 'assets/images/fio.png',
|
||||
supportedCurrencies: [CryptoCurrency.xmr, CryptoCurrency.btc]),
|
||||
ens(
|
||||
label: 'Ethereum Name Service',
|
||||
iconPath: 'assets/images/ens_icon.png',
|
||||
supportedCurrencies: [CryptoCurrency.xmr, CryptoCurrency.btc, CryptoCurrency.eth]),
|
||||
mastodon(
|
||||
label: 'Mastodon',
|
||||
iconPath: 'assets/images/mastodon.svg',
|
||||
alias: 'user@domain.tld',
|
||||
supportedCurrencies: [CryptoCurrency.xmr, CryptoCurrency.btc]),
|
||||
nostr(
|
||||
label: 'Nostr',
|
||||
iconPath: 'assets/images/nostr.png',
|
||||
supportedCurrencies: [CryptoCurrency.xmr, CryptoCurrency.btc]),
|
||||
thorChain(
|
||||
label: 'ThorChain',
|
||||
iconPath: 'assets/images/thorchain.png',
|
||||
supportedCurrencies: [CryptoCurrency.xmr, CryptoCurrency.btc]),
|
||||
wellKnown(
|
||||
label: '.well-known',
|
||||
iconPath: 'assets/icons/wk.svg',
|
||||
supportedCurrencies: [CryptoCurrency.xmr, CryptoCurrency.btc]),
|
||||
zanoAlias(
|
||||
label: 'Zano Alias',
|
||||
iconPath: 'assets/images/zano_icon.png',
|
||||
supportedCurrencies: [CryptoCurrency.xmr, CryptoCurrency.btc]),
|
||||
bip353(
|
||||
label: 'BIP-353',
|
||||
iconPath: '',
|
||||
supportedCurrencies: [CryptoCurrency.xmr, CryptoCurrency.btc]),
|
||||
contact(label: 'Contact', iconPath: '', supportedCurrencies: []),
|
||||
notParsed(label: 'Unknown', iconPath: '', supportedCurrencies: []);
|
||||
|
||||
const AddressSource({
|
||||
required this.label,
|
||||
required this.iconPath,
|
||||
this.alias = '',
|
||||
this.supportedCurrencies = const <CryptoCurrency>[],
|
||||
});
|
||||
|
||||
final String label;
|
||||
final String iconPath;
|
||||
final String alias;
|
||||
final List<CryptoCurrency> supportedCurrencies;
|
||||
}
|
||||
|
||||
extension AddressSourceIndex on AddressSource {
|
||||
int get raw => index;
|
||||
|
||||
static AddressSource fromRaw(int raw) =>
|
||||
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 {
|
||||
ParsedAddress({
|
||||
required this.addresses,
|
||||
this.name = '',
|
||||
this.description = '',
|
||||
const ParsedAddress({
|
||||
required this.parsedAddressByCurrencyMap,
|
||||
this.manualAddressByCurrencyMap,
|
||||
this.addressSource = AddressSource.notParsed,
|
||||
this.handle = '',
|
||||
this.profileImageUrl = '',
|
||||
this.profileName = '',
|
||||
this.parseFrom = ParseFrom.notParsed,
|
||||
this.description = '',
|
||||
});
|
||||
|
||||
factory ParsedAddress.fetchEmojiAddress({
|
||||
List<YatRecord>? addresses,
|
||||
required String name,
|
||||
}) {
|
||||
if (addresses?.isEmpty ?? true) {
|
||||
return ParsedAddress(addresses: [name], parseFrom: ParseFrom.yatRecord);
|
||||
}
|
||||
return ParsedAddress(
|
||||
addresses: addresses!.map((e) => e.address).toList(),
|
||||
name: name,
|
||||
parseFrom: ParseFrom.yatRecord,
|
||||
);
|
||||
}
|
||||
|
||||
factory ParsedAddress.fetchUnstoppableDomainAddress({
|
||||
String? address,
|
||||
required String name,
|
||||
}) {
|
||||
if (address?.isEmpty ?? true) {
|
||||
return ParsedAddress(addresses: [name]);
|
||||
}
|
||||
return ParsedAddress(
|
||||
addresses: [address!],
|
||||
name: name,
|
||||
parseFrom: ParseFrom.unstoppableDomains,
|
||||
);
|
||||
}
|
||||
|
||||
factory ParsedAddress.fetchBip353AddressAddress ({
|
||||
required String address,
|
||||
required String name,
|
||||
}) {
|
||||
return ParsedAddress(
|
||||
addresses: [address],
|
||||
name: name,
|
||||
parseFrom: ParseFrom.bip353,
|
||||
);
|
||||
}
|
||||
|
||||
factory ParsedAddress.fetchOpenAliasAddress(
|
||||
{required OpenaliasRecord record, required String name}) {
|
||||
if (record.address.isEmpty) {
|
||||
return ParsedAddress(addresses: [name]);
|
||||
}
|
||||
return ParsedAddress(
|
||||
addresses: [record.address],
|
||||
name: record.name,
|
||||
description: record.description,
|
||||
parseFrom: ParseFrom.openAlias,
|
||||
);
|
||||
}
|
||||
|
||||
factory ParsedAddress.fetchFioAddress({required String address, required String name}) {
|
||||
return ParsedAddress(
|
||||
addresses: [address],
|
||||
name: name,
|
||||
parseFrom: ParseFrom.fio,
|
||||
);
|
||||
}
|
||||
|
||||
factory ParsedAddress.fetchTwitterAddress(
|
||||
{required String address,
|
||||
required String name,
|
||||
required String profileImageUrl,
|
||||
required String profileName,
|
||||
String? description}) {
|
||||
return ParsedAddress(
|
||||
addresses: [address],
|
||||
name: name,
|
||||
description: description ?? '',
|
||||
profileImageUrl: profileImageUrl,
|
||||
profileName: profileName,
|
||||
parseFrom: ParseFrom.twitter,
|
||||
);
|
||||
}
|
||||
|
||||
factory ParsedAddress.fetchMastodonAddress(
|
||||
{required String address,
|
||||
required String name,
|
||||
required String profileImageUrl,
|
||||
required String profileName}) {
|
||||
return ParsedAddress(
|
||||
addresses: [address],
|
||||
name: name,
|
||||
parseFrom: ParseFrom.mastodon,
|
||||
profileImageUrl: profileImageUrl,
|
||||
profileName: profileName,
|
||||
);
|
||||
}
|
||||
|
||||
factory ParsedAddress.fetchContactAddress({required String address, required String name}) {
|
||||
return ParsedAddress(
|
||||
addresses: [address],
|
||||
name: name,
|
||||
parseFrom: ParseFrom.contact,
|
||||
);
|
||||
}
|
||||
|
||||
factory ParsedAddress.fetchEnsAddress({required String address, required String name}) {
|
||||
return ParsedAddress(
|
||||
addresses: [address],
|
||||
name: name,
|
||||
parseFrom: ParseFrom.ens,
|
||||
);
|
||||
}
|
||||
|
||||
factory ParsedAddress.nostrAddress(
|
||||
{required String address,
|
||||
required String name,
|
||||
required String profileImageUrl,
|
||||
required String profileName}) {
|
||||
return ParsedAddress(
|
||||
addresses: [address],
|
||||
name: name,
|
||||
parseFrom: ParseFrom.nostr,
|
||||
profileImageUrl: profileImageUrl,
|
||||
profileName: profileName,
|
||||
);
|
||||
}
|
||||
|
||||
factory ParsedAddress.thorChainAddress({required String address, required String name}) {
|
||||
return ParsedAddress(
|
||||
addresses: [address],
|
||||
name: name,
|
||||
parseFrom: ParseFrom.thorChain,
|
||||
);
|
||||
}
|
||||
|
||||
factory ParsedAddress.zanoAddress({required String address, required String name}) {
|
||||
return ParsedAddress(
|
||||
addresses: [address],
|
||||
name: name,
|
||||
parseFrom: ParseFrom.zanoAlias,
|
||||
);
|
||||
}
|
||||
|
||||
factory ParsedAddress.fetchWellKnownAddress({required String address, required String name}) {
|
||||
return ParsedAddress(
|
||||
addresses: [address],
|
||||
name: name,
|
||||
parseFrom: ParseFrom.wellKnown,
|
||||
);
|
||||
}
|
||||
|
||||
final List<String> addresses;
|
||||
final String name;
|
||||
final String description;
|
||||
final Map<CryptoCurrency, String> parsedAddressByCurrencyMap;
|
||||
final Map<CryptoCurrency, String>? manualAddressByCurrencyMap;
|
||||
final AddressSource addressSource;
|
||||
final String handle;
|
||||
final String profileImageUrl;
|
||||
final String profileName;
|
||||
final ParseFrom parseFrom;
|
||||
final String description;
|
||||
|
||||
ParsedAddress copyWith({
|
||||
Map<CryptoCurrency, String>? parsedAddressByCurrencyMap,
|
||||
Map<CryptoCurrency, String>? manualAddressByCurrencyMap,
|
||||
AddressSource? addressSource,
|
||||
String? handle,
|
||||
String? profileImageUrl,
|
||||
String? profileName,
|
||||
String? description,
|
||||
}) {
|
||||
return ParsedAddress(
|
||||
parsedAddressByCurrencyMap: parsedAddressByCurrencyMap ?? this.parsedAddressByCurrencyMap,
|
||||
manualAddressByCurrencyMap: manualAddressByCurrencyMap ?? this.manualAddressByCurrencyMap,
|
||||
addressSource: addressSource ?? this.addressSource,
|
||||
handle: handle ?? this.handle,
|
||||
profileImageUrl: profileImageUrl ?? this.profileImageUrl,
|
||||
profileName: profileName ?? this.profileName,
|
||||
description: description ?? this.description,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -85,6 +85,10 @@ class PreferencesKey {
|
|||
static const lookupsOpenAlias = 'looks_up_open_alias';
|
||||
static const lookupsENS = 'looks_up_ens';
|
||||
static const lookupsWellKnown = 'looks_up_well_known';
|
||||
static const lookupsFio = 'looks_up_fio';
|
||||
static const lookupsNostr = 'looks_up_nostr';
|
||||
static const lookupsThorChain = 'looks_up_thor_chain';
|
||||
static const lookupsBip353 = 'looks_up_bip353';
|
||||
static const usePayjoin = 'use_payjoin';
|
||||
static const showPayjoinCard = 'show_payjoin_card';
|
||||
static const showCameraConsent = 'show_camera_consent';
|
||||
|
|
|
@ -26,8 +26,7 @@ import 'package:cake_wallet/src/screens/cake_pay/cake_pay.dart';
|
|||
import 'package:cake_wallet/src/screens/connect_device/connect_device_page.dart';
|
||||
import 'package:cake_wallet/src/screens/connect_device/monero_hardware_wallet_options_page.dart';
|
||||
import 'package:cake_wallet/src/screens/connect_device/select_hardware_wallet_account_page.dart';
|
||||
import 'package:cake_wallet/src/screens/contact/contact_list_page.dart';
|
||||
import 'package:cake_wallet/src/screens/contact/contact_page.dart';
|
||||
import 'package:cake_wallet/src/screens/address_book/contact_list_page.dart';
|
||||
import 'package:cake_wallet/src/screens/dashboard/dashboard_page.dart';
|
||||
import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_dashboard_actions.dart';
|
||||
import 'package:cake_wallet/src/screens/dashboard/edit_token_page.dart';
|
||||
|
@ -147,6 +146,7 @@ import 'package:cw_core/wallet_type.dart';
|
|||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'entities/parsed_address.dart';
|
||||
import 'src/screens/buy/buy_sell_page.dart';
|
||||
import 'src/screens/dashboard/pages/nft_import_page.dart';
|
||||
|
||||
|
@ -580,11 +580,6 @@ Route<dynamic> createRoute(RouteSettings settings) {
|
|||
case Routes.pickerWalletAddress:
|
||||
return MaterialPageRoute<void>(builder: (_) => getIt.get<AddressListPage>());
|
||||
|
||||
case Routes.addressBookAddContact:
|
||||
return handleRouteWithPlatformAwareness(
|
||||
(context) => getIt.get<ContactPage>(param1: settings.arguments as ContactRecord?),
|
||||
);
|
||||
|
||||
case Routes.showKeys:
|
||||
return handleRouteWithPlatformAwareness(
|
||||
(context) => getIt.get<WalletKeysPage>(),
|
||||
|
|
|
@ -34,7 +34,13 @@ class Routes {
|
|||
static const addressBook = '/address_book';
|
||||
static const pickerAddressBook = '/picker_address_book';
|
||||
static const pickerWalletAddress = '/picker_wallet_address';
|
||||
static const addressBookAddContact = '/address_book_add_contact';
|
||||
static const newContactWelcomePage = '/new_contact_welcome_page';
|
||||
static const supportedHandlesPage = '/supported_handles_page';
|
||||
static const editNewContactGroupPage = '/edit_new_contact_group_page';
|
||||
static const editContactGroupPage = '/edit_contact_group_page';
|
||||
static const editAddressesPage = '/edit_addresses_page';
|
||||
static const editAddressPage = '/edit_address_page';
|
||||
static const contactPage = '/contact_page';
|
||||
static const showKeys = '/show_keys';
|
||||
static const exchangeConfirm = '/exchange_confirm';
|
||||
static const tradeHistory = '/trade_history';
|
||||
|
|
|
@ -1,18 +1,27 @@
|
|||
import 'package:cake_wallet/core/auth_service.dart';
|
||||
import 'package:cake_wallet/di.dart';
|
||||
import 'package:cake_wallet/entities/contact_base.dart';
|
||||
import 'package:cake_wallet/entities/contact_record.dart';
|
||||
import 'package:cake_wallet/entities/parse_address_from_domain.dart';
|
||||
import 'package:cake_wallet/entities/wallet_list_order_types.dart';
|
||||
import 'package:cake_wallet/generated/i18n.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/rounded_icon_button.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/wallet_list/filtered_list.dart';
|
||||
import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
|
||||
import 'package:cake_wallet/src/widgets/bottom_sheet/add_contact_bottom_sheet_widget.dart';
|
||||
import 'package:cake_wallet/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart';
|
||||
import 'package:cake_wallet/src/widgets/standard_list.dart';
|
||||
import 'package:cake_wallet/themes/core/material_base_theme.dart';
|
||||
import 'package:cake_wallet/utils/address_formatter.dart';
|
||||
import 'package:cake_wallet/utils/show_bar.dart';
|
||||
import 'package:cake_wallet/utils/show_pop_up.dart';
|
||||
import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
@ -56,16 +65,8 @@ class ContactListPage extends BasePage {
|
|||
// FIX-ME: Style
|
||||
//shape: CircleBorder(),
|
||||
onPressed: () async {
|
||||
if (contactListViewModel.shouldRequireTOTP2FAForAddingContacts) {
|
||||
authService.authenticateAction(
|
||||
context,
|
||||
route: Routes.addressBookAddContact,
|
||||
conditionToDetermineIfToUse2FA:
|
||||
contactListViewModel.shouldRequireTOTP2FAForAddingContacts,
|
||||
);
|
||||
} else {
|
||||
await Navigator.of(context).pushNamed(Routes.addressBookAddContact);
|
||||
}
|
||||
await _showAddressBookBottomSheet(
|
||||
context: context, contactListViewModel: contactListViewModel);
|
||||
},
|
||||
child: Offstage(),
|
||||
),
|
||||
|
@ -316,6 +317,7 @@ class _ContactListBodyState extends State<ContactListBody> {
|
|||
return Scaffold(
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
body: Container(
|
||||
padding: const EdgeInsets.fromLTRB(18, 8, 18, 0),
|
||||
child: FilteredList(
|
||||
list: contacts,
|
||||
updateFunction: widget.contactListViewModel.reorderAccordingToContactList,
|
||||
|
@ -323,31 +325,29 @@ class _ContactListBodyState extends State<ContactListBody> {
|
|||
shrinkWrap: true,
|
||||
itemBuilder: (context, index) {
|
||||
final contact = contacts[index];
|
||||
final contactContent =
|
||||
generateContactRaw(context, contact, contacts.length == index + 1);
|
||||
return GestureDetector(
|
||||
key: Key('${contact.name}'),
|
||||
onTap: () async {
|
||||
if (!widget.contactListViewModel.isEditable) {
|
||||
Navigator.of(context).pop(contact);
|
||||
return;
|
||||
}
|
||||
|
||||
final isCopied = await DialogService.showNameAndAddressDialog(context, contact);
|
||||
|
||||
if (isCopied) {
|
||||
await Clipboard.setData(ClipboardData(text: contact.address));
|
||||
await showBar<void>(context, S.of(context).copied_to_clipboard);
|
||||
}
|
||||
return ContactAddressesExpansionTile(
|
||||
key: Key(contact.key.toString()),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 8),
|
||||
manualByCurrency: contact.manual,
|
||||
fillColor: Theme.of(context).colorScheme.surfaceContainer,
|
||||
title: _buildContactTitle(
|
||||
context: context,
|
||||
contact: contact,
|
||||
contactListViewModel: widget.contactListViewModel),
|
||||
onEditPressed: (cur, lbl) async {
|
||||
await _showAddressBookBottomSheet(
|
||||
context: context,
|
||||
contactListViewModel: widget.contactListViewModel,
|
||||
initialRoute: Routes.editAddressPage,
|
||||
initialArgs: AddressEditRequest.address(
|
||||
contact: contact,
|
||||
currency: cur,
|
||||
label: lbl,
|
||||
kindIsManual: true,
|
||||
),
|
||||
);
|
||||
},
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: widget.contactListViewModel.isEditable
|
||||
? Slidable(
|
||||
key: Key('${contact.key}'),
|
||||
endActionPane: _actionPane(context, contact),
|
||||
child: contactContent,
|
||||
)
|
||||
: contactContent,
|
||||
onCopyPressed: (addr) => Clipboard.setData(ClipboardData(text: addr)),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
@ -358,71 +358,60 @@ class _ContactListBodyState extends State<ContactListBody> {
|
|||
);
|
||||
}
|
||||
|
||||
Widget generateContactRaw(BuildContext context, ContactRecord contact, bool isLast) {
|
||||
final image = contact.type.iconPath;
|
||||
final currencyIcon = image != null
|
||||
? Image.asset(image, height: 24, width: 24)
|
||||
: const SizedBox(height: 24, width: 24);
|
||||
return Column(
|
||||
Widget _buildContactTitle(
|
||||
{required BuildContext context,
|
||||
required ContactRecord contact,
|
||||
required ContactListViewModel contactListViewModel}) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Container(
|
||||
key: Key('${contact.name}'),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
),
|
||||
margin: const EdgeInsets.only(top: 4, bottom: 4, left: 16, right: 16),
|
||||
padding: const EdgeInsets.only(top: 16, bottom: 16, right: 16, left: 16),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
currencyIcon,
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(left: 12),
|
||||
child: Text(
|
||||
contact.name,
|
||||
style: Theme.of(context).textTheme.bodyMedium!,
|
||||
),
|
||||
))
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
child: Image(
|
||||
image: contact.avatarProvider,
|
||||
width: 24,
|
||||
height: 24,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 6),
|
||||
Text(
|
||||
contact.name,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
RoundedIconButton(
|
||||
icon: Icons.add,
|
||||
onPressed: () async => await _showAddressBookBottomSheet(
|
||||
context: context,
|
||||
contactListViewModel: contactListViewModel,
|
||||
initialRoute: Routes.editAddressPage,
|
||||
initialArgs: AddressEditRequest.address(
|
||||
contact: contact,
|
||||
currency: walletTypeToCryptoCurrency(widget.contactListViewModel.wallet.type),
|
||||
label: null,
|
||||
kindIsManual: true,
|
||||
))),
|
||||
const SizedBox(width: 8),
|
||||
RoundedIconButton(
|
||||
icon: Icons.edit,
|
||||
onPressed: () async => await _showAddressBookBottomSheet(
|
||||
context: context,
|
||||
contactListViewModel: contactListViewModel,
|
||||
initialRoute: Routes.contactPage,
|
||||
initialArgs: contact)),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
ActionPane _actionPane(BuildContext context, ContactRecord contact) {
|
||||
return ActionPane(
|
||||
motion: const ScrollMotion(),
|
||||
extentRatio: 0.4,
|
||||
children: [
|
||||
SlidableAction(
|
||||
onPressed: (_) async => await Navigator.of(context)
|
||||
.pushNamed(Routes.addressBookAddContact, arguments: contact),
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
foregroundColor: Theme.of(context).colorScheme.onSurface,
|
||||
icon: Icons.edit,
|
||||
label: S.of(context).edit,
|
||||
),
|
||||
SlidableAction(
|
||||
onPressed: (_) async {
|
||||
final isDelete = await DialogService.showAlertDialog(context);
|
||||
|
||||
if (isDelete) {
|
||||
await widget.contactListViewModel.delete(contact);
|
||||
}
|
||||
},
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
foregroundColor: Theme.of(context).colorScheme.onSurface,
|
||||
icon: CupertinoIcons.delete,
|
||||
label: S.of(context).delete,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget filterButtonWidget(BuildContext context, ContactListViewModel contactListViewModel) {
|
||||
final filterIcon = Image.asset(
|
||||
'assets/images/filter_icon.png',
|
||||
|
@ -472,6 +461,28 @@ class _ContactListBodyState extends State<ContactListBody> {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> _showAddressBookBottomSheet(
|
||||
{required BuildContext context,
|
||||
required ContactListViewModel contactListViewModel,
|
||||
String? initialRoute,
|
||||
Object? initialArgs}) async {
|
||||
await showModalBottomSheet<void>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (BuildContext bottomSheetContext) {
|
||||
return AddressBookBottomSheet(
|
||||
onHandlerSearch: (query) async {
|
||||
final address = await getIt
|
||||
.get<AddressResolverService>()
|
||||
.resolve(query: query, wallet: contactListViewModel.wallet);
|
||||
return address;
|
||||
},
|
||||
initialRoute: initialRoute,
|
||||
initialArgs: initialArgs,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
class DialogService {
|
||||
static Future<bool> showAlertDialog(BuildContext context) async {
|
||||
return await showPopUp<bool>(
|
||||
|
@ -500,9 +511,9 @@ class DialogService {
|
|||
textAlign: TextAlign.center,
|
||||
walletType: cryptoCurrencyToWalletType(contact.type),
|
||||
evenTextStyle: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontSize: 16,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
fontSize: 16,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
rightButtonText: S.of(context).copy,
|
||||
leftButtonText: S.of(context).cancel,
|
176
lib/src/screens/address_book/contact_page.dart
Normal file
|
@ -0,0 +1,176 @@
|
|||
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/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/rounded_icon_button.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:cw_core/crypto_currency.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
|
||||
class ContactPage extends BasePage {
|
||||
ContactPage({required this.contactViewModel});
|
||||
|
||||
final ContactViewModel contactViewModel;
|
||||
|
||||
@override
|
||||
Widget? leading(BuildContext context) => null;
|
||||
|
||||
@override
|
||||
Widget middle(BuildContext context) {
|
||||
return Observer(
|
||||
builder: (_) => Center(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
child: Image(
|
||||
width: 24,
|
||||
height: 24,
|
||||
image: contactViewModel.avatar,
|
||||
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) {
|
||||
final onHandlerSearch = (query) async {
|
||||
final address = await getIt
|
||||
.get<AddressResolverService>()
|
||||
.resolve(query: query as String, wallet: contactViewModel.wallet);
|
||||
return address;
|
||||
};
|
||||
return SizedBox(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
RoundedIconButton(icon: Icons.add, onPressed: () {
|
||||
Navigator.pushNamed(
|
||||
context,
|
||||
Routes.newContactWelcomePage,
|
||||
arguments: [onHandlerSearch, true, contactViewModel.record],
|
||||
);
|
||||
}),
|
||||
const SizedBox(width: 8),
|
||||
RoundedIconButton(icon: Icons.edit, onPressed: () {
|
||||
Navigator.pushNamed(
|
||||
context,
|
||||
Routes.editContactGroupPage,
|
||||
arguments: contactViewModel,
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget body(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final fillColor = currentTheme.isDark
|
||||
? CustomThemeColors.backgroundGradientColorDark.withAlpha(100)
|
||||
: CustomThemeColors.backgroundGradientColorLight;
|
||||
|
||||
return SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
ContactAddressesExpansionTile(
|
||||
key: ValueKey(contactViewModel.name),
|
||||
title: Text('Manual Addresses'),
|
||||
fillColor: fillColor,
|
||||
manualByCurrency: contactViewModel.manual,
|
||||
onCopyPressed: (address) async =>
|
||||
await Clipboard.setData(ClipboardData(text: address)),
|
||||
onEditPressed: (cur, lbl) {
|
||||
Navigator.pushNamed(
|
||||
context,
|
||||
Routes.editAddressPage,
|
||||
arguments: AddressEditRequest.address(
|
||||
contact: contactViewModel.record,
|
||||
currency: cur,
|
||||
label: lbl,
|
||||
kindIsManual: true,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
...contactViewModel.parsedBlocks.entries.map((entry) {
|
||||
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(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
ImageUtil.getImageFromPath(
|
||||
imagePath: src.iconPath,
|
||||
height: 24,
|
||||
width: 24,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Text(
|
||||
handle,
|
||||
style: theme.textTheme.labelSmall?.copyWith(
|
||||
fontSize: 12,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
fillColor: fillColor,
|
||||
manualByCurrency: byCurrency,
|
||||
onCopyPressed: (addr) => Clipboard.setData(ClipboardData(text: addr)),
|
||||
onEditPressed: (cur, lbl) {
|
||||
Navigator.pushNamed(
|
||||
context,
|
||||
Routes.editAddressPage,
|
||||
arguments: AddressEditRequest.address(
|
||||
contact: contactViewModel.record,
|
||||
currency: cur,
|
||||
label: lbl,
|
||||
kindIsManual: false,
|
||||
handle: handle,
|
||||
handleKey: entry.key,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
480
lib/src/screens/address_book/contact_welcome_page.dart
Normal file
|
@ -0,0 +1,480 @@
|
|||
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/generated/i18n.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/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/utils/image_utill.dart';
|
||||
import 'package:cake_wallet/utils/show_pop_up.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/currency.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
enum _InputMode { handle, address }
|
||||
|
||||
class NewContactWelcomePage extends BasePage {
|
||||
NewContactWelcomePage({required this.onSearch, this.handleOnly = false, this.existingContact});
|
||||
|
||||
final Future<List<ParsedAddress>> Function(String query) onSearch;
|
||||
final bool handleOnly;
|
||||
final ContactRecord? existingContact;
|
||||
|
||||
@override
|
||||
Widget body(BuildContext context) => NewContactWelcomePageBody(
|
||||
currentTheme: currentTheme,
|
||||
onSearch: onSearch,
|
||||
handleOnly: handleOnly,
|
||||
existingContact: existingContact);
|
||||
}
|
||||
|
||||
class NewContactWelcomePageBody extends StatefulWidget {
|
||||
const NewContactWelcomePageBody(
|
||||
{required this.currentTheme,
|
||||
required this.onSearch,
|
||||
required this.handleOnly,
|
||||
required this.existingContact});
|
||||
|
||||
final MaterialThemeBase currentTheme;
|
||||
final Future<List<ParsedAddress>> Function(String query) onSearch;
|
||||
final bool handleOnly;
|
||||
final ContactRecord? existingContact;
|
||||
|
||||
@override
|
||||
State<NewContactWelcomePageBody> createState() => _NewContactWelcomePageBodyState();
|
||||
}
|
||||
|
||||
class _NewContactWelcomePageBodyState extends State<NewContactWelcomePageBody> {
|
||||
_InputMode _mode = _InputMode.handle;
|
||||
|
||||
CryptoCurrency _selectedCurrency = CryptoCurrency.btc;
|
||||
|
||||
final _handleCtl = TextEditingController();
|
||||
final _addressCtl = TextEditingController();
|
||||
|
||||
final _controller = TextEditingController();
|
||||
final _focusNode = FocusNode();
|
||||
final _debouncer = Duration(milliseconds: 700);
|
||||
Timer? _debounceTimer;
|
||||
List<ParsedAddress> _results = [];
|
||||
bool _isSearching = false;
|
||||
ParsedAddress? _selected;
|
||||
String _typedAddress = '';
|
||||
|
||||
final String contentImage = 'assets/images/add_contact_coins_img.png';
|
||||
final String contentText =
|
||||
'Contacts allows you to create a profile with multiple addresses, as well as detect them automatically from social media profiles. Start by entering a social handle or an address manually';
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_debounceTimer?.cancel();
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _handleChanged(String query) {
|
||||
_debounceTimer?.cancel();
|
||||
_debounceTimer = Timer(_debouncer, () async {
|
||||
if (query.trim().isEmpty) {
|
||||
setState(() => _results = []);
|
||||
return;
|
||||
}
|
||||
setState(() => _isSearching = true);
|
||||
try {
|
||||
final res = await widget.onSearch(query);
|
||||
if (mounted) setState(() => _results = res);
|
||||
} finally {
|
||||
if (mounted) setState(() => _isSearching = false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _pickCurrency() {
|
||||
_focusNode.unfocus();
|
||||
showPopUp<void>(
|
||||
context: context,
|
||||
builder: (_) => CurrencyPicker(
|
||||
selectedAtIndex: CryptoCurrency.all.indexOf(_selectedCurrency),
|
||||
items: CryptoCurrency.all,
|
||||
title: S.of(context).please_select,
|
||||
hintText: S.of(context).search_currency,
|
||||
onItemSelected: (Currency c) => setState(() {
|
||||
_selectedCurrency = c as CryptoCurrency;
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _currencyPrefix(BuildContext ctx) {
|
||||
final txtStyle = Theme.of(ctx).textTheme.bodyMedium!.copyWith(fontWeight: FontWeight.w600);
|
||||
|
||||
return InkWell(
|
||||
splashFactory: NoSplash.splashFactory,
|
||||
onTap: _pickCurrency,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
border: Border.all(
|
||||
color: Theme.of(ctx).colorScheme.outline,
|
||||
width: 1,
|
||||
)),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ImageUtil.getImageFromPath(
|
||||
imagePath: _selectedCurrency.iconPath ?? '',
|
||||
height: 24,
|
||||
width: 24,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(_selectedCurrency.name.toUpperCase(), style: txtStyle),
|
||||
const Icon(Icons.keyboard_arrow_down_sharp, size: 24),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _segmentedSwitcher(BuildContext ctx) {
|
||||
if (widget.handleOnly) return const SizedBox.shrink();
|
||||
final txt = Theme.of(ctx).textTheme.bodyMedium;
|
||||
final seg = (_InputMode m, String label) => ButtonSegment<_InputMode>(
|
||||
value: m,
|
||||
label: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
opacity: _mode == m ? 1 : 0,
|
||||
child: const Icon(Icons.check_circle, size: 16),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Text(label, style: txt),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
child: SegmentedButton<_InputMode>(
|
||||
segments: [seg(_InputMode.handle, 'handle'), seg(_InputMode.address, 'address')],
|
||||
selected: <_InputMode>{_mode},
|
||||
showSelectedIcon: false,
|
||||
style: ButtonStyle(
|
||||
shape: WidgetStatePropertyAll(
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))),
|
||||
minimumSize: const WidgetStatePropertyAll(Size.fromHeight(44)),
|
||||
padding: const WidgetStatePropertyAll(EdgeInsets.symmetric(vertical: 12)),
|
||||
foregroundColor: const WidgetStatePropertyAll(Colors.white),
|
||||
backgroundColor: const WidgetStatePropertyAll(Colors.transparent),
|
||||
overlayColor: const WidgetStatePropertyAll(Colors.transparent),
|
||||
splashFactory: NoSplash.splashFactory,
|
||||
),
|
||||
onSelectionChanged: (s) {
|
||||
final next = s.first;
|
||||
setState(() {
|
||||
_mode = next;
|
||||
|
||||
if (next == _InputMode.address) {
|
||||
_selected = null;
|
||||
_results.clear();
|
||||
_isSearching = false;
|
||||
_debounceTimer?.cancel();
|
||||
} else {
|
||||
_handleChanged(_handleCtl.text);
|
||||
}
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInputField() {
|
||||
final fillColor = widget.currentTheme.isDark
|
||||
? CustomThemeColors.backgroundGradientColorDark
|
||||
: CustomThemeColors.backgroundGradientColorLight;
|
||||
|
||||
final isHandleMode = widget.handleOnly ? true : _mode == _InputMode.handle;
|
||||
final hasDropdown = _results.isNotEmpty || _isSearching;
|
||||
|
||||
final radius = BorderRadius.vertical(
|
||||
top: const Radius.circular(12),
|
||||
bottom: hasDropdown ? Radius.zero : const Radius.circular(12),
|
||||
);
|
||||
|
||||
final outline = OutlineInputBorder(
|
||||
borderRadius: radius,
|
||||
borderSide: BorderSide.none,
|
||||
);
|
||||
|
||||
return StandardTextFormFieldWidget(
|
||||
focusNode: _focusNode,
|
||||
controller: isHandleMode ? _handleCtl : _addressCtl,
|
||||
labelText: isHandleMode ? 'Enter handle' : 'Enter address',
|
||||
fillColor: fillColor,
|
||||
onChanged: isHandleMode ? _handleChanged : (v) => setState(() => _typedAddress = v.trim()),
|
||||
prefixIcon: isHandleMode
|
||||
? SizedBox(height: 50)
|
||||
: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: _currencyPrefix(context),
|
||||
),
|
||||
prefixIconConstraints: isHandleMode
|
||||
? const BoxConstraints(
|
||||
minWidth: 12,
|
||||
maxWidth: 12,
|
||||
minHeight: 50,
|
||||
maxHeight: 50,
|
||||
)
|
||||
: null,
|
||||
suffixIconConstraints: const BoxConstraints(
|
||||
minWidth: 32,
|
||||
maxWidth: 40,
|
||||
minHeight: 30,
|
||||
maxHeight: 30,
|
||||
),
|
||||
suffixIcon: RoundedIconButton(
|
||||
icon: Icons.paste_outlined,
|
||||
iconSize: 20,
|
||||
width: 38,
|
||||
height: 36,
|
||||
onPressed: () async {
|
||||
final data = await Clipboard.getData(Clipboard.kTextPlain);
|
||||
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),
|
||||
outline: outline,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bottomInset = MediaQuery.of(context).viewInsets.bottom;
|
||||
final fillColor = widget.currentTheme.isDark
|
||||
? CustomThemeColors.backgroundGradientColorDark
|
||||
: CustomThemeColors.backgroundGradientColorLight;
|
||||
|
||||
final hasDropdown = _results.isNotEmpty || _isSearching;
|
||||
|
||||
return LayoutBuilder(builder: (context, constraints) {
|
||||
return SingleChildScrollView(
|
||||
reverse: true,
|
||||
padding: EdgeInsets.only(bottom: bottomInset),
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(minHeight: constraints.maxHeight),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
child: ImageUtil.getImageFromPath(
|
||||
imagePath: contentImage,
|
||||
fit: BoxFit.contain,
|
||||
height: 120,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: Text(
|
||||
contentText,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyLarge!,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8, top: 16),
|
||||
child: SizedBox(width: double.infinity, child: _segmentedSwitcher(context)),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildInputField(),
|
||||
if (hasDropdown)
|
||||
Container(
|
||||
constraints: const BoxConstraints(maxHeight: 85),
|
||||
decoration: BoxDecoration(
|
||||
color: fillColor,
|
||||
borderRadius: const BorderRadius.only(
|
||||
bottomLeft: Radius.circular(12),
|
||||
bottomRight: Radius.circular(12),
|
||||
)),
|
||||
child: _isSearching
|
||||
? const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(24),
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
),
|
||||
)
|
||||
: ParsedAddressListWidget(
|
||||
items: _results,
|
||||
fillColor: fillColor,
|
||||
selected: _selected,
|
||||
onItemSelected: (selected) {
|
||||
setState(() {
|
||||
_selected = (_selected == selected) ? null : selected;
|
||||
});
|
||||
_focusNode.unfocus();
|
||||
},
|
||||
),
|
||||
),
|
||||
widget.handleOnly || _mode == _InputMode.handle
|
||||
? InkWell(
|
||||
splashFactory: NoSplash.splashFactory,
|
||||
onTap: () {
|
||||
_focusNode.unfocus();
|
||||
Navigator.of(context).pushNamed(Routes.supportedHandlesPage);
|
||||
},
|
||||
child: SizedBox(
|
||||
height: 36,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
||||
child: Row(
|
||||
children: [
|
||||
Text('View supported handles',
|
||||
style: Theme.of(context).textTheme.bodyMedium),
|
||||
const SizedBox(width: 4),
|
||||
Icon(Icons.arrow_forward_ios,
|
||||
size: 14,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox(height: 36),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 24, top: 8),
|
||||
child: LoadingPrimaryButton(
|
||||
text: S.of(context).seed_language_next,
|
||||
onPressed: () {
|
||||
_focusNode.unfocus();
|
||||
if (_mode == _InputMode.handle) {
|
||||
Navigator.pushNamed(
|
||||
context,
|
||||
Routes.editNewContactGroupPage,
|
||||
arguments: [_selected!, widget.existingContact],
|
||||
);
|
||||
_selected = null;
|
||||
} else {
|
||||
final parsed = ParsedAddress(
|
||||
parsedAddressByCurrencyMap: {},
|
||||
manualAddressByCurrencyMap: {
|
||||
_selectedCurrency: _typedAddress.trim(),
|
||||
},
|
||||
addressSource: AddressSource.contact,
|
||||
handle: '',
|
||||
profileName: '',
|
||||
profileImageUrl: 'assets/images/profile.png',
|
||||
description: '',
|
||||
);
|
||||
|
||||
Navigator.pushNamed(
|
||||
context,
|
||||
Routes.editNewContactGroupPage,
|
||||
arguments: [parsed, null],
|
||||
);
|
||||
}
|
||||
},
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
width: 150,
|
||||
height: 40,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
isLoading: false,
|
||||
isDisabled: widget.handleOnly
|
||||
? _selected == null || _isSearching
|
||||
: (_mode == _InputMode.handle
|
||||
? _selected == null || _isSearching
|
||||
: _typedAddress.isEmpty),
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class ParsedAddressListWidget extends StatelessWidget {
|
||||
const ParsedAddressListWidget(
|
||||
{super.key,
|
||||
required this.items,
|
||||
required this.fillColor,
|
||||
this.onItemSelected,
|
||||
this.selected});
|
||||
|
||||
final List<ParsedAddress> items;
|
||||
final Color fillColor;
|
||||
final ParsedAddress? selected;
|
||||
final ValueChanged<ParsedAddress>? onItemSelected;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView.separated(
|
||||
shrinkWrap: true,
|
||||
itemCount: items.length,
|
||||
separatorBuilder: (context, index) => const SizedBox(height: 6),
|
||||
itemBuilder: (context, index) {
|
||||
final src = items[index];
|
||||
return ListTile(
|
||||
title: Text(src.addressSource.label, style: Theme.of(context).textTheme.bodyLarge),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ImageUtil.getImageFromPath(
|
||||
imagePath: src.profileImageUrl, height: 24, width: 24, borderRadius: 12),
|
||||
const SizedBox(width: 6),
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 120, minWidth: 80),
|
||||
child: Text(src.handle, style: Theme.of(context).textTheme.bodyLarge)),
|
||||
const SizedBox(width: 6),
|
||||
SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: selected == src
|
||||
? Icon(Icons.check_circle,
|
||||
size: 20, color: Theme.of(context).colorScheme.primary)
|
||||
: Icon(Icons.circle_outlined,
|
||||
size: 20, color: Theme.of(context).colorScheme.primary)),
|
||||
],
|
||||
),
|
||||
tileColor: fillColor,
|
||||
dense: true,
|
||||
visualDensity: VisualDensity(horizontal: 0, vertical: -3),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 12),
|
||||
leading: ImageUtil.getImageFromPath(
|
||||
imagePath: src.addressSource.iconPath, height: 24, width: 24),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
onTap: () => onItemSelected?.call(src),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
195
lib/src/screens/address_book/edit_address_page.dart
Normal file
|
@ -0,0 +1,195 @@
|
|||
import 'package:cake_wallet/core/address_validator.dart';
|
||||
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/src/screens/exchange/widgets/currency_picker.dart';
|
||||
import 'package:cake_wallet/themes/utils/custom_theme_colors.dart';
|
||||
import 'package:cake_wallet/utils/image_utill.dart';
|
||||
import 'package:cake_wallet/utils/show_pop_up.dart';
|
||||
import 'package:cake_wallet/view_model/contact_list/contact_view_model.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/currency.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
|
||||
class EditAddressPage extends BasePage {
|
||||
EditAddressPage({required this.contactViewModel})
|
||||
: _formKey = GlobalKey<FormState>(),
|
||||
_labelController = TextEditingController(text: contactViewModel.label),
|
||||
_addressController = TextEditingController(text: contactViewModel.address) {
|
||||
_labelController.addListener(() => contactViewModel.label = _labelController.text);
|
||||
_addressController.addListener(() => contactViewModel.address = _addressController.text);
|
||||
}
|
||||
|
||||
@override
|
||||
String get title => contactViewModel.isAddressEdit ? 'Edit Address' : 'Add Address';
|
||||
|
||||
final ContactViewModel contactViewModel;
|
||||
final GlobalKey<FormState> _formKey;
|
||||
final TextEditingController _labelController;
|
||||
final TextEditingController _addressController;
|
||||
|
||||
@override
|
||||
Widget body(BuildContext context) {
|
||||
final fillColor = currentTheme.isDark
|
||||
? CustomThemeColors.backgroundGradientColorDark.withAlpha(100)
|
||||
: CustomThemeColors.backgroundGradientColorLight;
|
||||
|
||||
return Observer(builder: (_) {
|
||||
final initialCurrency = contactViewModel.currency;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text(initialCurrency.fullName ?? initialCurrency.name,
|
||||
style: Theme.of(context).textTheme.bodyMedium),
|
||||
trailing: Icon(Icons.keyboard_arrow_down_outlined,
|
||||
size: 24, color: Theme.of(context).colorScheme.onSurface),
|
||||
tileColor: fillColor,
|
||||
dense: true,
|
||||
visualDensity: VisualDensity(horizontal: 0, vertical: -3),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 12),
|
||||
leading: ImageUtil.getImageFromPath(
|
||||
imagePath: initialCurrency.iconPath ?? '', height: 24, width: 24),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
onTap: () => _presentCurrencyPicker(context, contactViewModel)),
|
||||
const SizedBox(height: 8),
|
||||
StandardTextFormFieldWidget(
|
||||
controller: _labelController,
|
||||
labelText: 'Address label',
|
||||
fillColor: fillColor,
|
||||
suffixIcon: RoundedIconButton(
|
||||
icon: Icons.paste_outlined,
|
||||
onPressed: () async {
|
||||
final data = await Clipboard.getData(Clipboard.kTextPlain);
|
||||
final text = data?.text ?? '';
|
||||
if (text.trim().isEmpty) return;
|
||||
_labelController.text = text.trim();
|
||||
},
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(6)))),
|
||||
addressValidator: (value) {
|
||||
if (value == null || value.trim().isEmpty) return 'Label cannot be empty';
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
StandardTextFormFieldWidget(
|
||||
controller: _addressController,
|
||||
labelText: S.of(context).address,
|
||||
fillColor: fillColor,
|
||||
suffixIcon: RoundedIconButton(
|
||||
icon: Icons.paste_outlined,
|
||||
onPressed: () async {
|
||||
final data = await Clipboard.getData(Clipboard.kTextPlain);
|
||||
final text = data?.text ?? '';
|
||||
if (text.trim().isEmpty) return;
|
||||
_addressController.text = text.trim();
|
||||
},
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(6)))),
|
||||
addressValidator: AddressValidator(type: contactViewModel.currency)),
|
||||
],
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 24),
|
||||
child: Row(
|
||||
children: [
|
||||
if (contactViewModel.isAddressEdit)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: RoundedIconButton(
|
||||
icon: Icons.delete_outline_rounded,
|
||||
onPressed: () async {
|
||||
await contactViewModel.deleteCurrentAddress();
|
||||
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, rootNavigator: true).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;
|
||||
}
|
||||
if (contactViewModel.mode == ContactEditMode.manualAddress) {
|
||||
await contactViewModel.saveManualAddress();
|
||||
} else {
|
||||
await contactViewModel.saveParsedAddress();
|
||||
}
|
||||
if (context.mounted) Navigator.of(context, rootNavigator: true).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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _presentCurrencyPicker(BuildContext context, ContactViewModel contactViewModel) {
|
||||
showPopUp<void>(
|
||||
builder: (_) => CurrencyPicker(
|
||||
selectedAtIndex: contactViewModel.currencies.indexOf(contactViewModel.currency),
|
||||
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,
|
||||
);
|
||||
}
|
264
lib/src/screens/address_book/edit_contact_group_page.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
207
lib/src/screens/address_book/edit_new_contact_group_page.dart
Normal file
|
@ -0,0 +1,207 @@
|
|||
import 'package:cake_wallet/entities/contact.dart';
|
||||
import 'package:cake_wallet/entities/contact_record.dart';
|
||||
import 'package:cake_wallet/entities/parsed_address.dart';
|
||||
import 'package:cake_wallet/routes.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/image_utill.dart';
|
||||
import 'package:cake_wallet/view_model/contact_list/contact_view_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class EditNewContactGroupPage extends BasePage {
|
||||
EditNewContactGroupPage({
|
||||
required this.selectedParsedAddress,
|
||||
required this.contactViewModel,
|
||||
}) : _formKey = GlobalKey<FormState>(),
|
||||
_groupLabelCtl = TextEditingController(
|
||||
text: selectedParsedAddress.profileName ?? '',
|
||||
);
|
||||
|
||||
final ParsedAddress selectedParsedAddress;
|
||||
final ContactViewModel contactViewModel;
|
||||
|
||||
final GlobalKey<FormState> _formKey;
|
||||
final TextEditingController _groupLabelCtl;
|
||||
|
||||
@override
|
||||
String? get title => 'New 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: [
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
selectedParsedAddress.addressSource == AddressSource.contact
|
||||
? 'Choose a contact name and icon'
|
||||
: 'Contact info auto-detected from ${selectedParsedAddress.addressSource.label}',
|
||||
style: theme.textTheme.bodyLarge,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
IntrinsicHeight(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
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: [
|
||||
ImageUtil.getImageFromPath(
|
||||
imagePath: selectedParsedAddress.profileImageUrl,
|
||||
height: 24,
|
||||
width: 24,
|
||||
borderRadius: 30.0),
|
||||
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: TextFormField(
|
||||
controller: _groupLabelCtl,
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
isCollapsed: true,
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
||||
labelText: 'Address group name',
|
||||
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: Colors.transparent)),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(15)),
|
||||
borderSide: BorderSide(color: Colors.transparent),
|
||||
),
|
||||
),
|
||||
style: theme.textTheme.bodyMedium,
|
||||
// validator: (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(),
|
||||
// );
|
||||
// return clash ? 'Group with this name already exists' : null;
|
||||
// },
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 24, top: 8),
|
||||
child: LoadingPrimaryButton(
|
||||
text: 'Next',
|
||||
width: 150,
|
||||
height: 40,
|
||||
onPressed: () async {
|
||||
if (!(_formKey.currentState?.validate() ?? false)) return;
|
||||
|
||||
if (contactViewModel.record != null) {
|
||||
final record = contactViewModel.record!;
|
||||
final handleKey =
|
||||
'${selectedParsedAddress.addressSource.label}-${selectedParsedAddress.handle}'
|
||||
.trim();
|
||||
|
||||
selectedParsedAddress.parsedAddressByCurrencyMap.forEach((cur, addr) {
|
||||
record.setParsedAddress(
|
||||
handleKey,
|
||||
cur,
|
||||
cur.title,
|
||||
addr.trim(),
|
||||
);
|
||||
});
|
||||
|
||||
if (context.mounted) {
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
}
|
||||
} 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.contactPage,
|
||||
arguments: record,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
isLoading: false,
|
||||
isDisabled: false,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
23
lib/src/screens/address_book/entities/user_handles.dart
Normal 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;
|
||||
}
|
41
lib/src/screens/address_book/supported_handles_page.dart
Normal file
|
@ -0,0 +1,41 @@
|
|||
import 'package:cake_wallet/entities/parsed_address.dart';
|
||||
import 'package:cake_wallet/src/screens/address_book/widgets/handles_list_widget.dart';
|
||||
import 'package:cake_wallet/src/screens/base_page.dart';
|
||||
import 'package:cake_wallet/themes/utils/custom_theme_colors.dart';
|
||||
import 'package:cake_wallet/view_model/contact_list/contact_view_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SupportedHandlesPage extends BasePage {
|
||||
SupportedHandlesPage({required this.contactViewModel});
|
||||
|
||||
final ContactViewModel contactViewModel;
|
||||
|
||||
@override
|
||||
String? get title => 'Supported Handles';
|
||||
|
||||
@override
|
||||
Widget body(BuildContext context) {
|
||||
final fill = currentTheme.isDark
|
||||
? CustomThemeColors.backgroundGradientColorDark
|
||||
: CustomThemeColors.backgroundGradientColorLight;
|
||||
|
||||
final selectedInit = <AddressSource>[
|
||||
for (final src in supportedSources)
|
||||
if (contactViewModel.lookupMap[src.label]!.$1()) src
|
||||
];
|
||||
|
||||
return HandlesListWidget(
|
||||
items: supportedSources,
|
||||
fillColor: fill,
|
||||
initiallySelected: selectedInit,
|
||||
onSelectionChanged: (sel) {
|
||||
for (final src in supportedSources) {
|
||||
final pair = contactViewModel.lookupMap[src.label]!;
|
||||
pair.$2(sel.contains(src));
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
import 'package:cake_wallet/utils/address_formatter.dart';
|
||||
import 'package:cake_wallet/utils/image_utill.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
typedef EditCallback = void Function(CryptoCurrency currency, String label);
|
||||
typedef CopyCallback = void Function(String address);
|
||||
typedef ManualByCurrencyMap = Map<CryptoCurrency, Map<String, String>>;
|
||||
|
||||
class ContactAddressesExpansionTile extends StatelessWidget {
|
||||
const ContactAddressesExpansionTile({
|
||||
super.key,
|
||||
required this.manualByCurrency,
|
||||
required this.fillColor,
|
||||
this.title,
|
||||
this.contentPadding,
|
||||
this.onEditPressed,
|
||||
this.onCopyPressed,
|
||||
});
|
||||
|
||||
final ManualByCurrencyMap manualByCurrency;
|
||||
final Color fillColor;
|
||||
final Widget? title;
|
||||
final EdgeInsetsGeometry? contentPadding;
|
||||
final EditCallback? onEditPressed;
|
||||
final CopyCallback? onCopyPressed;
|
||||
|
||||
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),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _addressRow(
|
||||
BuildContext c, {
|
||||
required CryptoCurrency currency,
|
||||
required String label,
|
||||
required String address,
|
||||
}) {
|
||||
return ListTile(
|
||||
title: Text(label, style: Theme.of(c).textTheme.bodyMedium),
|
||||
subtitle: AddressFormatter.buildSegmentedAddress(
|
||||
address: address,
|
||||
walletType: cryptoCurrencyToWalletType(currency),
|
||||
evenTextStyle: Theme.of(c).textTheme.labelSmall!,
|
||||
visibleChunks: 4,
|
||||
shouldTruncate: true,
|
||||
),
|
||||
leading: ImageUtil.getImageFromPath(
|
||||
imagePath: currency.iconPath ?? '',
|
||||
height: 24,
|
||||
width: 24,
|
||||
),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_circleIcon(
|
||||
context: c,
|
||||
icon: Icons.edit,
|
||||
onPressed: () => onEditPressed?.call(currency, label),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_circleIcon(
|
||||
context: c,
|
||||
icon: Icons.copy_all_outlined,
|
||||
onPressed: () => onCopyPressed?.call(address),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
dense: true,
|
||||
visualDensity: const VisualDensity(horizontal: 0, vertical: -4),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
color: fillColor,
|
||||
),
|
||||
child: Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
dividerColor: Colors.transparent,
|
||||
listTileTheme: ListTileThemeData(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
horizontalTitleGap: 4,
|
||||
),
|
||||
splashFactory : NoSplash.splashFactory,
|
||||
splashColor : Colors.transparent,
|
||||
highlightColor: Colors.transparent,
|
||||
hoverColor : Colors.transparent,),
|
||||
child: Padding(
|
||||
padding: contentPadding ?? const EdgeInsets.fromLTRB(12, 4, 12, 8),
|
||||
child: ExpansionTile(
|
||||
iconColor: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
tilePadding: EdgeInsets.zero,
|
||||
dense: true,
|
||||
visualDensity: VisualDensity.compact,
|
||||
title: title ?? const SizedBox(),
|
||||
children: [
|
||||
for (final curEntry in manualByCurrency.entries) ...[
|
||||
for (final labelEntry in curEntry.value.entries)
|
||||
_addressRow(
|
||||
context,
|
||||
currency: curEntry.key,
|
||||
label: labelEntry.key,
|
||||
address: labelEntry.value,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:cake_wallet/entities/parsed_address.dart';
|
||||
import 'package:cake_wallet/utils/image_utill.dart';
|
||||
|
||||
class HandlesListWidget extends StatefulWidget {
|
||||
const HandlesListWidget(
|
||||
{super.key,
|
||||
required this.items,
|
||||
required this.fillColor,
|
||||
this.initiallySelected = const [],
|
||||
this.onSelectionChanged});
|
||||
|
||||
final List<AddressSource> items;
|
||||
final Color fillColor;
|
||||
|
||||
final List<AddressSource> initiallySelected;
|
||||
|
||||
final ValueChanged<Set<AddressSource>>? onSelectionChanged;
|
||||
|
||||
@override
|
||||
State<HandlesListWidget> createState() => _HandlesListWidgetState();
|
||||
}
|
||||
|
||||
class _HandlesListWidgetState extends State<HandlesListWidget> {
|
||||
late final Set<AddressSource> _selected = widget.initiallySelected.toSet();
|
||||
|
||||
void _toggle(AddressSource src) {
|
||||
setState(() {
|
||||
if (_selected.contains(src)) {
|
||||
_selected.remove(src);
|
||||
} else {
|
||||
_selected.add(src);
|
||||
}
|
||||
});
|
||||
widget.onSelectionChanged?.call(_selected);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final iconColor = theme.colorScheme.primary;
|
||||
|
||||
return ListView.separated(
|
||||
shrinkWrap: true,
|
||||
primary: false,
|
||||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
||||
itemCount: widget.items.length,
|
||||
separatorBuilder: (_, __) => const SizedBox(height: 6),
|
||||
itemBuilder: (ctx, i) {
|
||||
final src = widget.items[i];
|
||||
final isSelected = _selected.contains(src);
|
||||
|
||||
return ListTile(
|
||||
title: Text(src.label, style: theme.textTheme.bodyMedium),
|
||||
trailing: Text(src.alias, style: theme.textTheme.bodyMedium),
|
||||
tileColor: widget.fillColor,
|
||||
splashColor: Colors.transparent,
|
||||
dense: true,
|
||||
visualDensity: const VisualDensity(horizontal: 0, vertical: -3),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
leading: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: isSelected
|
||||
? Icon(Icons.check_circle, size: 20, color: iconColor)
|
||||
: Icon(Icons.circle_outlined, size: 20, color: iconColor),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
ImageUtil.getImageFromPath(
|
||||
imagePath: src.iconPath,
|
||||
height: 24,
|
||||
width: 24,
|
||||
),
|
||||
],
|
||||
),
|
||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12))),
|
||||
onTap: () => _toggle(src),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
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,
|
||||
this.suffixIconConstraints,
|
||||
this.prefixIconConstraints,
|
||||
this.outline,
|
||||
});
|
||||
|
||||
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 BoxConstraints? suffixIconConstraints;
|
||||
final BoxConstraints? prefixIconConstraints;
|
||||
final void Function(String)? onChanged;
|
||||
final InputBorder? outline;
|
||||
|
||||
@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: outline ?? OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(15)),
|
||||
borderSide: BorderSide(color: Theme.of(context).colorScheme.outline)),
|
||||
enabledBorder: outline ?? OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(15)),
|
||||
borderSide: BorderSide(color: Colors.transparent),
|
||||
),
|
||||
focusedBorder: outline?.copyWith(
|
||||
borderSide: BorderSide(color: Theme.of(context).colorScheme.primary)
|
||||
),
|
||||
suffixIcon: Padding(padding: const EdgeInsets.only(right: 10), child: suffixIcon),
|
||||
suffix: suffix,
|
||||
prefixIcon: prefixIcon,
|
||||
prefixIconConstraints: prefixIconConstraints,
|
||||
suffixIconConstraints: suffixIconConstraints ?? const BoxConstraints(
|
||||
minWidth: 34,
|
||||
maxWidth: 34,
|
||||
minHeight: 24,
|
||||
maxHeight: 24,
|
||||
)),
|
||||
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
onChanged: onChanged,
|
||||
validator: addressValidator,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -509,8 +509,8 @@ class BuySellPage extends BasePage {
|
|||
|
||||
Future<String> fetchParsedAddress(
|
||||
BuildContext context, String domain, CryptoCurrency currency) async {
|
||||
final parsedAddress = await getIt.get<AddressResolver>().resolve(context, domain, currency);
|
||||
final address = await extractAddressFromParsed(context, parsedAddress);
|
||||
return address;
|
||||
final parsedAddress = await getIt.get<AddressResolverService>().resolve(query: domain,wallet: buySellViewModel.wallet, currency: currency);
|
||||
|
||||
return ''; //TODO: fix return parsedAddress.addressByCurrencyMap[currency] ?? '';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import 'package:cake_wallet/src/screens/cake_pay/widgets/text_icon_button.dart';
|
|||
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
|
||||
import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
|
||||
import 'package:cake_wallet/src/widgets/base_alert_dialog.dart';
|
||||
import 'package:cake_wallet/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart';
|
||||
import 'package:cake_wallet/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart';
|
||||
import 'package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart';
|
||||
import 'package:cake_wallet/src/widgets/primary_button.dart';
|
||||
|
@ -389,9 +390,11 @@ class CakePayBuyCardDetailPage extends BasePage {
|
|||
return ConfirmSendingBottomSheet(
|
||||
key: ValueKey('send_page_confirm_sending_dialog_key'),
|
||||
currentTheme: currentTheme,
|
||||
footerType: FooterType.slideActionButton,
|
||||
walletType: cakePayPurchaseViewModel.sendViewModel.walletType,
|
||||
paymentId: S.of(popupContext).payment_id,
|
||||
paymentIdValue: order?.orderId,
|
||||
cakePayBuyCardViewModel: cakePayPurchaseViewModel,
|
||||
expirationTime: cakePayPurchaseViewModel.formattedRemainingTime,
|
||||
titleText: S.of(popupContext).confirm_transaction,
|
||||
titleIconPath: cakePayPurchaseViewModel.sendViewModel.selectedCryptoCurrency.iconPath,
|
||||
|
@ -405,7 +408,7 @@ class CakePayBuyCardDetailPage extends BasePage {
|
|||
feeFiatAmount:
|
||||
cakePayPurchaseViewModel.sendViewModel.pendingTransactionFeeFiatAmountFormatted,
|
||||
outputs: cakePayPurchaseViewModel.sendViewModel.outputs,
|
||||
onSlideComplete: () async {
|
||||
onSlideActionComplete: () async {
|
||||
Navigator.of(popupContext).pop();
|
||||
cakePayPurchaseViewModel.sendViewModel.commitTransaction(context);
|
||||
},
|
||||
|
|
|
@ -1,190 +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.address;
|
||||
_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 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -641,9 +641,8 @@ class ExchangePage extends BasePage {
|
|||
|
||||
Future<String> fetchParsedAddress(
|
||||
BuildContext context, String domain, CryptoCurrency currency) async {
|
||||
final parsedAddress = await getIt.get<AddressResolver>().resolve(context, domain, currency);
|
||||
final address = await extractAddressFromParsed(context, parsedAddress);
|
||||
return address;
|
||||
final parsedAddress = await getIt.get<AddressResolverService>().resolve(query: domain, currency: currency, wallet: exchangeViewModel.wallet);
|
||||
return ''; //TODO: fix return parsedAddress.addressByCurrencyMap[currency] ?? '';
|
||||
}
|
||||
|
||||
void _showFeeAlert(BuildContext context) async {
|
||||
|
|
|
@ -3,6 +3,7 @@ import 'package:cake_wallet/routes.dart';
|
|||
import 'package:cake_wallet/src/screens/exchange/widgets/desktop_exchange_cards_section.dart';
|
||||
import 'package:cake_wallet/src/screens/exchange/widgets/mobile_exchange_cards_section.dart';
|
||||
import 'package:cake_wallet/src/screens/exchange_trade/widgets/exchange_trade_card_item_widget.dart';
|
||||
import 'package:cake_wallet/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart';
|
||||
import 'package:cake_wallet/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart';
|
||||
import 'package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart';
|
||||
import 'package:cake_wallet/themes/core/material_base_theme.dart';
|
||||
|
@ -274,6 +275,7 @@ class ExchangeTradeState extends State<ExchangeTradeForm> {
|
|||
return ConfirmSendingBottomSheet(
|
||||
key: ValueKey('exchange_trade_page_confirm_sending_bottom_sheet_key'),
|
||||
currentTheme: widget.currentTheme,
|
||||
footerType: FooterType.slideActionButton,
|
||||
walletType: widget.exchangeTradeViewModel.sendViewModel.walletType,
|
||||
titleText: S.of(bottomSheetContext).confirm_transaction,
|
||||
titleIconPath:
|
||||
|
@ -293,10 +295,11 @@ class ExchangeTradeState extends State<ExchangeTradeForm> {
|
|||
feeFiatAmount: widget.exchangeTradeViewModel.sendViewModel
|
||||
.pendingTransactionFeeFiatAmountFormatted,
|
||||
outputs: widget.exchangeTradeViewModel.sendViewModel.outputs,
|
||||
onSlideComplete: () async {
|
||||
if (bottomSheetContext.mounted) {
|
||||
Navigator.of(bottomSheetContext).pop();
|
||||
}
|
||||
onSlideActionComplete: () async {
|
||||
if (bottomSheetContext.mounted) {
|
||||
Navigator.of(bottomSheetContext).pop();
|
||||
}
|
||||
|
||||
widget.exchangeTradeViewModel.sendViewModel.commitTransaction(context);
|
||||
},
|
||||
);
|
||||
|
@ -306,9 +309,10 @@ class ExchangeTradeState extends State<ExchangeTradeForm> {
|
|||
});
|
||||
}
|
||||
|
||||
|
||||
if (state is TransactionCommitted) {
|
||||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
(_) async {
|
||||
(_) async {
|
||||
if (!mounted) return;
|
||||
|
||||
await showModalBottomSheet<void>(
|
||||
|
@ -317,16 +321,17 @@ class ExchangeTradeState extends State<ExchangeTradeForm> {
|
|||
builder: (BuildContext bottomSheetContext) {
|
||||
return InfoBottomSheet(
|
||||
currentTheme: widget.currentTheme,
|
||||
footerType: FooterType.singleActionButton,
|
||||
titleText: S.of(bottomSheetContext).transaction_sent,
|
||||
contentImage: 'assets/images/birthday_cake.png',
|
||||
actionButtonText: S.of(bottomSheetContext).close,
|
||||
actionButtonKey: ValueKey('send_page_sent_dialog_ok_button_key'),
|
||||
actionButton: () {
|
||||
singleActionButtonText: S.of(bottomSheetContext).close,
|
||||
singleActionButtonKey: ValueKey('send_page_sent_dialog_ok_button_key'),
|
||||
onSingleActionButtonPressed: () {
|
||||
Navigator.of(bottomSheetContext).pop();
|
||||
if (mounted) {
|
||||
Navigator.of(context).pushNamedAndRemoveUntil(
|
||||
Routes.dashboard,
|
||||
(route) => false,
|
||||
(route) => false,
|
||||
);
|
||||
}
|
||||
RequestReviewHandler.requestReview();
|
||||
|
|
|
@ -4,6 +4,7 @@ import 'package:cake_wallet/src/screens/base_page.dart';
|
|||
import 'package:cake_wallet/src/screens/integrations/deuro/widgets/interest_card_widget.dart';
|
||||
import 'package:cake_wallet/src/screens/integrations/deuro/widgets/savings_card_widget.dart';
|
||||
import 'package:cake_wallet/src/screens/integrations/deuro/widgets/savings_edit_sheet.dart';
|
||||
import 'package:cake_wallet/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart';
|
||||
import 'package:cake_wallet/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart';
|
||||
import 'package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart';
|
||||
import 'package:cake_wallet/src/widgets/gradient_background.dart';
|
||||
|
@ -128,11 +129,11 @@ class DEuroSavingsPage extends BasePage {
|
|||
feeValue: tx.feeFormatted,
|
||||
feeFiatAmount: "",
|
||||
outputs: [],
|
||||
onSlideComplete: () async {
|
||||
onSlideActionComplete: () async {
|
||||
Navigator.of(bottomSheetContext).pop(true);
|
||||
dEuroViewModel.commitTransaction();
|
||||
},
|
||||
change: tx.change,
|
||||
change: tx.change, footerType: FooterType.slideActionButton,
|
||||
),
|
||||
);
|
||||
|
||||
|
@ -159,11 +160,11 @@ class DEuroSavingsPage extends BasePage {
|
|||
feeValue: tx.feeFormatted,
|
||||
feeFiatAmount: "",
|
||||
outputs: [],
|
||||
onSlideComplete: () {
|
||||
onSlideActionComplete: () {
|
||||
Navigator.of(bottomSheetContext).pop(true);
|
||||
dEuroViewModel.commitApprovalTransaction();
|
||||
},
|
||||
change: tx.change,
|
||||
change: tx.change, footerType: FooterType.slideActionButton,
|
||||
),
|
||||
);
|
||||
|
||||
|
@ -180,12 +181,13 @@ class DEuroSavingsPage extends BasePage {
|
|||
isDismissible: false,
|
||||
builder: (BuildContext bottomSheetContext) => InfoBottomSheet(
|
||||
currentTheme: currentTheme,
|
||||
footerType: FooterType.singleActionButton,
|
||||
titleText: S.of(bottomSheetContext).transaction_sent,
|
||||
contentImage: 'assets/images/birthday_cake.png',
|
||||
content: S.of(bottomSheetContext).deuro_tx_commited_content,
|
||||
actionButtonText: S.of(bottomSheetContext).close,
|
||||
actionButtonKey: ValueKey('send_page_sent_dialog_ok_button_key'),
|
||||
actionButton: () => Navigator.of(bottomSheetContext).pop(),
|
||||
singleActionButtonText: S.of(bottomSheetContext).close,
|
||||
singleActionButtonKey: ValueKey('send_page_sent_dialog_ok_button_key'),
|
||||
onSingleActionButtonPressed: () => Navigator.of(bottomSheetContext).pop(),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:cake_wallet/entities/qr_view_data.dart';
|
||||
import 'package:cake_wallet/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart';
|
||||
import 'package:cake_wallet/src/widgets/primary_button.dart';
|
||||
import 'package:cake_wallet/routes.dart';
|
||||
import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart';
|
||||
|
@ -317,14 +318,13 @@ class QRWidget extends StatelessWidget {
|
|||
titleText: S.of(context).payjoin_unavailable_sheet_title,
|
||||
content: S.of(context).payjoin_unavailable_sheet_content,
|
||||
currentTheme: currentTheme,
|
||||
isTwoAction: true,
|
||||
leftButtonText: S.of(context).learn_more,
|
||||
actionLeftButton: () => launchUrl(
|
||||
Uri.parse("https://docs.cakewallet.com/cryptos/bitcoin/#payjoin"),
|
||||
mode: LaunchMode.externalApplication,
|
||||
),
|
||||
rightButtonText: S.of(context).ok,
|
||||
actionRightButton: () => Navigator.of(context).pop(),
|
||||
footerType: FooterType.doubleActionButton,
|
||||
doubleActionLeftButtonText: S.of(context).learn_more,
|
||||
onLeftActionButtonPressed: () => launchUrl(
|
||||
Uri.parse("https://docs.cakewallet.com/cryptos/bitcoin/#payjoin"),
|
||||
mode: LaunchMode.externalApplication),
|
||||
doubleActionRightButtonText: S.of(context).ok,
|
||||
onRightActionButtonPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -17,12 +17,14 @@ import 'package:cake_wallet/src/widgets/adaptable_page_view.dart';
|
|||
import 'package:cake_wallet/src/widgets/add_template_button.dart';
|
||||
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
|
||||
import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
|
||||
import 'package:cake_wallet/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart';
|
||||
import 'package:cake_wallet/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart';
|
||||
import 'package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart';
|
||||
import 'package:cake_wallet/src/widgets/keyboard_done_button.dart';
|
||||
import 'package:cake_wallet/src/widgets/picker.dart';
|
||||
import 'package:cake_wallet/src/widgets/primary_button.dart';
|
||||
import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
|
||||
import 'package:cake_wallet/src/widgets/simple_checkbox.dart';
|
||||
import 'package:cake_wallet/src/widgets/template_tile.dart';
|
||||
import 'package:cake_wallet/src/widgets/trail_button.dart';
|
||||
import 'package:cake_wallet/utils/payment_request.dart';
|
||||
|
@ -569,6 +571,7 @@ class SendPage extends BasePage {
|
|||
key: ValueKey('send_page_confirm_sending_dialog_key'),
|
||||
titleText: S.of(bottomSheetContext).confirm_transaction,
|
||||
currentTheme: currentTheme,
|
||||
footerType: FooterType.slideActionButton,
|
||||
walletType: sendViewModel.walletType,
|
||||
titleIconPath: sendViewModel.selectedCryptoCurrency.iconPath,
|
||||
currency: sendViewModel.selectedCryptoCurrency,
|
||||
|
@ -581,7 +584,7 @@ class SendPage extends BasePage {
|
|||
feeValue: sendViewModel.pendingTransaction!.feeFormatted,
|
||||
feeFiatAmount: sendViewModel.pendingTransactionFeeFiatAmountFormatted,
|
||||
outputs: sendViewModel.outputs,
|
||||
onSlideComplete: () async {
|
||||
onSlideActionComplete: () async {
|
||||
Navigator.of(bottomSheetContext).pop(true);
|
||||
sendViewModel.commitTransaction(context);
|
||||
},
|
||||
|
@ -602,68 +605,38 @@ class SendPage extends BasePage {
|
|||
return;
|
||||
}
|
||||
|
||||
newContactAddress = newContactAddress ?? sendViewModel.newContactAddress();
|
||||
// newContactAddress = newContactAddress ?? sendViewModel.newContactAddress(); //TODO: fix this
|
||||
//
|
||||
// if (newContactAddress?.address != null &&
|
||||
// isRegularElectrumAddress(newContactAddress!.address)) {
|
||||
// newContactAddress = null;
|
||||
// }
|
||||
|
||||
if (newContactAddress?.address != null &&
|
||||
isRegularElectrumAddress(newContactAddress!.address)) {
|
||||
newContactAddress = null;
|
||||
}
|
||||
|
||||
bool showContactSheet = (newContactAddress != null && sendViewModel.showAddressBookPopup);
|
||||
// bool showContactSheet = (newContactAddress != null && sendViewModel.showAddressBookPopup);
|
||||
|
||||
await showModalBottomSheet<void>(
|
||||
context: context,
|
||||
isDismissible: false,
|
||||
builder: (BuildContext bottomSheetContext) {
|
||||
return showContactSheet && sendViewModel.ocpRequest == null
|
||||
? InfoBottomSheet(
|
||||
currentTheme: currentTheme,
|
||||
showDontAskMeCheckbox: true,
|
||||
onCheckboxChanged: (value) => sendViewModel.setShowAddressBookPopup(!value),
|
||||
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,
|
||||
isTwoAction: true,
|
||||
leftButtonText: 'No',
|
||||
rightButtonText: 'Yes',
|
||||
actionLeftButton: () {
|
||||
Navigator.of(bottomSheetContext).pop();
|
||||
if (context.mounted) {
|
||||
Navigator.of(context)
|
||||
.pushNamedAndRemoveUntil(Routes.dashboard, (route) => false);
|
||||
}
|
||||
RequestReviewHandler.requestReview();
|
||||
newContactAddress = null;
|
||||
},
|
||||
actionRightButton: () {
|
||||
Navigator.of(bottomSheetContext).pop();
|
||||
RequestReviewHandler.requestReview();
|
||||
if (context.mounted) {
|
||||
Navigator.of(context).pushNamed(Routes.addressBookAddContact,
|
||||
arguments: newContactAddress);
|
||||
}
|
||||
newContactAddress = null;
|
||||
},
|
||||
)
|
||||
: InfoBottomSheet(
|
||||
currentTheme: currentTheme,
|
||||
titleText: S.of(bottomSheetContext).transaction_sent,
|
||||
contentImage: 'assets/images/birthday_cake.png',
|
||||
actionButtonText: S.of(bottomSheetContext).close,
|
||||
actionButtonKey: ValueKey('send_page_sent_dialog_ok_button_key'),
|
||||
actionButton: () {
|
||||
Navigator.of(bottomSheetContext).pop();
|
||||
Future.delayed(Duration.zero, () {
|
||||
if (context.mounted) {
|
||||
Navigator.of(context)
|
||||
.pushNamedAndRemoveUntil(Routes.dashboard, (route) => false);
|
||||
}
|
||||
RequestReviewHandler.requestReview();
|
||||
newContactAddress = null;
|
||||
});
|
||||
},
|
||||
);
|
||||
return InfoBottomSheet(
|
||||
currentTheme: currentTheme,
|
||||
footerType: FooterType.singleActionButton,
|
||||
titleText: S.of(bottomSheetContext).transaction_sent,
|
||||
contentImage: 'assets/images/birthday_cake.png',
|
||||
singleActionButtonText: S.of(bottomSheetContext).close,
|
||||
singleActionButtonKey: ValueKey('send_page_transaction_sent_button_key'),
|
||||
onSingleActionButtonPressed: () {
|
||||
Navigator.of(bottomSheetContext).pop();
|
||||
Future.delayed(Duration.zero, () {
|
||||
if (context.mounted) {
|
||||
Navigator.of(context)
|
||||
.pushNamedAndRemoveUntil(Routes.dashboard, (route) => false);
|
||||
}
|
||||
RequestReviewHandler.requestReview();
|
||||
newContactAddress = null;
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -710,14 +683,14 @@ class SendPage extends BasePage {
|
|||
dialogContext = context;
|
||||
return InfoBottomSheet(
|
||||
currentTheme: currentTheme,
|
||||
footerType: FooterType.singleActionButton,
|
||||
titleText: S.of(context).proceed_on_device,
|
||||
contentImage:
|
||||
'assets/images/hardware_wallet/ledger_nano_x.png',
|
||||
'assets/images/hardware_wallet/ledger_nano_x.png',
|
||||
contentImageColor: Theme.of(context).colorScheme.onSurface,
|
||||
content: S.of(context).proceed_on_device_description,
|
||||
isTwoAction: false,
|
||||
actionButtonText: S.of(context).cancel,
|
||||
actionButton: () {
|
||||
singleActionButtonText: S.of(context).cancel,
|
||||
onSingleActionButtonPressed: () {
|
||||
sendViewModel.state = InitialExecutionState();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
|
|
|
@ -1,135 +1,134 @@
|
|||
import 'package:cake_wallet/entities/parsed_address.dart';
|
||||
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
|
||||
import 'package:cake_wallet/utils/show_pop_up.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'choose_yat_address_alert.dart';
|
||||
|
||||
Future<String> extractAddressFromParsed(
|
||||
BuildContext context,
|
||||
ParsedAddress parsedAddress) async {
|
||||
if (!context.mounted) return parsedAddress.addresses.first;
|
||||
|
||||
var title = '';
|
||||
var content = '';
|
||||
var address = '';
|
||||
var profileImageUrl = '';
|
||||
var profileName = '';
|
||||
|
||||
switch (parsedAddress.parseFrom) {
|
||||
case ParseFrom.unstoppableDomains:
|
||||
title = S.of(context).address_detected;
|
||||
content = S.of(context).address_from_domain(parsedAddress.name);
|
||||
address = parsedAddress.addresses.first;
|
||||
break;
|
||||
case ParseFrom.ens:
|
||||
title = S.of(context).address_detected;
|
||||
content = S.of(context).extracted_address_content('${parsedAddress.name} (ENS)');
|
||||
address = parsedAddress.addresses.first;
|
||||
break;
|
||||
case ParseFrom.openAlias:
|
||||
title = S.of(context).address_detected;
|
||||
content = S.of(context).extracted_address_content('${parsedAddress.name} (OpenAlias)');
|
||||
address = parsedAddress.addresses.first;
|
||||
break;
|
||||
case ParseFrom.wellKnown:
|
||||
title = S.of(context).address_detected;
|
||||
content = S.of(context).extracted_address_content('${parsedAddress.name} (Well-Known)');
|
||||
address = parsedAddress.addresses.first;
|
||||
break;
|
||||
case ParseFrom.fio:
|
||||
title = S.of(context).address_detected;
|
||||
content = S.of(context).extracted_address_content('${parsedAddress.name} (FIO)');
|
||||
address = parsedAddress.addresses.first;
|
||||
break;
|
||||
case ParseFrom.twitter:
|
||||
title = S.of(context).address_detected;
|
||||
content = S.of(context).extracted_address_content('${parsedAddress.name} (Twitter)');
|
||||
address = parsedAddress.addresses.first;
|
||||
profileImageUrl = parsedAddress.profileImageUrl;
|
||||
profileName = parsedAddress.profileName;
|
||||
break;
|
||||
case ParseFrom.mastodon:
|
||||
title = S.of(context).address_detected;
|
||||
content = S.of(context).extracted_address_content('${parsedAddress.name} (Mastodon)');
|
||||
address = parsedAddress.addresses.first;
|
||||
profileImageUrl = parsedAddress.profileImageUrl;
|
||||
profileName = parsedAddress.profileName;
|
||||
break;
|
||||
case ParseFrom.nostr:
|
||||
title = S.of(context).address_detected;
|
||||
content = S.of(context).extracted_address_content('${parsedAddress.name} (Nostr NIP-05)');
|
||||
address = parsedAddress.addresses.first;
|
||||
profileImageUrl = parsedAddress.profileImageUrl;
|
||||
profileName = parsedAddress.profileName;
|
||||
break;
|
||||
case ParseFrom.thorChain:
|
||||
title = S.of(context).address_detected;
|
||||
content = S.of(context).extracted_address_content('${parsedAddress.name} (ThorChain)');
|
||||
address = parsedAddress.addresses.first;
|
||||
break;
|
||||
case ParseFrom.zanoAlias:
|
||||
title = S.of(context).address_detected;
|
||||
content = S.of(context).extracted_address_content('${parsedAddress.name} (Zano Alias)');
|
||||
address = parsedAddress.addresses.first;
|
||||
break;
|
||||
case ParseFrom.bip353:
|
||||
title = S.of(context).address_detected;
|
||||
content = S.of(context).extracted_address_content('${parsedAddress.name} (BIP-353)');
|
||||
address = parsedAddress.addresses.first;
|
||||
break;
|
||||
case ParseFrom.yatRecord:
|
||||
if (parsedAddress.name.isEmpty) {
|
||||
title = S.of(context).yat_error;
|
||||
content = S.of(context).yat_error_content;
|
||||
address = parsedAddress.addresses.first;
|
||||
break;
|
||||
}
|
||||
|
||||
title = S.of(context).address_detected;
|
||||
content = S.of(context).address_from_yat(parsedAddress.name);
|
||||
|
||||
if (parsedAddress.addresses.length == 1) {
|
||||
address = parsedAddress.addresses.first;
|
||||
break;
|
||||
}
|
||||
|
||||
content += S.of(context).choose_address;
|
||||
|
||||
address = await showPopUp<String?>(
|
||||
context: context,
|
||||
builder: (context) => PopScope(
|
||||
child: ChooseYatAddressAlert(
|
||||
alertTitle: title,
|
||||
alertContent: content,
|
||||
addresses: parsedAddress.addresses,
|
||||
),
|
||||
canPop: false,
|
||||
),
|
||||
) ??
|
||||
'';
|
||||
|
||||
if (address.isEmpty) {
|
||||
return parsedAddress.name;
|
||||
}
|
||||
|
||||
return address;
|
||||
case ParseFrom.contact:
|
||||
case ParseFrom.notParsed:
|
||||
return parsedAddress.addresses.first;
|
||||
}
|
||||
|
||||
await showPopUp<void>(
|
||||
context: context,
|
||||
builder: (context) => AlertWithOneAction(
|
||||
alertTitle: title,
|
||||
headerTitleText: profileName.isEmpty ? null : profileName,
|
||||
headerImageProfileUrl: profileImageUrl.isEmpty ? null : profileImageUrl,
|
||||
alertContent: content,
|
||||
buttonText: S.of(context).ok,
|
||||
buttonAction: () => Navigator.of(context).pop(),
|
||||
),
|
||||
);
|
||||
|
||||
return address;
|
||||
}
|
||||
// import 'package:cake_wallet/entities/parsed_address.dart';
|
||||
// import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
|
||||
// import 'package:cake_wallet/utils/show_pop_up.dart';
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'package:cake_wallet/generated/i18n.dart';
|
||||
// import 'choose_yat_address_alert.dart';
|
||||
//
|
||||
// Future<String> extractAddressFromParsed(
|
||||
// BuildContext context,
|
||||
// ParsedAddress parsedAddress) async {
|
||||
// var title = '';
|
||||
// var content = '';
|
||||
// var address = '';
|
||||
// var profileImageUrl = '';
|
||||
// var profileName = '';
|
||||
//
|
||||
// switch (parsedAddress.addressSource) {
|
||||
// case AddressSource.unstoppableDomains:
|
||||
// title = S.of(context).address_detected;
|
||||
// content = S.of(context).address_from_domain(parsedAddress.handle);
|
||||
// address = parsedAddress.addresses.first;
|
||||
// break;
|
||||
// case AddressSource.ens:
|
||||
// title = S.of(context).address_detected;
|
||||
// content = S.of(context).extracted_address_content('${parsedAddress.handle} (ENS)');
|
||||
// address = parsedAddress.addresses.first;
|
||||
// break;
|
||||
// case AddressSource.openAlias:
|
||||
// title = S.of(context).address_detected;
|
||||
// content = S.of(context).extracted_address_content('${parsedAddress.handle} (OpenAlias)');
|
||||
// address = parsedAddress.addresses.first;
|
||||
// break;
|
||||
// case AddressSource.wellKnown:
|
||||
// title = S.of(context).address_detected;
|
||||
// content = S.of(context).extracted_address_content('${parsedAddress.handle} (Well-Known)');
|
||||
// address = parsedAddress.addresses.first;
|
||||
// break;
|
||||
// case AddressSource.fio:
|
||||
// title = S.of(context).address_detected;
|
||||
// content = S.of(context).extracted_address_content('${parsedAddress.handle} (FIO)');
|
||||
// address = parsedAddress.addresses.first;
|
||||
// break;
|
||||
// case AddressSource.twitter:
|
||||
// title = S.of(context).address_detected;
|
||||
// content = S.of(context).extracted_address_content('${parsedAddress.handle} (Twitter)');
|
||||
// address = parsedAddress.addresses.first;
|
||||
// profileImageUrl = parsedAddress.profileImageUrl;
|
||||
// profileName = parsedAddress.profileName;
|
||||
// break;
|
||||
// case AddressSource.mastodon:
|
||||
// title = S.of(context).address_detected;
|
||||
// content = S.of(context).extracted_address_content('${parsedAddress.handle} (Mastodon)');
|
||||
// address = parsedAddress.addresses.first;
|
||||
// profileImageUrl = parsedAddress.profileImageUrl;
|
||||
// profileName = parsedAddress.profileName;
|
||||
// break;
|
||||
// case AddressSource.nostr:
|
||||
// title = S.of(context).address_detected;
|
||||
// content = S.of(context).extracted_address_content('${parsedAddress.handle} (Nostr NIP-05)');
|
||||
// address = parsedAddress.addresses.first;
|
||||
// profileImageUrl = parsedAddress.profileImageUrl;
|
||||
// profileName = parsedAddress.profileName;
|
||||
// break;
|
||||
// case AddressSource.thorChain:
|
||||
// title = S.of(context).address_detected;
|
||||
// content = S.of(context).extracted_address_content('${parsedAddress.handle} (ThorChain)');
|
||||
// address = parsedAddress.addresses.first;
|
||||
// break;
|
||||
// case AddressSource.zanoAlias:
|
||||
// title = S.of(context).address_detected;
|
||||
// content = S.of(context).extracted_address_content('${parsedAddress.handle} (Zano Alias)');
|
||||
// address = parsedAddress.addresses.first;
|
||||
// break;
|
||||
// case AddressSource.bip353:
|
||||
// title = S.of(context).address_detected;
|
||||
// content = S.of(context).extracted_address_content('${parsedAddress.handle} (BIP-353)');
|
||||
// address = parsedAddress.addresses.first;
|
||||
// break;
|
||||
// case AddressSource.yatRecord:
|
||||
// if (parsedAddress.handle.isEmpty) {
|
||||
// title = S.of(context).yat_error;
|
||||
// content = S.of(context).yat_error_content;
|
||||
// address = parsedAddress.addresses.first;
|
||||
// break;
|
||||
// }
|
||||
//
|
||||
// title = S.of(context).address_detected;
|
||||
// content = S.of(context).address_from_yat(parsedAddress.handle);
|
||||
//
|
||||
// if (parsedAddress.addresses.length == 1) {
|
||||
// address = parsedAddress.addresses.first;
|
||||
// break;
|
||||
// }
|
||||
//
|
||||
// content += S.of(context).choose_address;
|
||||
//
|
||||
// address = await showPopUp<String?>(
|
||||
// context: context,
|
||||
// builder: (BuildContext context) {
|
||||
//
|
||||
// return WillPopScope(
|
||||
// child: ChooseYatAddressAlert(
|
||||
// alertTitle: title,
|
||||
// alertContent: content,
|
||||
// addresses: parsedAddress.addresses),
|
||||
// onWillPop: () async => false);
|
||||
// }) ?? '';
|
||||
//
|
||||
// if (address.isEmpty) {
|
||||
// return parsedAddress.handle;
|
||||
// }
|
||||
//
|
||||
// return address;
|
||||
// case AddressSource.contact:
|
||||
// case AddressSource.notParsed:
|
||||
// address = parsedAddress.addresses.first;
|
||||
// return address;
|
||||
// }
|
||||
//
|
||||
// await showPopUp<void>(
|
||||
// context: context,
|
||||
// builder: (BuildContext context) {
|
||||
//
|
||||
// return AlertWithOneAction(
|
||||
// alertTitle: title,
|
||||
// headerTitleText: profileName.isEmpty ? null : profileName,
|
||||
// headerImageProfileUrl: profileImageUrl.isEmpty ? null : profileImageUrl,
|
||||
// alertContent: content,
|
||||
// buttonText: S.of(context).ok,
|
||||
// buttonAction: () => Navigator.of(context).pop());
|
||||
// });
|
||||
//
|
||||
// return address;
|
||||
// }
|
||||
|
|
|
@ -53,8 +53,22 @@ class DomainLookupsPage extends BasePage {
|
|||
title: 'Zano Aliases',
|
||||
value: _privacySettingsViewModel.lookupsZanoAlias,
|
||||
onValueChange: (_, bool value) => _privacySettingsViewModel.setLookupsZanoAlias(value)),
|
||||
|
||||
//if (!isHaven) it does not work correctly
|
||||
SettingsSwitcherCell(
|
||||
title: 'FIO',
|
||||
value: _privacySettingsViewModel.lookupsFio,
|
||||
onValueChange: (_, bool value) => _privacySettingsViewModel.setLookupsFio(value)),
|
||||
SettingsSwitcherCell(
|
||||
title: 'Nostr',
|
||||
value: _privacySettingsViewModel.lookupsNostr,
|
||||
onValueChange: (_, bool value) => _privacySettingsViewModel.setLookupsNostr(value)),
|
||||
SettingsSwitcherCell(
|
||||
title: 'ThorChain',
|
||||
value: _privacySettingsViewModel.lookupsThorChain,
|
||||
onValueChange: (_, bool value) => _privacySettingsViewModel.setLookupsThorChain(value)),
|
||||
SettingsSwitcherCell(
|
||||
title: 'BIP-353',
|
||||
value: _privacySettingsViewModel.lookupsBip353,
|
||||
onValueChange: (_, bool value) => _privacySettingsViewModel.setLookupsBip353(value)),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
@ -8,6 +8,7 @@ import 'package:cake_wallet/src/screens/transaction_details/transaction_expandab
|
|||
import 'package:cake_wallet/src/screens/transaction_details/widgets/textfield_list_row.dart';
|
||||
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
|
||||
import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
|
||||
import 'package:cake_wallet/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart';
|
||||
import 'package:cake_wallet/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart';
|
||||
import 'package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart';
|
||||
import 'package:cake_wallet/src/widgets/list_row.dart';
|
||||
|
@ -190,6 +191,7 @@ class RBFDetailsPage extends BasePage {
|
|||
key: ValueKey('rbf_confirm_sending_bottom_sheet'),
|
||||
titleText: S.of(bottomSheetContext).confirm_transaction,
|
||||
currentTheme: currentTheme,
|
||||
footerType: FooterType.slideActionButton,
|
||||
walletType: transactionDetailsViewModel.sendViewModel.walletType,
|
||||
titleIconPath: transactionDetailsViewModel.sendViewModel.selectedCryptoCurrency.iconPath,
|
||||
currency: transactionDetailsViewModel.sendViewModel.selectedCryptoCurrency,
|
||||
|
@ -200,7 +202,7 @@ class RBFDetailsPage extends BasePage {
|
|||
feeValue: transactionDetailsViewModel.sendViewModel.pendingTransaction!.feeFormatted,
|
||||
feeFiatAmount: transactionDetailsViewModel.sendViewModel.pendingTransactionFeeFiatAmountFormatted,
|
||||
outputs: transactionDetailsViewModel.sendViewModel.outputs,
|
||||
onSlideComplete: () async {
|
||||
onSlideActionComplete: () async {
|
||||
Navigator.of(bottomSheetContext).pop();
|
||||
await transactionDetailsViewModel.sendViewModel.commitTransaction(context);
|
||||
try {
|
||||
|
|
|
@ -10,6 +10,7 @@ class FilteredList extends StatefulWidget {
|
|||
this.canReorder = true,
|
||||
this.shrinkWrap = false,
|
||||
this.physics,
|
||||
this.itemPadding = const EdgeInsets.symmetric(vertical: 4),
|
||||
});
|
||||
|
||||
final ObservableList<dynamic> list;
|
||||
|
@ -18,39 +19,51 @@ class FilteredList extends StatefulWidget {
|
|||
final bool canReorder;
|
||||
final bool shrinkWrap;
|
||||
final ScrollPhysics? physics;
|
||||
final EdgeInsets itemPadding;
|
||||
|
||||
@override
|
||||
FilteredListState createState() => FilteredListState();
|
||||
}
|
||||
|
||||
class FilteredListState extends State<FilteredList> {
|
||||
Widget _buildPaddedItem(BuildContext ctx, int index) {
|
||||
return Padding(
|
||||
key: ValueKey(widget.list[index]),
|
||||
padding: widget.itemPadding,
|
||||
child: widget.itemBuilder(ctx, index),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.canReorder) {
|
||||
return Observer(
|
||||
builder: (_) => ReorderableListView.builder(
|
||||
shrinkWrap: widget.shrinkWrap,
|
||||
physics: widget.physics ?? const BouncingScrollPhysics(),
|
||||
itemBuilder: widget.itemBuilder,
|
||||
itemCount: widget.list.length,
|
||||
onReorder: (int oldIndex, int newIndex) {
|
||||
if (oldIndex < newIndex) {
|
||||
newIndex -= 1;
|
||||
}
|
||||
final dynamic item = widget.list.removeAt(oldIndex);
|
||||
widget.list.insert(newIndex, item);
|
||||
widget.updateFunction();
|
||||
},
|
||||
),
|
||||
);
|
||||
} else {
|
||||
if (!widget.canReorder) {
|
||||
return Observer(
|
||||
builder: (_) => ListView.builder(
|
||||
shrinkWrap: widget.shrinkWrap,
|
||||
physics: widget.physics ?? const BouncingScrollPhysics(),
|
||||
itemBuilder: widget.itemBuilder,
|
||||
itemCount: widget.list.length,
|
||||
itemBuilder: _buildPaddedItem,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Observer(
|
||||
builder: (_) => ReorderableListView.builder(
|
||||
shrinkWrap: widget.shrinkWrap,
|
||||
physics: widget.physics ?? const BouncingScrollPhysics(),
|
||||
itemCount: widget.list.length,
|
||||
itemBuilder: _buildPaddedItem,
|
||||
onReorder: (oldIndex, newIndex) {
|
||||
if (oldIndex < newIndex) newIndex -= 1;
|
||||
final item = widget.list.removeAt(oldIndex);
|
||||
widget.list.insert(newIndex, item);
|
||||
widget.updateFunction();
|
||||
},
|
||||
proxyDecorator: (child, _, __) => Material(
|
||||
color: Colors.transparent,
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,147 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:cake_wallet/di.dart';
|
||||
import 'package:cake_wallet/entities/contact_record.dart';
|
||||
import 'package:cake_wallet/entities/parsed_address.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_contact_group_page.dart';
|
||||
import 'package:cake_wallet/src/screens/address_book/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/entities/address_edit_request.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';
|
||||
|
||||
class AddressBookBottomSheet extends StatelessWidget {
|
||||
const AddressBookBottomSheet({
|
||||
super.key,
|
||||
required this.onHandlerSearch,
|
||||
this.initialRoute,
|
||||
this.initialArgs,
|
||||
});
|
||||
|
||||
final Future<List<ParsedAddress>> Function(String q) onHandlerSearch;
|
||||
final String? initialRoute;
|
||||
final Object? initialArgs;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ClipRRect(
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
|
||||
child: Material(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildDragHandle(context),
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: MediaQuery.of(context).size.height * .7,
|
||||
),
|
||||
child: _AddContactNavigator(
|
||||
onHandlerSearch: onHandlerSearch,
|
||||
initialRoute: initialRoute ?? Navigator.defaultRouteName,
|
||||
initialArgs: initialArgs,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildDragHandle(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 16),
|
||||
child: Row(
|
||||
children: [
|
||||
const Spacer(flex: 4),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Container(
|
||||
height: 6,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Spacer(flex: 4),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _AddContactNavigator extends StatelessWidget {
|
||||
const _AddContactNavigator({
|
||||
required this.onHandlerSearch,
|
||||
required this.initialRoute,
|
||||
this.initialArgs,
|
||||
});
|
||||
|
||||
final Future<List<ParsedAddress>> Function(String) onHandlerSearch;
|
||||
final String initialRoute;
|
||||
final Object? initialArgs;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Navigator(
|
||||
onGenerateInitialRoutes: (_, __) => [
|
||||
_routeFor(initialRoute, initialArgs),
|
||||
],
|
||||
onGenerateRoute: (settings) => _routeFor(
|
||||
settings.name ?? Navigator.defaultRouteName,
|
||||
settings.arguments,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Route<dynamic> _routeFor(String name, Object? args) {
|
||||
late final Widget page;
|
||||
|
||||
switch (name) {
|
||||
case Routes.supportedHandlesPage:
|
||||
page = getIt<SupportedHandlesPage>();
|
||||
break;
|
||||
case Routes.newContactWelcomePage:
|
||||
final list = args as List<dynamic>;
|
||||
final onSearch = list[0] as Future<List<ParsedAddress>> Function(String);
|
||||
final 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:
|
||||
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;
|
||||
case Routes.contactPage:
|
||||
page = getIt<ContactPage>(param1: args as ContactRecord);
|
||||
break;
|
||||
case Routes.editAddressPage:
|
||||
page = getIt<EditAddressPage>(param1: args as AddressEditRequest);
|
||||
break;
|
||||
default:
|
||||
page = NewContactWelcomePage(onSearch: onHandlerSearch, handleOnly: false);
|
||||
}
|
||||
|
||||
return MaterialPageRoute(
|
||||
builder: (_) => page,
|
||||
settings: RouteSettings(name: name, arguments: args),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,65 +1,78 @@
|
|||
import 'package:cake_wallet/src/widgets/primary_button.dart';
|
||||
import 'package:cake_wallet/src/widgets/standard_slide_button_widget.dart';
|
||||
import 'package:cake_wallet/themes/core/material_base_theme.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
enum FooterType { none, slideActionButton, singleActionButton, doubleActionButton }
|
||||
|
||||
abstract class BaseBottomSheet extends StatelessWidget {
|
||||
const BaseBottomSheet({
|
||||
super.key,
|
||||
required this.titleText,
|
||||
this.titleIconPath,
|
||||
required this.footerType,
|
||||
this.currentTheme,
|
||||
this.slideActionButtonText,
|
||||
this.onSlideActionComplete,
|
||||
this.singleActionButtonText,
|
||||
this.accessibleNavigationModeSlideActionButtonText,
|
||||
this.onSingleActionButtonPressed,
|
||||
this.singleActionButtonKey,
|
||||
this.doubleActionLeftButtonText,
|
||||
this.doubleActionRightButtonText,
|
||||
this.onLeftActionButtonPressed,
|
||||
this.onRightActionButtonPressed,
|
||||
this.leftActionButtonKey,
|
||||
this.rightActionButtonKey,
|
||||
}) : assert(footerType == FooterType.none || currentTheme != null,
|
||||
'currentTheme is required unless footerType is none');
|
||||
|
||||
final String titleText;
|
||||
final String? titleIconPath;
|
||||
final MaterialThemeBase? currentTheme;
|
||||
final FooterType footerType;
|
||||
final String? slideActionButtonText;
|
||||
final VoidCallback? onSlideActionComplete;
|
||||
final String? singleActionButtonText;
|
||||
final String? accessibleNavigationModeSlideActionButtonText;
|
||||
final VoidCallback? onSingleActionButtonPressed;
|
||||
final Key? singleActionButtonKey;
|
||||
final String? doubleActionLeftButtonText;
|
||||
final String? doubleActionRightButtonText;
|
||||
final VoidCallback? onLeftActionButtonPressed;
|
||||
final VoidCallback? onRightActionButtonPressed;
|
||||
final Key? leftActionButtonKey;
|
||||
final Key? rightActionButtonKey;
|
||||
|
||||
const BaseBottomSheet({required this.titleText, this.titleIconPath});
|
||||
|
||||
Widget headerWidget(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
child: Row(
|
||||
children: [
|
||||
const Spacer(flex: 4),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Container(
|
||||
height: 6,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
Widget _buildDragHandle(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
child: Row(
|
||||
children: [
|
||||
const Spacer(flex: 4),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Container(
|
||||
height: 6,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
const Spacer(flex: 4),
|
||||
],
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (titleIconPath != null)
|
||||
Image.asset(titleIconPath!, height: 24, width: 24, excludeFromSemantics: true)
|
||||
else
|
||||
Container(),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
titleText,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.titleLarge!.copyWith(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
),
|
||||
const Spacer(flex: 4),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget contentWidget(BuildContext context);
|
||||
|
||||
Widget footerWidget(BuildContext context);
|
||||
Widget contentWidget(BuildContext context);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ConstrainedBox(
|
||||
constraints: BoxConstraints(maxHeight: 600),
|
||||
constraints: BoxConstraints(maxHeight: 900),
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(30.0), topRight: Radius.circular(30.0)),
|
||||
|
@ -68,13 +81,97 @@ abstract class BaseBottomSheet extends StatelessWidget {
|
|||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
headerWidget(context),
|
||||
buildHeader(context) ?? _buildDragHandle(context),
|
||||
contentWidget(context),
|
||||
footerWidget(context),
|
||||
_buildFooter(context),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget? buildHeader(BuildContext context) => Column(
|
||||
children: [
|
||||
_buildDragHandle(context),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (titleIconPath != null) ...[
|
||||
Image.asset(titleIconPath!, height: 24, width: 24),
|
||||
const SizedBox(width: 6),
|
||||
],
|
||||
Text(
|
||||
titleText,
|
||||
style: Theme.of(context).textTheme.titleLarge!.copyWith(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 13),
|
||||
],
|
||||
);
|
||||
|
||||
Widget _buildFooter(BuildContext context) {
|
||||
switch (footerType) {
|
||||
case FooterType.none:
|
||||
return const SizedBox.shrink();
|
||||
|
||||
case FooterType.slideActionButton:
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(40, 12, 40, 34),
|
||||
child: StandardSlideButton(
|
||||
buttonText: slideActionButtonText ?? '',
|
||||
onSlideComplete: onSlideActionComplete ?? () {},
|
||||
currentTheme: currentTheme!,
|
||||
accessibleNavigationModeButtonText: accessibleNavigationModeSlideActionButtonText ?? '',
|
||||
),
|
||||
);
|
||||
|
||||
case FooterType.singleActionButton:
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 34),
|
||||
child: LoadingPrimaryButton(
|
||||
key: singleActionButtonKey,
|
||||
text: singleActionButtonText ?? '',
|
||||
onPressed: onSingleActionButtonPressed ?? () {},
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
isLoading: false,
|
||||
isDisabled: false,
|
||||
),
|
||||
);
|
||||
|
||||
case FooterType.doubleActionButton:
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 34),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: PrimaryButton(
|
||||
key: leftActionButtonKey,
|
||||
text: doubleActionLeftButtonText ?? '',
|
||||
onPressed: onLeftActionButtonPressed,
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
textColor: Theme.of(context).colorScheme.onSecondaryContainer
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: PrimaryButton(
|
||||
key: rightActionButtonKey,
|
||||
text: doubleActionRightButtonText ?? '',
|
||||
onPressed: onRightActionButtonPressed,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,37 +2,28 @@ import 'package:cake_wallet/generated/i18n.dart';
|
|||
import 'package:cake_wallet/src/widgets/standard_slide_button_widget.dart';
|
||||
import 'package:cake_wallet/themes/core/material_base_theme.dart';
|
||||
import 'package:cake_wallet/utils/address_formatter.dart';
|
||||
import 'package:cake_wallet/utils/image_utill.dart';
|
||||
import 'package:cake_wallet/view_model/cake_pay/cake_pay_buy_card_view_model.dart';
|
||||
import 'package:cake_wallet/view_model/send/output.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/pending_transaction.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
|
||||
import '../../../view_model/cake_pay/cake_pay_purchase_view_model.dart';
|
||||
import 'base_bottom_sheet_widget.dart';
|
||||
|
||||
class ConfirmSendingBottomSheet extends BaseBottomSheet {
|
||||
final CryptoCurrency currency;
|
||||
final MaterialThemeBase currentTheme;
|
||||
final String? paymentId;
|
||||
final String? paymentIdValue;
|
||||
final String? expirationTime;
|
||||
final String amount;
|
||||
final String amountValue;
|
||||
final String fiatAmountValue;
|
||||
final String fee;
|
||||
final String feeValue;
|
||||
final String feeFiatAmount;
|
||||
final List<Output> outputs;
|
||||
final VoidCallback onSlideComplete;
|
||||
final WalletType walletType;
|
||||
final PendingChange? change;
|
||||
final bool isOpenCryptoPay;
|
||||
|
||||
ConfirmSendingBottomSheet({
|
||||
required String titleText,
|
||||
required MaterialThemeBase currentTheme,
|
||||
required FooterType footerType,
|
||||
String? titleIconPath,
|
||||
String? slideActionButtonText,
|
||||
VoidCallback? onSlideActionComplete,
|
||||
String? accessibleNavigationModeSlideActionButtonText,
|
||||
required this.currency,
|
||||
required this.currentTheme,
|
||||
this.paymentId,
|
||||
this.paymentIdValue,
|
||||
this.expirationTime,
|
||||
|
@ -43,13 +34,42 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet {
|
|||
required this.feeValue,
|
||||
required this.feeFiatAmount,
|
||||
required this.outputs,
|
||||
required this.onSlideComplete,
|
||||
required this.walletType,
|
||||
this.change,
|
||||
this.isOpenCryptoPay = false,
|
||||
this.cakePayBuyCardViewModel,
|
||||
this.quantity,
|
||||
Key? key,
|
||||
}) : showScrollbar = outputs.length > 3,
|
||||
super(titleText: titleText, titleIconPath: titleIconPath);
|
||||
_currentTheme = currentTheme,
|
||||
super(
|
||||
titleText: titleText,
|
||||
titleIconPath: titleIconPath,
|
||||
currentTheme: currentTheme,
|
||||
footerType: footerType,
|
||||
slideActionButtonText: slideActionButtonText ?? 'Swipe to send',
|
||||
onSlideActionComplete: onSlideActionComplete,
|
||||
accessibleNavigationModeSlideActionButtonText:
|
||||
accessibleNavigationModeSlideActionButtonText,
|
||||
key: key);
|
||||
|
||||
final CryptoCurrency currency;
|
||||
final MaterialThemeBase _currentTheme;
|
||||
final String? paymentId;
|
||||
final String? paymentIdValue;
|
||||
final String? expirationTime;
|
||||
final String amount;
|
||||
final String amountValue;
|
||||
final String fiatAmountValue;
|
||||
final String fee;
|
||||
final String feeValue;
|
||||
final String feeFiatAmount;
|
||||
final List<Output> outputs;
|
||||
final WalletType walletType;
|
||||
final PendingChange? change;
|
||||
final bool isOpenCryptoPay;
|
||||
final CakePayPurchaseViewModel? cakePayBuyCardViewModel;
|
||||
final String? quantity;
|
||||
|
||||
final bool showScrollbar;
|
||||
final ScrollController scrollController = ScrollController();
|
||||
|
@ -57,15 +77,15 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet {
|
|||
@override
|
||||
Widget contentWidget(BuildContext context) {
|
||||
final itemTitleTextStyle = Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
decoration: TextDecoration.none,
|
||||
);
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
decoration: TextDecoration.none,
|
||||
);
|
||||
final itemSubTitleTextStyle = Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
decoration: TextDecoration.none,
|
||||
);
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
decoration: TextDecoration.none,
|
||||
);
|
||||
|
||||
final tileBackgroundColor = Theme.of(context).colorScheme.surfaceContainer;
|
||||
|
||||
|
@ -73,19 +93,24 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet {
|
|||
padding: EdgeInsets.fromLTRB(8, 0, showScrollbar ? 16 : 8, 8),
|
||||
child: Column(
|
||||
children: [
|
||||
if (paymentId != null && paymentIdValue != null)
|
||||
if (paymentId != null && paymentIdValue != null && cakePayBuyCardViewModel != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: AddressTile(
|
||||
itemTitle: paymentId!,
|
||||
itemTitleTextStyle: itemTitleTextStyle,
|
||||
walletType: walletType,
|
||||
isBatchSending: false,
|
||||
amount: '',
|
||||
address: paymentIdValue!,
|
||||
itemSubTitleTextStyle: itemSubTitleTextStyle,
|
||||
tileBackgroundColor: tileBackgroundColor,
|
||||
),
|
||||
child: Observer(
|
||||
builder: (_) => AddressTile(
|
||||
itemTitle: paymentId!,
|
||||
itemTitleTextStyle: itemTitleTextStyle,
|
||||
amountTextStyle: itemSubTitleTextStyle,
|
||||
walletType: walletType,
|
||||
amount: expirationTime != null
|
||||
? S.current.offer_expires_in +
|
||||
' ${cakePayBuyCardViewModel!.formattedRemainingTime}'
|
||||
: null,
|
||||
address: paymentIdValue!,
|
||||
itemSubTitleTextStyle: itemSubTitleTextStyle,
|
||||
tileBackgroundColor: tileBackgroundColor,
|
||||
applyAddressFormatting: false,
|
||||
)),
|
||||
),
|
||||
StandardTile(
|
||||
itemTitle: amount,
|
||||
|
@ -116,39 +141,43 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet {
|
|||
itemBuilder: (context, index) {
|
||||
final bool isBatchSending = outputs.length > 1;
|
||||
final item = outputs[index];
|
||||
final contactName = item.parsedAddress.name;
|
||||
final contactName = item.parsedAddress.handle;
|
||||
final isCakePayName = contactName == 'Cake Pay';
|
||||
final batchContactTitle =
|
||||
'${index + 1}/${outputs.length} - ${contactName.isEmpty ? 'Address' : contactName}';
|
||||
final _address = item.isParsedAddress ? item.extractedAddress : item.address;
|
||||
final _amount = item.cryptoAmount.replaceAll(',', '.') + ' ${currency.title}';
|
||||
return isBatchSending || contactName.isNotEmpty
|
||||
? AddressExpansionTile(
|
||||
contactType: isOpenCryptoPay ? 'Open CryptoPay' : S.of(context).contact,
|
||||
name: isBatchSending ? batchContactTitle : contactName,
|
||||
address: _address,
|
||||
amount: _amount,
|
||||
walletType: walletType,
|
||||
isBatchSending: isBatchSending,
|
||||
itemTitleTextStyle: itemTitleTextStyle,
|
||||
itemSubTitleTextStyle: itemSubTitleTextStyle,
|
||||
tileBackgroundColor: tileBackgroundColor,
|
||||
)
|
||||
return isBatchSending || (contactName.isNotEmpty && !isCakePayName)
|
||||
? ExpansionAddressTile(
|
||||
contactType: isOpenCryptoPay ? 'Open CryptoPay' : S.of(context).contact,
|
||||
name: isBatchSending ? batchContactTitle : contactName,
|
||||
address: _address,
|
||||
amount: _amount,
|
||||
walletType: walletType,
|
||||
isBatchSending: isBatchSending,
|
||||
itemTitleTextStyle: itemTitleTextStyle,
|
||||
itemSubTitleTextStyle: itemSubTitleTextStyle,
|
||||
tileBackgroundColor: tileBackgroundColor,
|
||||
)
|
||||
: AddressTile(
|
||||
itemTitle: S.of(context).address,
|
||||
itemTitleTextStyle: itemTitleTextStyle,
|
||||
isBatchSending: isBatchSending,
|
||||
walletType: walletType,
|
||||
amount: _amount,
|
||||
address: _address,
|
||||
itemSubTitleTextStyle: itemSubTitleTextStyle,
|
||||
tileBackgroundColor: tileBackgroundColor,
|
||||
);
|
||||
itemTitle: isCakePayName
|
||||
? item.parsedAddress.profileName
|
||||
: S.of(context).address,
|
||||
imagePath: isCakePayName ? item.parsedAddress.profileImageUrl : null,
|
||||
itemTitleTextStyle: itemTitleTextStyle,
|
||||
walletType: walletType,
|
||||
amount: isCakePayName ? item.fiatAmount : _amount,
|
||||
address: _address,
|
||||
itemSubTitle: isCakePayName ? quantity : null,
|
||||
itemSubTitleTextStyle: itemSubTitleTextStyle,
|
||||
tileBackgroundColor: tileBackgroundColor,
|
||||
);
|
||||
},
|
||||
),
|
||||
if (change != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: AddressExpansionTile(
|
||||
child: ExpansionAddressTile(
|
||||
contactType: 'Change',
|
||||
name: S.of(context).send_change_to_you,
|
||||
address: change!.address,
|
||||
|
@ -184,31 +213,6 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet {
|
|||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget footerWidget(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.fromLTRB(40, 12, 40, 34),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
boxShadow: [
|
||||
if (showScrollbar)
|
||||
BoxShadow(
|
||||
color: Theme.of(context).colorScheme.outlineVariant,
|
||||
spreadRadius: 2,
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 0),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: StandardSlideButton(
|
||||
onSlideComplete: onSlideComplete,
|
||||
buttonText: 'Swipe to send',
|
||||
currentTheme: currentTheme,
|
||||
accessibleNavigationModeButtonText: S.of(context).send,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class StandardTile extends StatelessWidget {
|
||||
|
@ -265,22 +269,28 @@ class AddressTile extends StatelessWidget {
|
|||
super.key,
|
||||
required this.itemTitle,
|
||||
required this.itemTitleTextStyle,
|
||||
required this.isBatchSending,
|
||||
required this.amount,
|
||||
required this.address,
|
||||
required this.itemSubTitleTextStyle,
|
||||
required this.tileBackgroundColor,
|
||||
required this.walletType,
|
||||
this.amountTextStyle,
|
||||
this.applyAddressFormatting = true,
|
||||
this.imagePath,
|
||||
this.amount,
|
||||
this.itemSubTitle,
|
||||
});
|
||||
|
||||
final String itemTitle;
|
||||
final TextStyle itemTitleTextStyle;
|
||||
final bool isBatchSending;
|
||||
final String amount;
|
||||
final String? amount;
|
||||
final String address;
|
||||
final TextStyle itemSubTitleTextStyle;
|
||||
final TextStyle? amountTextStyle;
|
||||
final Color tileBackgroundColor;
|
||||
final WalletType walletType;
|
||||
final bool applyAddressFormatting;
|
||||
final String? imagePath;
|
||||
final String? itemSubTitle;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -296,17 +306,56 @@ class AddressTile extends StatelessWidget {
|
|||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(itemTitle, style: itemTitleTextStyle),
|
||||
if (isBatchSending) Text(amount, style: itemTitleTextStyle),
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
if (imagePath != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: ImageUtil.getImageFromPath(
|
||||
imagePath: imagePath!, height: 40, width: 40),
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: Text(
|
||||
itemTitle,
|
||||
style: itemTitleTextStyle,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
softWrap: false,
|
||||
)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (amount != null) Text(amount!, style: amountTextStyle ?? itemTitleTextStyle),
|
||||
],
|
||||
),
|
||||
AddressFormatter.buildSegmentedAddress(
|
||||
address: address,
|
||||
walletType: walletType,
|
||||
evenTextStyle: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
address.isEmpty
|
||||
? Container()
|
||||
: applyAddressFormatting
|
||||
? AddressFormatter.buildSegmentedAddress(
|
||||
address: address,
|
||||
walletType: walletType,
|
||||
evenTextStyle: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
decoration: TextDecoration.none,
|
||||
))
|
||||
: Text(
|
||||
address,
|
||||
style: itemTitleTextStyle,
|
||||
),
|
||||
itemSubTitle == null
|
||||
? Container()
|
||||
: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Text(itemSubTitle!, style: itemSubTitleTextStyle),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -314,8 +363,8 @@ class AddressTile extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
class AddressExpansionTile extends StatelessWidget {
|
||||
const AddressExpansionTile({
|
||||
class ExpansionAddressTile extends StatelessWidget {
|
||||
const ExpansionAddressTile({
|
||||
super.key,
|
||||
required this.contactType,
|
||||
required this.name,
|
||||
|
@ -371,9 +420,9 @@ class AddressExpansionTile extends StatelessWidget {
|
|||
Text(
|
||||
isBatchSending ? amount : name,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
fontWeight: FontWeight.w600,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -385,9 +434,8 @@ class AddressExpansionTile extends StatelessWidget {
|
|||
address: address,
|
||||
walletType: walletType,
|
||||
evenTextStyle: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
@ -1,65 +1,84 @@
|
|||
import 'package:auto_size_text/auto_size_text.dart';
|
||||
|
||||
|
||||
import 'package:cake_wallet/utils/image_utill.dart';
|
||||
|
||||
import 'package:cake_wallet/src/widgets/primary_button.dart';
|
||||
import 'package:cake_wallet/themes/core/material_base_theme.dart';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
|
||||
import 'base_bottom_sheet_widget.dart';
|
||||
|
||||
class LoadingBottomSheet extends BaseBottomSheet {
|
||||
LoadingBottomSheet({required String titleText, String? titleIconPath})
|
||||
: super(titleText: titleText, titleIconPath: titleIconPath);
|
||||
LoadingBottomSheet(
|
||||
{required String titleText, String? titleIconPath})
|
||||
: super(titleText: titleText, titleIconPath: titleIconPath, footerType: FooterType.none);
|
||||
|
||||
@override
|
||||
Widget contentWidget(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 200,
|
||||
height: 300,
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget footerWidget(BuildContext context) => const SizedBox(height: 94);
|
||||
}
|
||||
|
||||
class InfoBottomSheet extends BaseBottomSheet {
|
||||
final MaterialThemeBase currentTheme;
|
||||
final String? contentImage;
|
||||
final Color? contentImageColor;
|
||||
final String? content;
|
||||
final bool isTwoAction;
|
||||
final bool showDontAskMeCheckbox;
|
||||
final Function(bool)? onCheckboxChanged;
|
||||
final String? actionButtonText;
|
||||
final VoidCallback? actionButton;
|
||||
final Key? actionButtonKey;
|
||||
final String? leftButtonText;
|
||||
final String? rightButtonText;
|
||||
final VoidCallback? actionLeftButton;
|
||||
final VoidCallback? actionRightButton;
|
||||
final Key? rightActionButtonKey;
|
||||
final Key? leftActionButtonKey;
|
||||
|
||||
InfoBottomSheet({
|
||||
required String titleText,
|
||||
String? titleIconPath,
|
||||
required this.currentTheme,
|
||||
required this.footerType,
|
||||
this.contentImage,
|
||||
this.contentImageColor,
|
||||
this.content,
|
||||
this.isTwoAction = false,
|
||||
this.showDontAskMeCheckbox = false,
|
||||
this.onCheckboxChanged,
|
||||
this.actionButtonText,
|
||||
this.actionButton,
|
||||
this.actionButtonKey,
|
||||
this.leftButtonText,
|
||||
this.rightButtonText,
|
||||
this.actionLeftButton,
|
||||
this.actionRightButton,
|
||||
this.rightActionButtonKey,
|
||||
this.singleActionButtonText,
|
||||
this.onSingleActionButtonPressed,
|
||||
this.singleActionButtonKey,
|
||||
this.doubleActionLeftButtonText,
|
||||
this.doubleActionRightButtonText,
|
||||
this.onLeftActionButtonPressed,
|
||||
this.onRightActionButtonPressed,
|
||||
this.leftActionButtonKey,
|
||||
}) : super(titleText: titleText, titleIconPath: titleIconPath);
|
||||
this.rightActionButtonKey,
|
||||
Widget? actionPanel,
|
||||
Key? key,
|
||||
}) : _actionPanel = actionPanel,
|
||||
super(
|
||||
titleText: titleText,
|
||||
titleIconPath: titleIconPath,
|
||||
currentTheme: currentTheme,
|
||||
footerType: footerType,
|
||||
singleActionButtonText: singleActionButtonText,
|
||||
onSingleActionButtonPressed: onSingleActionButtonPressed,
|
||||
singleActionButtonKey: singleActionButtonKey,
|
||||
doubleActionLeftButtonText: doubleActionLeftButtonText,
|
||||
doubleActionRightButtonText: doubleActionRightButtonText,
|
||||
onLeftActionButtonPressed: onLeftActionButtonPressed,
|
||||
onRightActionButtonPressed: onRightActionButtonPressed,
|
||||
leftActionButtonKey: leftActionButtonKey,
|
||||
rightActionButtonKey: rightActionButtonKey,
|
||||
key: key);
|
||||
|
||||
final MaterialThemeBase currentTheme;
|
||||
final FooterType footerType;
|
||||
final String? contentImage;
|
||||
final Color? contentImageColor;
|
||||
final String? content;
|
||||
final Widget? _actionPanel;
|
||||
final String? singleActionButtonText;
|
||||
final VoidCallback? onSingleActionButtonPressed;
|
||||
final Key? singleActionButtonKey;
|
||||
final String? doubleActionLeftButtonText;
|
||||
final String? doubleActionRightButtonText;
|
||||
final VoidCallback? onLeftActionButtonPressed;
|
||||
final VoidCallback? onRightActionButtonPressed;
|
||||
final Key? rightActionButtonKey;
|
||||
final Key? leftActionButtonKey;
|
||||
|
||||
Widget get defaultActionPanel => const SizedBox();
|
||||
Widget get actionPanel => _actionPanel ?? defaultActionPanel;
|
||||
|
||||
@override
|
||||
Widget contentWidget(BuildContext context) {
|
||||
|
@ -70,7 +89,12 @@ class InfoBottomSheet extends BaseBottomSheet {
|
|||
if (contentImage != null)
|
||||
Expanded(
|
||||
flex: 4,
|
||||
child: getImage(contentImage!, imageColor: contentImageColor),
|
||||
child: ImageUtil.getImageFromPath(
|
||||
imagePath: contentImage!,
|
||||
svgImageColor: contentImageColor,
|
||||
fit: BoxFit.contain,
|
||||
borderRadius: 10,
|
||||
),
|
||||
)
|
||||
else
|
||||
Container(),
|
||||
|
@ -87,134 +111,20 @@ class InfoBottomSheet extends BaseBottomSheet {
|
|||
content!,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Spacer(flex: 2),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (showDontAskMeCheckbox)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 34.0),
|
||||
child: Row(
|
||||
children: [
|
||||
SimpleCheckbox(onChanged: onCheckboxChanged),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Don’t ask me next time',
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actionPanel ?? const SizedBox(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget footerWidget(BuildContext context) {
|
||||
if (isTwoAction) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 34),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(right: 8.0, top: 8.0),
|
||||
child: PrimaryButton(
|
||||
key: leftActionButtonKey,
|
||||
onPressed: actionLeftButton,
|
||||
text: leftButtonText ?? '',
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
textColor: Theme.of(context).colorScheme.onSecondaryContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(left: 8.0, top: 8.0),
|
||||
child: PrimaryButton(
|
||||
key: rightActionButtonKey,
|
||||
onPressed: actionRightButton,
|
||||
text: rightButtonText ?? '',
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 34),
|
||||
child: LoadingPrimaryButton(
|
||||
key: actionButtonKey,
|
||||
onPressed: actionButton ?? () {},
|
||||
text: actionButtonText ?? '',
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
isLoading: false,
|
||||
isDisabled: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget getImage(String imagePath, {Color? imageColor}) {
|
||||
final bool isSvg = imagePath.endsWith('.svg');
|
||||
if (isSvg) {
|
||||
return SvgPicture.asset(
|
||||
imagePath,
|
||||
colorFilter: imageColor != null ? ColorFilter.mode(imageColor, BlendMode.srcIn) : null,
|
||||
);
|
||||
} else {
|
||||
return Image.asset(imagePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SimpleCheckbox extends StatefulWidget {
|
||||
SimpleCheckbox({this.onChanged});
|
||||
|
||||
final Function(bool)? onChanged;
|
||||
|
||||
@override
|
||||
State<SimpleCheckbox> createState() => _SimpleCheckboxState();
|
||||
}
|
||||
|
||||
class _SimpleCheckboxState extends State<SimpleCheckbox> {
|
||||
bool initialValue = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 24.0,
|
||||
width: 24.0,
|
||||
child: Checkbox(
|
||||
value: initialValue,
|
||||
onChanged: (value) => setState(() {
|
||||
initialValue = value!;
|
||||
widget.onChanged?.call(value);
|
||||
}),
|
||||
checkColor: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
activeColor: Colors.transparent,
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
side: WidgetStateBorderSide.resolveWith((states) =>
|
||||
BorderSide(color: Theme.of(context).colorScheme.onSurfaceVariant, width: 1.0)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -21,7 +21,7 @@ class InfoStepsBottomSheet extends BaseBottomSheet {
|
|||
required this.steps,
|
||||
String? titleIconPath,
|
||||
required this.currentTheme,
|
||||
}) : super(titleText: titleText, titleIconPath: titleIconPath);
|
||||
}) : super(titleText: titleText, titleIconPath: titleIconPath, footerType: FooterType.none);
|
||||
|
||||
@override
|
||||
Widget contentWidget(BuildContext context) => SizedBox(
|
||||
|
@ -108,7 +108,4 @@ class InfoStepsBottomSheet extends BaseBottomSheet {
|
|||
],
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
Widget footerWidget(BuildContext context) => SizedBox.shrink();
|
||||
}
|
||||
|
|
|
@ -79,6 +79,8 @@ class LoadingPrimaryButton extends StatelessWidget {
|
|||
required this.text,
|
||||
required this.color,
|
||||
required this.textColor,
|
||||
this.width,
|
||||
this.height,
|
||||
this.isDisabled = false,
|
||||
this.isLoading = false,
|
||||
super.key,
|
||||
|
@ -87,6 +89,8 @@ class LoadingPrimaryButton extends StatelessWidget {
|
|||
final VoidCallback onPressed;
|
||||
final Color color;
|
||||
final Color textColor;
|
||||
final double? width;
|
||||
final double? height;
|
||||
final bool isLoading;
|
||||
final bool isDisabled;
|
||||
final String text;
|
||||
|
@ -96,8 +100,8 @@ class LoadingPrimaryButton extends StatelessWidget {
|
|||
return ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
height: 52.0,
|
||||
width: width ?? double.infinity,
|
||||
height: height ?? 52.0,
|
||||
child: TextButton(
|
||||
onPressed: (isLoading || isDisabled)
|
||||
? null
|
||||
|
|
34
lib/src/widgets/simple_checkbox.dart
Normal file
|
@ -0,0 +1,34 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class SimpleCheckbox extends StatefulWidget {
|
||||
SimpleCheckbox({this.onChanged});
|
||||
|
||||
final Function(bool)? onChanged;
|
||||
|
||||
@override
|
||||
State<SimpleCheckbox> createState() => _SimpleCheckboxState();
|
||||
}
|
||||
|
||||
class _SimpleCheckboxState extends State<SimpleCheckbox> {
|
||||
bool initialValue = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 24.0,
|
||||
width: 24.0,
|
||||
child: Checkbox(
|
||||
value: initialValue,
|
||||
onChanged: (value) => setState(() {
|
||||
initialValue = value!;
|
||||
widget.onChanged?.call(value);
|
||||
}),
|
||||
checkColor: Theme.of(context).textTheme.titleLarge!.color,
|
||||
activeColor: Colors.transparent,
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
side: WidgetStateBorderSide.resolveWith((states) => BorderSide(
|
||||
color: Theme.of(context).textTheme.titleLarge!.color!, width: 1.0)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -118,6 +118,10 @@ abstract class SettingsStoreBase with Store {
|
|||
required this.lookupsOpenAlias,
|
||||
required this.lookupsENS,
|
||||
required this.lookupsWellKnown,
|
||||
required this.lookupsFio,
|
||||
required this.lookupsNostr,
|
||||
required this.lookupsThorChain,
|
||||
required this.lookupsBip353,
|
||||
required this.usePayjoin,
|
||||
required this.showPayjoinCard,
|
||||
required this.customBitcoinFeeRate,
|
||||
|
@ -488,6 +492,26 @@ abstract class SettingsStoreBase with Store {
|
|||
(bool looksUpWellKnown) =>
|
||||
_sharedPreferences.setBool(PreferencesKey.lookupsWellKnown, looksUpWellKnown));
|
||||
|
||||
reaction(
|
||||
(_) => lookupsFio,
|
||||
(bool looksUpWellKnown) =>
|
||||
_sharedPreferences.setBool(PreferencesKey.lookupsWellKnown, looksUpWellKnown));
|
||||
|
||||
reaction(
|
||||
(_) => lookupsNostr,
|
||||
(bool looksUpWellKnown) =>
|
||||
_sharedPreferences.setBool(PreferencesKey.lookupsWellKnown, looksUpWellKnown));
|
||||
|
||||
reaction(
|
||||
(_) => lookupsThorChain,
|
||||
(bool looksUpWellKnown) =>
|
||||
_sharedPreferences.setBool(PreferencesKey.lookupsWellKnown, looksUpWellKnown));
|
||||
|
||||
reaction(
|
||||
(_) => lookupsBip353,
|
||||
(bool looksUpWellKnown) =>
|
||||
_sharedPreferences.setBool(PreferencesKey.lookupsWellKnown, looksUpWellKnown));
|
||||
|
||||
reaction(
|
||||
(_) => usePayjoin,
|
||||
(bool usePayjoin) =>
|
||||
|
@ -816,6 +840,18 @@ abstract class SettingsStoreBase with Store {
|
|||
@observable
|
||||
bool lookupsWellKnown;
|
||||
|
||||
@observable
|
||||
bool lookupsFio;
|
||||
|
||||
@observable
|
||||
bool lookupsNostr;
|
||||
|
||||
@observable
|
||||
bool lookupsThorChain;
|
||||
|
||||
@observable
|
||||
bool lookupsBip353;
|
||||
|
||||
@observable
|
||||
bool usePayjoin;
|
||||
|
||||
|
@ -1022,6 +1058,10 @@ abstract class SettingsStoreBase with Store {
|
|||
final lookupsOpenAlias = sharedPreferences.getBool(PreferencesKey.lookupsOpenAlias) ?? true;
|
||||
final lookupsENS = sharedPreferences.getBool(PreferencesKey.lookupsENS) ?? true;
|
||||
final lookupsWellKnown = sharedPreferences.getBool(PreferencesKey.lookupsWellKnown) ?? true;
|
||||
final lookupsFio = sharedPreferences.getBool(PreferencesKey.lookupsFio) ?? true;
|
||||
final lookupsNostr = sharedPreferences.getBool(PreferencesKey.lookupsNostr) ?? true;
|
||||
final lookupsThorChain = sharedPreferences.getBool(PreferencesKey.lookupsThorChain) ?? true;
|
||||
final lookupsBip353 = sharedPreferences.getBool(PreferencesKey.lookupsBip353) ?? true;
|
||||
final usePayjoin = sharedPreferences.getBool(PreferencesKey.usePayjoin) ?? false;
|
||||
final showPayjoinCard = sharedPreferences.getBool(PreferencesKey.showPayjoinCard) ?? true;
|
||||
final customBitcoinFeeRate = sharedPreferences.getInt(PreferencesKey.customBitcoinFeeRate) ?? 1;
|
||||
|
@ -1327,6 +1367,10 @@ abstract class SettingsStoreBase with Store {
|
|||
lookupsOpenAlias: lookupsOpenAlias,
|
||||
lookupsENS: lookupsENS,
|
||||
lookupsWellKnown: lookupsWellKnown,
|
||||
lookupsFio: lookupsFio,
|
||||
lookupsNostr: lookupsNostr,
|
||||
lookupsThorChain: lookupsThorChain,
|
||||
lookupsBip353: lookupsBip353,
|
||||
usePayjoin: usePayjoin,
|
||||
showPayjoinCard: showPayjoinCard,
|
||||
customBitcoinFeeRate: customBitcoinFeeRate,
|
||||
|
|
|
@ -2,6 +2,7 @@ import 'dart:convert';
|
|||
|
||||
import 'package:cake_wallet/.secrets.g.dart' as secrets;
|
||||
import 'package:cake_wallet/twitter/twitter_user.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:cw_core/utils/proxy_wrapper.dart';
|
||||
|
||||
class TwitterApi {
|
||||
|
@ -10,7 +11,9 @@ class TwitterApi {
|
|||
static const apiHost = 'api.twitter.com';
|
||||
static const userPath = '/2/users/by/username/';
|
||||
|
||||
static Future<TwitterUser> lookupUserByName({required String userName}) async {
|
||||
static Future<TwitterUser?> lookupUserByName({required String userName}) async {
|
||||
|
||||
try{
|
||||
final queryParams = {
|
||||
'user.fields': 'description,profile_image_url',
|
||||
'expansions': 'pinned_tweet_id',
|
||||
|
@ -41,8 +44,14 @@ class TwitterApi {
|
|||
}
|
||||
|
||||
return TwitterUser.fromJson(responseJSON, _getPinnedTweet(responseJSON));
|
||||
|
||||
} catch (e) {
|
||||
printV('Error in lookupUserByName: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static Tweet? _getPinnedTweet(Map<String, dynamic> responseJSON) {
|
||||
try {
|
||||
final tweetId = responseJSON['data']['pinned_tweet_id'] as String?;
|
||||
|
@ -61,6 +70,7 @@ class TwitterApi {
|
|||
|
||||
return Tweet(id: tweetId, text: pinnedTweetText);
|
||||
} catch (e) {
|
||||
printV('Error in _getPinnedTweet: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,8 +10,8 @@ class AddressFormatter {
|
|||
TextStyle? oddTextStyle,
|
||||
TextAlign? textAlign,
|
||||
bool shouldTruncate = false,
|
||||
int visibleChunks = 2,
|
||||
}) {
|
||||
|
||||
final cleanAddress = address.replaceAll('bitcoincash:', '');
|
||||
final isMWEB = address.startsWith('ltcmweb');
|
||||
final chunkSize = walletType != null ? _getChunkSize(walletType) : 4;
|
||||
|
@ -21,8 +21,10 @@ class AddressFormatter {
|
|||
address: cleanAddress,
|
||||
isMWEB: isMWEB,
|
||||
chunkSize: chunkSize,
|
||||
visibleChunks: visibleChunks,
|
||||
evenTextStyle: evenTextStyle,
|
||||
oddTextStyle: oddTextStyle ?? evenTextStyle.copyWith(color: evenTextStyle.color!.withAlpha(150)),
|
||||
oddTextStyle:
|
||||
oddTextStyle ?? evenTextStyle.copyWith(color: evenTextStyle.color!.withAlpha(150)),
|
||||
textAlign: textAlign,
|
||||
);
|
||||
} else {
|
||||
|
@ -31,7 +33,8 @@ class AddressFormatter {
|
|||
isMWEB: isMWEB,
|
||||
chunkSize: chunkSize,
|
||||
evenTextStyle: evenTextStyle,
|
||||
oddTextStyle: oddTextStyle ?? evenTextStyle.copyWith(color: evenTextStyle.color!.withAlpha(128)),
|
||||
oddTextStyle:
|
||||
oddTextStyle ?? evenTextStyle.copyWith(color: evenTextStyle.color!.withAlpha(128)),
|
||||
textAlign: textAlign,
|
||||
);
|
||||
}
|
||||
|
@ -45,7 +48,6 @@ class AddressFormatter {
|
|||
required TextStyle oddTextStyle,
|
||||
TextAlign? textAlign,
|
||||
}) {
|
||||
|
||||
final chunks = <String>[];
|
||||
|
||||
if (isMWEB) {
|
||||
|
@ -86,40 +88,24 @@ class AddressFormatter {
|
|||
required String address,
|
||||
required bool isMWEB,
|
||||
required int chunkSize,
|
||||
required int visibleChunks,
|
||||
required TextStyle evenTextStyle,
|
||||
required TextStyle oddTextStyle,
|
||||
TextAlign? textAlign,
|
||||
}) {
|
||||
|
||||
if (isMWEB) {
|
||||
const fixedPrefix = 'ltcmweb';
|
||||
final secondChunkStart = fixedPrefix.length;
|
||||
const chunkSize = 4;
|
||||
final secondChunk = address.substring(
|
||||
secondChunkStart,
|
||||
math.min(secondChunkStart + chunkSize, address.length),
|
||||
);
|
||||
final lastChunk = address.substring(address.length - chunkSize);
|
||||
const prefix = 'ltcmweb';
|
||||
final rest = address.substring(prefix.length);
|
||||
|
||||
final spans = <TextSpan>[
|
||||
TextSpan(text: '$fixedPrefix ', style: evenTextStyle),
|
||||
TextSpan(text: '$secondChunk ', style: oddTextStyle),
|
||||
TextSpan(text: '... ', style: oddTextStyle),
|
||||
TextSpan(text: lastChunk, style: evenTextStyle),
|
||||
];
|
||||
final chunks = <String>[];
|
||||
for (int i = 0; i < rest.length; i += chunkSize) {
|
||||
chunks.add(rest.substring(i, math.min(i + chunkSize, rest.length)));
|
||||
}
|
||||
|
||||
return RichText(
|
||||
text: TextSpan(children: spans),
|
||||
textAlign: textAlign ?? TextAlign.start,
|
||||
overflow: TextOverflow.visible,
|
||||
);
|
||||
} else {
|
||||
final int digitCount = chunkSize;
|
||||
|
||||
if (address.length <= 2 * digitCount) {
|
||||
if (chunks.length <= visibleChunks + 1) {
|
||||
return _buildFullSegmentedAddress(
|
||||
address: address,
|
||||
isMWEB: isMWEB,
|
||||
isMWEB: true,
|
||||
chunkSize: chunkSize,
|
||||
evenTextStyle: evenTextStyle,
|
||||
oddTextStyle: oddTextStyle,
|
||||
|
@ -127,25 +113,60 @@ class AddressFormatter {
|
|||
);
|
||||
}
|
||||
|
||||
final String firstPart = address.substring(0, digitCount);
|
||||
final String secondPart =
|
||||
address.substring(digitCount, digitCount * 2);
|
||||
final String lastPart =
|
||||
address.substring(address.length - digitCount);
|
||||
|
||||
final spans = <TextSpan>[
|
||||
TextSpan(text: '$firstPart ', style: evenTextStyle),
|
||||
TextSpan(text: '$secondPart ', style: oddTextStyle),
|
||||
TextSpan(text: '... ', style: oddTextStyle),
|
||||
TextSpan(text: lastPart, style: evenTextStyle),
|
||||
TextSpan(text: '$prefix ', style: evenTextStyle),
|
||||
];
|
||||
|
||||
for (var i = 0; i < visibleChunks; i++) {
|
||||
final style = (i.isEven) ? oddTextStyle : evenTextStyle;
|
||||
spans.add(TextSpan(text: '${chunks[i]} ', style: style));
|
||||
}
|
||||
|
||||
spans.add(TextSpan(text: '... ', style: oddTextStyle));
|
||||
|
||||
final lastStyle = (visibleChunks.isEven) ? evenTextStyle : oddTextStyle;
|
||||
spans.add(TextSpan(text: chunks.last, style: lastStyle));
|
||||
|
||||
return RichText(
|
||||
text: TextSpan(children: spans),
|
||||
textAlign: textAlign ?? TextAlign.start,
|
||||
overflow: TextOverflow.visible,
|
||||
);
|
||||
}
|
||||
|
||||
final chunks = <String>[];
|
||||
for (int i = 0; i < address.length; i += chunkSize) {
|
||||
chunks.add(address.substring(i, math.min(i + chunkSize, address.length)));
|
||||
}
|
||||
|
||||
if (chunks.length <= visibleChunks + 1) {
|
||||
return _buildFullSegmentedAddress(
|
||||
address: address,
|
||||
isMWEB: false,
|
||||
chunkSize: chunkSize,
|
||||
evenTextStyle: evenTextStyle,
|
||||
oddTextStyle: oddTextStyle,
|
||||
textAlign: textAlign,
|
||||
);
|
||||
}
|
||||
|
||||
final spans = <TextSpan>[];
|
||||
|
||||
for (var i = 0; i < visibleChunks; i++) {
|
||||
final style = (i.isEven) ? evenTextStyle : oddTextStyle;
|
||||
spans.add(TextSpan(text: '${chunks[i]} ', style: style));
|
||||
}
|
||||
|
||||
spans.add(TextSpan(text: '... ', style: oddTextStyle));
|
||||
|
||||
final lastStyle = (visibleChunks.isEven) ? oddTextStyle : evenTextStyle;
|
||||
spans.add(TextSpan(text: chunks.last, style: lastStyle));
|
||||
|
||||
return RichText(
|
||||
text: TextSpan(children: spans),
|
||||
textAlign: textAlign ?? TextAlign.start,
|
||||
overflow: TextOverflow.visible,
|
||||
);
|
||||
}
|
||||
|
||||
static int _getChunkSize(WalletType walletType) {
|
||||
|
|
|
@ -1,78 +1,126 @@
|
|||
import 'package:cw_core/utils/proxy_wrapper.dart';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:cw_core/utils/proxy_wrapper.dart';
|
||||
|
||||
class ImageUtil {
|
||||
static Widget getImageFromPath({required String imagePath, double? height, double? width}) {
|
||||
static Widget getImageFromPath({
|
||||
required String imagePath,
|
||||
double? height,
|
||||
double? width,
|
||||
Color? svgImageColor,
|
||||
BoxFit? fit,
|
||||
double? borderRadius,
|
||||
}) {
|
||||
bool isNetworkImage = imagePath.startsWith('http') || imagePath.startsWith('https');
|
||||
|
||||
if (CakeTor.instance.enabled && isNetworkImage) {
|
||||
imagePath = "assets/images/tor_logo.svg";
|
||||
isNetworkImage = false;
|
||||
}
|
||||
|
||||
final bool isSvg = imagePath.endsWith('.svg');
|
||||
final double _height = height ?? 35;
|
||||
final double _width = width ?? 35;
|
||||
final bool ignoreSize = fit != null;
|
||||
final double? _height = ignoreSize ? null : (height ?? 35);
|
||||
final double? _width = ignoreSize ? null : (width ?? 35);
|
||||
|
||||
Widget img;
|
||||
|
||||
if (isNetworkImage) {
|
||||
return isSvg
|
||||
img = isSvg
|
||||
? SvgPicture.network(
|
||||
key: ValueKey(imagePath),
|
||||
imagePath,
|
||||
key: ValueKey(imagePath),
|
||||
height: _height,
|
||||
width: _width,
|
||||
placeholderBuilder: (BuildContext context) => Container(
|
||||
height: _height,
|
||||
width: _width,
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
),
|
||||
fit: fit ?? BoxFit.contain,
|
||||
placeholderBuilder: (_) => _placeholder(_height, _width),
|
||||
)
|
||||
: Image.network(
|
||||
key: ValueKey(imagePath),
|
||||
imagePath,
|
||||
key: ValueKey(imagePath),
|
||||
height: _height,
|
||||
width: _width,
|
||||
loadingBuilder:
|
||||
(BuildContext context, Widget child, ImageChunkEvent? loadingProgress) {
|
||||
if (loadingProgress == null) {
|
||||
return child;
|
||||
}
|
||||
return Container(
|
||||
height: _height,
|
||||
width: _width,
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(
|
||||
value: loadingProgress.expectedTotalBytes != null
|
||||
? loadingProgress.cumulativeBytesLoaded /
|
||||
loadingProgress.expectedTotalBytes!
|
||||
: null,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
errorBuilder: (BuildContext context, Object exception, StackTrace? stackTrace) {
|
||||
return Container(
|
||||
height: _height,
|
||||
width: _width,
|
||||
);
|
||||
},
|
||||
fit: fit,
|
||||
loadingBuilder: (_, child, progress) =>
|
||||
progress == null ? child : _placeholder(_height, _width),
|
||||
errorBuilder: (_, __, ___) => const SizedBox.shrink(),
|
||||
);
|
||||
} else {
|
||||
return isSvg
|
||||
img = isSvg
|
||||
? SvgPicture.asset(
|
||||
imagePath,
|
||||
key: ValueKey(imagePath),
|
||||
height: _height,
|
||||
width: _width,
|
||||
placeholderBuilder: (_) => Icon(Icons.error),
|
||||
key: ValueKey(imagePath),
|
||||
fit: fit ?? BoxFit.contain,
|
||||
colorFilter:
|
||||
svgImageColor != null ? ColorFilter.mode(svgImageColor, BlendMode.srcIn) : null,
|
||||
placeholderBuilder: (_) => const Icon(Icons.error),
|
||||
)
|
||||
: Image.asset(
|
||||
imagePath,
|
||||
key: ValueKey(imagePath),
|
||||
height: _height,
|
||||
width: _width,
|
||||
errorBuilder: (_, __, ___) => Icon(Icons.error),
|
||||
key: ValueKey(imagePath),
|
||||
fit: fit,
|
||||
errorBuilder: (_, __, ___) => const Icon(Icons.error),
|
||||
);
|
||||
}
|
||||
|
||||
if (borderRadius != null && borderRadius > 0) {
|
||||
img = ClipRRect(
|
||||
borderRadius: BorderRadius.circular(borderRadius),
|
||||
child: img,
|
||||
);
|
||||
}
|
||||
|
||||
return img;
|
||||
}
|
||||
|
||||
static Widget _placeholder(double? height, double? width) {
|
||||
return (height != null || width != null)
|
||||
? SizedBox(
|
||||
height: height,
|
||||
width: width,
|
||||
child: const Center(child: CircularProgressIndicator()),
|
||||
)
|
||||
: const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
static Future<String?> saveAvatarLocally(String imageUriOrPath) async {
|
||||
if (imageUriOrPath.isEmpty) return null;
|
||||
|
||||
try {
|
||||
final dir = await getApplicationDocumentsDirectory();
|
||||
String ext = p.extension(imageUriOrPath);
|
||||
if (ext.isEmpty) ext = '.png';
|
||||
|
||||
final file = File('${dir.path}/${DateTime.now().millisecondsSinceEpoch}$ext');
|
||||
|
||||
if (imageUriOrPath.startsWith('http')) {
|
||||
final response = await ProxyWrapper()
|
||||
.get(
|
||||
clearnetUri: Uri.parse(imageUriOrPath),
|
||||
)
|
||||
.catchError((error) {
|
||||
throw Exception('HTTP request failed: $error');
|
||||
});
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
await file.writeAsBytes(response.bodyBytes);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
await File(imageUriOrPath).copy(file.path);
|
||||
}
|
||||
|
||||
return file.existsSync() ? file.path : null;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import 'package:cake_wallet/generated/i18n.dart';
|
|||
import 'package:cake_wallet/store/settings_store.dart';
|
||||
import 'package:cake_wallet/utils/mobx.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/wallet_base.dart';
|
||||
import 'package:cw_core/wallet_info.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
|
@ -17,11 +18,11 @@ import 'package:mobx/mobx.dart';
|
|||
|
||||
part 'contact_list_view_model.g.dart';
|
||||
|
||||
class ContactListViewModel = ContactListViewModelBase with _$ContactListViewModel;
|
||||
class ContactListViewModel = ContactListViewModelBase with _$ContactListViewModel;
|
||||
|
||||
abstract class ContactListViewModelBase with Store {
|
||||
ContactListViewModelBase(
|
||||
this.contactSource, this.walletInfoSource, this._currency, this.settingsStore)
|
||||
this.contactSource, this.walletInfoSource,this.wallet, this._currency, this.settingsStore)
|
||||
: contacts = ObservableList<ContactRecord>(),
|
||||
walletContacts = [],
|
||||
isAutoGenerateEnabled =
|
||||
|
@ -100,6 +101,7 @@ abstract class ContactListViewModelBase with Store {
|
|||
final ObservableList<ContactRecord> contacts;
|
||||
final List<WalletContact> walletContacts;
|
||||
final CryptoCurrency? _currency;
|
||||
final WalletBase wallet;
|
||||
StreamSubscription<BoxEvent>? _subscription;
|
||||
final SettingsStore settingsStore;
|
||||
|
||||
|
@ -152,9 +154,9 @@ abstract class ContactListViewModelBase with Store {
|
|||
Future<void> sortGroupByType() async {
|
||||
List<Contact> contactsSourceCopy = contactSource.values.toList();
|
||||
|
||||
contactsSourceCopy.sort((a, b) => ascending
|
||||
? a.type.toString().compareTo(b.type.toString())
|
||||
: b.type.toString().compareTo(a.type.toString()));
|
||||
// contactsSourceCopy.sort((a, b) => ascending //TODO fix sort by type
|
||||
// ? a.type.toString().compareTo(b.type.toString())
|
||||
// : b.type.toString().compareTo(a.type.toString()));
|
||||
|
||||
await reorderContacts(contactsSourceCopy);
|
||||
}
|
||||
|
|
|
@ -1,90 +1,328 @@
|
|||
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/parsed_address.dart';
|
||||
import 'package:cake_wallet/src/screens/address_book/entities/address_edit_request.dart';
|
||||
import 'package:cake_wallet/src/screens/address_book/entities/user_handles.dart';
|
||||
import 'package:cake_wallet/store/settings_store.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/wallet_base.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hive/hive.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';
|
||||
|
||||
class ContactViewModel = ContactViewModelBase with _$ContactViewModel;
|
||||
enum ContactEditMode {
|
||||
contactInfo,
|
||||
manualAddress,
|
||||
parsedAddress,
|
||||
}
|
||||
|
||||
abstract class ContactViewModelBase with Store {
|
||||
ContactViewModelBase(this._contacts, {ContactRecord? contact})
|
||||
: state = InitialExecutionState(),
|
||||
class ContactViewModel = _ContactViewModel with _$ContactViewModel;
|
||||
|
||||
abstract class _ContactViewModel with Store {
|
||||
_ContactViewModel(
|
||||
this.box,
|
||||
this.wallet,
|
||||
this.settingsStore, {
|
||||
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,
|
||||
_contact = contact,
|
||||
name = contact?.name ?? '',
|
||||
address = contact?.address ?? '',
|
||||
currency = contact?.type,
|
||||
lastChange = contact?.lastChange;
|
||||
state = InitialExecutionState(),
|
||||
name = request?.contact?.name ?? '',
|
||||
handle = request?.contact?.handle ?? '',
|
||||
profileName = request?.contact?.profileName ?? '',
|
||||
description = request?.contact?.description ?? '',
|
||||
imagePath = request?.contact?.imagePath ?? '',
|
||||
sourceType = request?.contact?.sourceType ?? AddressSource.notParsed,
|
||||
currency = request?.currency ?? CryptoCurrency.xmr,
|
||||
label = request?.label ?? '',
|
||||
address = '',
|
||||
handleKey = request?.handleKey ?? '' {
|
||||
_initMapsFromRecord();
|
||||
|
||||
if (request?.label != null && record != null) {
|
||||
currency = request!.currency!;
|
||||
label = request.label!;
|
||||
address = _targetMap[currency]?[label] ?? '';
|
||||
|
||||
_rememberOriginal(
|
||||
blockKey: mode == ContactEditMode.parsedAddress
|
||||
? (request.handleKey ?? _defaultHandleKey())
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final Box<Contact> box;
|
||||
final WalletBase wallet;
|
||||
final SettingsStore? settingsStore;
|
||||
ContactRecord? record;
|
||||
|
||||
@observable
|
||||
ExecutionState state;
|
||||
|
||||
@observable
|
||||
String name;
|
||||
String name, handle, profileName, description, imagePath;
|
||||
@observable
|
||||
AddressSource sourceType;
|
||||
|
||||
@observable
|
||||
String address;
|
||||
CryptoCurrency currency;
|
||||
@observable
|
||||
String label, address, handleKey;
|
||||
|
||||
@observable
|
||||
CryptoCurrency? currency;
|
||||
ObservableMap<CryptoCurrency, Map<String, String>> manual = ObservableMap();
|
||||
@observable
|
||||
ObservableMap<CryptoCurrency, Map<String, String>> parsed = ObservableMap();
|
||||
@observable
|
||||
ObservableMap<String, Map<CryptoCurrency, Map<String, String>>> parsedBlocks = ObservableMap();
|
||||
|
||||
DateTime? lastChange;
|
||||
final ContactEditMode mode;
|
||||
final List<CryptoCurrency> currencies;
|
||||
|
||||
CryptoCurrency? _originalCur;
|
||||
String? _originalLabel, _originalAddress, _originalHandleKey;
|
||||
|
||||
@computed
|
||||
bool get isReady =>
|
||||
name.isNotEmpty &&
|
||||
(currency?.toString().isNotEmpty ?? false) &&
|
||||
address.isNotEmpty;
|
||||
bool get isReady => name.trim().isNotEmpty || manual.isNotEmpty || parsed.isNotEmpty;
|
||||
|
||||
final List<CryptoCurrency> currencies;
|
||||
final Box<Contact> _contacts;
|
||||
final ContactRecord? _contact;
|
||||
@computed
|
||||
List<UserHandles> get userHandles =>
|
||||
parsedBlocks.keys.map((k) => UserHandles(handleKey: k)).toList();
|
||||
|
||||
@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
|
||||
Future<void> saveManualAddress() async {
|
||||
_ensureRecord();
|
||||
|
||||
final map = manual.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;
|
||||
manual[currency] = Map.of(map);
|
||||
|
||||
record!.setManualAddress(currency, newLabel, newAddress);
|
||||
_rememberOriginal();
|
||||
state = ExecutedSuccessfullyState();
|
||||
}
|
||||
|
||||
@action
|
||||
Future<void> saveParsedAddress() async {
|
||||
_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();
|
||||
}
|
||||
|
||||
@action
|
||||
Future<void> deleteCurrentAddress() async {
|
||||
if (!isAddressEdit) return;
|
||||
_ensureRecord();
|
||||
|
||||
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
|
||||
void reset() {
|
||||
address = '';
|
||||
name = '';
|
||||
currency = null;
|
||||
name = handle = profileName = description = imagePath = '';
|
||||
label = address = handleKey = '';
|
||||
currency = CryptoCurrency.xmr;
|
||||
manual.clear();
|
||||
parsed.clear();
|
||||
parsedBlocks.clear();
|
||||
_originalCur = null;
|
||||
_originalLabel = null;
|
||||
_originalAddress = null;
|
||||
_originalHandleKey = null;
|
||||
state = InitialExecutionState();
|
||||
}
|
||||
|
||||
Future<void> save() async {
|
||||
try {
|
||||
state = IsExecutingState();
|
||||
final now = DateTime.now();
|
||||
void _initMapsFromRecord() {
|
||||
if (record == null) return;
|
||||
|
||||
final nameExists = _contact == null
|
||||
? doesContactNameExist(name)
|
||||
: doesContactNameExist(name) && _contact.original.name != name;
|
||||
|
||||
if (nameExists) {
|
||||
state = FailureState(S.current.contact_name_exists);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_contact != null && _contact.original.isInBox) {
|
||||
_contact.name = name;
|
||||
_contact.address = address;
|
||||
_contact.type = currency!;
|
||||
_contact.lastChange = now;
|
||||
await _contact.save();
|
||||
} else {
|
||||
await _contacts
|
||||
.add(Contact(name: name, address: address, type: currency!, lastChange: now));
|
||||
}
|
||||
|
||||
lastChange = now;
|
||||
state = ExecutedSuccessfullyState();
|
||||
} catch (e) {
|
||||
state = FailureState(e.toString());
|
||||
}
|
||||
manual = ObservableMap.of(record!.manual);
|
||||
parsed = ObservableMap.of(record!.parsedByCurrency);
|
||||
parsedBlocks = ObservableMap.of(record!.parsedBlocks);
|
||||
}
|
||||
|
||||
bool doesContactNameExist(String name) {
|
||||
return _contacts.values.any((contact) => contact.name == name);
|
||||
void _ensureRecord() {
|
||||
if (record != null) return;
|
||||
final newContact = Contact(name: name.trim().isEmpty ? 'No name' : name, address: '');
|
||||
box.put(newContact.key, newContact);
|
||||
record = ContactRecord(box, newContact);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
late final Map<String, (bool Function(), void Function(bool))> lookupMap = settingsStore != null
|
||||
? {
|
||||
AddressSource.twitter.label: (
|
||||
() => settingsStore!.lookupsTwitter,
|
||||
(v) => settingsStore!.lookupsTwitter = v
|
||||
),
|
||||
AddressSource.zanoAlias.label: (
|
||||
() => settingsStore!.lookupsZanoAlias,
|
||||
(v) => settingsStore!.lookupsZanoAlias = v
|
||||
),
|
||||
AddressSource.mastodon.label: (
|
||||
() => settingsStore!.lookupsMastodon,
|
||||
(v) => settingsStore!.lookupsMastodon = v
|
||||
),
|
||||
AddressSource.yatRecord.label: (
|
||||
() => settingsStore!.lookupsYatService,
|
||||
(v) => settingsStore!.lookupsYatService = v
|
||||
),
|
||||
AddressSource.unstoppableDomains.label: (
|
||||
() => settingsStore!.lookupsUnstoppableDomains,
|
||||
(v) => settingsStore!.lookupsUnstoppableDomains = v
|
||||
),
|
||||
AddressSource.openAlias.label: (
|
||||
() => settingsStore!.lookupsOpenAlias,
|
||||
(v) => settingsStore!.lookupsOpenAlias = v
|
||||
),
|
||||
AddressSource.ens.label: (
|
||||
() => settingsStore!.lookupsENS,
|
||||
(v) => settingsStore!.lookupsENS = v
|
||||
),
|
||||
AddressSource.wellKnown.label: (
|
||||
() => settingsStore!.lookupsWellKnown,
|
||||
(v) => settingsStore!.lookupsWellKnown = v
|
||||
),
|
||||
AddressSource.fio.label: (
|
||||
() => settingsStore!.lookupsFio,
|
||||
(v) => settingsStore!.lookupsFio = v
|
||||
),
|
||||
AddressSource.nostr.label: (
|
||||
() => settingsStore!.lookupsNostr,
|
||||
(v) => settingsStore!.lookupsNostr = v
|
||||
),
|
||||
AddressSource.thorChain.label: (
|
||||
() => settingsStore!.lookupsThorChain,
|
||||
(v) => settingsStore!.lookupsThorChain = v
|
||||
),
|
||||
AddressSource.bip353.label: (
|
||||
() => settingsStore!.lookupsBip353,
|
||||
(v) => settingsStore!.lookupsBip353 = v
|
||||
),
|
||||
}
|
||||
: {};
|
||||
}
|
|
@ -73,10 +73,9 @@ class WalletRestoreFromQRCode {
|
|||
|
||||
static String? _extractAddressFromUrl(String rawString, WalletType type) {
|
||||
try {
|
||||
return AddressResolver.extractAddressByType(
|
||||
return AddressResolverService.extractAddressByType(
|
||||
raw: rawString,
|
||||
type: walletTypeToCryptoCurrency(type),
|
||||
requireSurroundingWhitespaces: false,
|
||||
);
|
||||
} catch (_) {
|
||||
return null;
|
||||
|
|
|
@ -45,7 +45,7 @@ abstract class OutputBase with Store {
|
|||
address = '',
|
||||
note = '',
|
||||
extractedAddress = '',
|
||||
parsedAddress = ParsedAddress(addresses: []) {
|
||||
parsedAddress = ParsedAddress(parsedAddressByCurrencyMap: {}) {
|
||||
_setCryptoNumMaximumFractionDigits();
|
||||
}
|
||||
|
||||
|
@ -79,7 +79,7 @@ abstract class OutputBase with Store {
|
|||
|
||||
@computed
|
||||
bool get isParsedAddress =>
|
||||
parsedAddress.parseFrom != ParseFrom.notParsed && parsedAddress.name.isNotEmpty;
|
||||
parsedAddress.addressSource != AddressSource.notParsed && parsedAddress.handle != null;
|
||||
|
||||
@observable
|
||||
String? stealthAddress;
|
||||
|
@ -241,7 +241,7 @@ abstract class OutputBase with Store {
|
|||
|
||||
void resetParsedAddress() {
|
||||
extractedAddress = '';
|
||||
parsedAddress = ParsedAddress(addresses: []);
|
||||
parsedAddress = ParsedAddress( parsedAddressByCurrencyMap: {});
|
||||
}
|
||||
|
||||
@action
|
||||
|
@ -323,15 +323,16 @@ abstract class OutputBase with Store {
|
|||
Future<void> fetchParsedAddress(BuildContext context) async {
|
||||
final domain = address;
|
||||
final currency = cryptoCurrencyHandler();
|
||||
parsedAddress = await getIt.get<AddressResolver>().resolve(context, domain, currency);
|
||||
extractedAddress = await extractAddressFromParsed(context, parsedAddress);
|
||||
note = parsedAddress.description;
|
||||
final parsedAddresses = await getIt.get<AddressResolverService>().resolve(query: domain, currency: currency, wallet: _wallet);
|
||||
parsedAddress = parsedAddresses.first;
|
||||
extractedAddress = ''; //TODO: fix return parsedAddress.addressByCurrencyMap[currency] ?? '';
|
||||
note = parsedAddress.description ?? '';
|
||||
}
|
||||
|
||||
void loadContact(ContactBase contact) {
|
||||
address = contact.name;
|
||||
parsedAddress = ParsedAddress.fetchContactAddress(address: contact.address, name: contact.name);
|
||||
extractedAddress = parsedAddress.addresses.first;
|
||||
note = parsedAddress.description;
|
||||
parsedAddress = ParsedAddress(parsedAddressByCurrencyMap: {contact.type : contact.address}, handle: contact.name);
|
||||
extractedAddress = parsedAddress.parsedAddressByCurrencyMap[contact.type] ?? '';
|
||||
note = parsedAddress.description ?? '';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -312,7 +312,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
|||
List<CryptoCurrency> currencies;
|
||||
|
||||
bool get hasYat => outputs
|
||||
.any((out) => out.isParsedAddress && out.parsedAddress.parseFrom == ParseFrom.yatRecord);
|
||||
.any((out) => out.isParsedAddress && out.parsedAddress.addressSource == AddressSource.yatRecord);
|
||||
|
||||
WalletType get walletType => wallet.type;
|
||||
|
||||
|
@ -431,7 +431,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
|||
|
||||
outputs.first.address = paymentRequest.address;
|
||||
outputs.first.parsedAddress =
|
||||
ParsedAddress(addresses: [paymentRequest.address], name: ocpRequest!.receiverName);
|
||||
ParsedAddress(parsedAddressByCurrencyMap: {currency:paymentRequest.address}, handle: ocpRequest!.receiverName);
|
||||
outputs.first.setCryptoAmount(paymentRequest.amount);
|
||||
outputs.first.note = ocpRequest!.receiverName;
|
||||
|
||||
|
@ -708,7 +708,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
|||
for (var output in outputs) {
|
||||
String address;
|
||||
if (output.isParsedAddress) {
|
||||
address = output.parsedAddress.addresses.first;
|
||||
address = output.parsedAddress.parsedAddressByCurrencyMap[selectedCryptoCurrency] ?? '';
|
||||
} else {
|
||||
address = output.address;
|
||||
}
|
||||
|
@ -720,8 +720,15 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
|||
contactListViewModel.contactSource,
|
||||
Contact(
|
||||
name: '',
|
||||
address: address,
|
||||
type: selectedCryptoCurrency,
|
||||
parsedByHandle: {
|
||||
'handle':
|
||||
|
||||
{
|
||||
3: {'label4': address}
|
||||
}
|
||||
},
|
||||
manualAddresses: {3: {'label5':address} //TODO : Fix this hardcoded value
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -103,6 +103,18 @@ abstract class PrivacySettingsViewModelBase with Store {
|
|||
@computed
|
||||
bool get looksUpWellKnown => _settingsStore.lookupsWellKnown;
|
||||
|
||||
@computed
|
||||
bool get lookupsFio => _settingsStore.lookupsFio;
|
||||
|
||||
@computed
|
||||
bool get lookupsNostr => _settingsStore.lookupsNostr;
|
||||
|
||||
@computed
|
||||
bool get lookupsThorChain => _settingsStore.lookupsThorChain;
|
||||
|
||||
@computed
|
||||
bool get lookupsBip353 => _settingsStore.lookupsBip353;
|
||||
|
||||
@computed
|
||||
bool get usePayjoin => _settingsStore.usePayjoin;
|
||||
|
||||
|
@ -169,6 +181,18 @@ abstract class PrivacySettingsViewModelBase with Store {
|
|||
ethereum!.updateEtherscanUsageState(_wallet, value);
|
||||
}
|
||||
|
||||
@action
|
||||
void setLookupsFio(bool value) => _settingsStore.lookupsFio = value;
|
||||
|
||||
@action
|
||||
void setLookupsNostr(bool value) => _settingsStore.lookupsNostr = value;
|
||||
|
||||
@action
|
||||
void setLookupsThorChain(bool value) => _settingsStore.lookupsThorChain = value;
|
||||
|
||||
@action
|
||||
void setLookupsBip353(bool value) => _settingsStore.lookupsBip353 = value;
|
||||
|
||||
@action
|
||||
void setUsePolygonScan(bool value) {
|
||||
_settingsStore.usePolygonScan = value;
|
||||
|
|