diff --git a/lib/data/models/contact_cubit.dart b/lib/data/models/contact_cubit.dart index bbb263fd067eb420c85fb3e1a9cbe88a089ec1c2..31b1ae529e1fb2db87d1b084f3793d9ee3e005e6 100644 --- a/lib/data/models/contact_cubit.dart +++ b/lib/data/models/contact_cubit.dart @@ -93,7 +93,8 @@ class ContactsCubit extends HydratedCubit<ContactsState> { emit(state.copyWith(filteredContacts: contacts)); } - List<Contact> search(String query) { + List<Contact> search(String initialQuery) { + final String query = normalizeQuery(initialQuery); if (query.isEmpty) { return state.contacts; } diff --git a/lib/g1/api.dart b/lib/g1/api.dart index ef383d0de0ead36eddcfe60d909f08dec4a33e02..1a605fd0ba8016f42198d6a834525e6d46476ac8 100644 --- a/lib/g1/api.dart +++ b/lib/g1/api.dart @@ -50,7 +50,8 @@ Future<Response> getPeers() async { } } -Future<Response> searchCPlusUser(String searchTerm) async { +Future<Response> searchCPlusUser(String initialSearchTerm) async { + final String searchTerm = normalizeQuery(initialSearchTerm); final String searchTermLower = searchTerm.toLowerCase(); final String searchTermCapitalized = searchTermLower[0].toUpperCase() + searchTermLower.substring(1); @@ -105,11 +106,8 @@ Not found sample: "found": false } */ -Future<List<Contact>> searchWot(String searchTermRaw) async { - // If pubkey, remove checksum - final String searchTerm = validateKey(searchTermRaw) - ? extractPublicKey(searchTermRaw) - : searchTermRaw; +Future<List<Contact>> searchWot(String initialSearchTerm) async { + final String searchTerm = normalizeQuery(initialSearchTerm); final Response response = await requestDuniterWithRetry( '/wot/lookup/$searchTerm', retryWith404: false); diff --git a/lib/g1/g1_helper.dart b/lib/g1/g1_helper.dart index 1a38e6d4a04c89a7c655197ee822c289d176bb2c..aba3375be92b03525d1a552b89af78c12fe4685a 100644 --- a/lib/g1/g1_helper.dart +++ b/lib/g1/g1_helper.dart @@ -257,3 +257,12 @@ extension Ex on double { } String extractPublicKey(String key) => key.split(':')[0]; + +String normalizeQuery(String initialQuery) { + String query = initialQuery; + if (validateKey(query)) { + // Is a pubKey + query = extractPublicKey(initialQuery); + } + return query; +} diff --git a/lib/ui/nfc_helper.dart b/lib/ui/nfc_helper.dart index ad580807556ac37628553591272019dbeb8c90cd..57f0ad99872d9a9b8a4f1f68b054a9eb033464ec 100644 --- a/lib/ui/nfc_helper.dart +++ b/lib/ui/nfc_helper.dart @@ -4,6 +4,7 @@ import 'package:flutter_nfc_kit/flutter_nfc_kit.dart'; import 'package:ndef/ndef.dart' as ndef; import 'package:ndef/record.dart'; import 'package:ndef/record/uri.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; import 'logger.dart'; @@ -23,6 +24,8 @@ Future<void> writeNfcUrl(String url) async { if ((ndefAvailable == null || ndefWritable == null) && (ndefAvailable != null && !ndefAvailable) || (ndefWritable != null && !ndefWritable)) { + await Sentry.captureMessage( + 'Tag does not have NDEF capability or is not writable'); logger('Tag does not have NDEF capability or is not writable'); return; } @@ -35,6 +38,7 @@ Future<void> writeNfcUrl(String url) async { // iOS only: show an alert message await FlutterNfcKit.finish(iosAlertMessage: 'Success'); } catch (e) { + // await Sentry.captureMessage('Error while writing to tag: $e'); logger('Error while writing to tag: $e'); await FlutterNfcKit.finish(iosErrorMessage: 'Failed'); } @@ -48,9 +52,12 @@ Future<String?> readNfcUrl() async { final List<NDEFRecord> records = await FlutterNfcKit.readNDEFRecords(); for (final NDEFRecord record in records) { if (record is UriRecord) { - return record.uri.toString(); + // record.uri.toString() contains the URL but does not respect upper/lower case + return record.iriString; } } + } else { + // await Sentry.captureMessage('NFT no available'); } return null; } diff --git a/lib/ui/widgets/first_screen/pay_contact_search_page.dart b/lib/ui/widgets/first_screen/pay_contact_search_page.dart index 0f9f35e1e3fd859009ea30a7710be1c3da400d10..f13954a8f8bb6093346406662f6a21d405fe1a0e 100644 --- a/lib/ui/widgets/first_screen/pay_contact_search_page.dart +++ b/lib/ui/widgets/first_screen/pay_contact_search_page.dart @@ -25,7 +25,9 @@ import '../third_screen/contacts_page.dart'; import 'contact_fav_icon.dart'; class PayContactSearchPage extends StatefulWidget { - const PayContactSearchPage({super.key}); + const PayContactSearchPage({super.key, this.uri}); + + final String? uri; @override State<PayContactSearchPage> createState() => _PayContactSearchPageState(); @@ -65,11 +67,11 @@ class _PayContactSearchPageState extends State<PayContactSearchPage> { if (cPlusResponse.statusCode != 404) { // Add cplus users final List<dynamic> hits = ((const JsonDecoder() - .convert(cPlusResponse.body) as Map<String, dynamic>)['hits'] - as Map<String, dynamic>)['hits'] as List<dynamic>; + .convert(cPlusResponse.body) as Map<String, dynamic>)['hits'] + as Map<String, dynamic>)['hits'] as List<dynamic>; for (final dynamic hit in hits) { final Contact c = - await contactFromResultSearch(hit as Map<String, dynamic>); + await contactFromResultSearch(hit as Map<String, dynamic>); logger('Contact retrieved in c+ search $c'); ContactsCache().addContact(c); setState(() { @@ -89,11 +91,11 @@ class _PayContactSearchPageState extends State<PayContactSearchPage> { // retrieve extra results with c+ profile for (final Contact wotC in wotResults) { final Contact cachedWotProfile = - await ContactsCache().getContact(wotC.pubKey); + await ContactsCache().getContact(wotC.pubKey); if (cachedWotProfile.name == null) { // Users without c+ profile final Contact cPlusProfile = - await getProfile(cachedWotProfile.pubKey, true); + await getProfile(cachedWotProfile.pubKey, true); ContactsCache().addContact(cPlusProfile); } } @@ -134,29 +136,30 @@ class _PayContactSearchPageState extends State<PayContactSearchPage> { return Scaffold( appBar: AppBar( title: Text(tr('search_user_title')), - backgroundColor: Theme - .of(context) - .colorScheme - .primary, - foregroundColor: Theme - .of(context) - .colorScheme - .inversePrimary, + backgroundColor: Theme.of(context).colorScheme.primary, + foregroundColor: Theme.of(context).colorScheme.inversePrimary, actions: <Widget>[ - if (nft) IconButton( - icon: const Icon(Icons.nfc), - onPressed: () async { - final String? nfcUrl = await readNfcUrl(); - if (nfcUrl is String && nfcUrl != null && nfcUrl != '-1') { - await _onKeyScanned(nfcUrl, paymentCubit); - } - }, - ), + if (nft) + IconButton( + icon: const Icon(Icons.nfc), + onPressed: () async { + final String? nfcUrl = await readNfcUrl(); + if (nfcUrl is String && + nfcUrl != null && + nfcUrl != '-1') { + await _onKeyScanned(nfcUrl, paymentCubit); + if (!mounted) { + return; + } + Navigator.pop(context); + } + }, + ), IconButton( icon: const Icon(Icons.qr_code_scanner), onPressed: () async { - final String? scannedKey = await QrManager.qrScan( - context); + final String? scannedKey = + await QrManager.qrScan(context); if (scannedKey is String && scannedKey != null && scannedKey != '-1') { @@ -187,9 +190,7 @@ class _PayContactSearchPageState extends State<PayContactSearchPage> { suffixIcon: IconButton( icon: const Icon(Icons.search), onPressed: () => - _searchTerm.length < 3 - ? null - : _search(), + _searchTerm.length < 3 ? null : _search(), ), ), onChanged: (String value) { @@ -201,38 +202,36 @@ class _PayContactSearchPageState extends State<PayContactSearchPage> { ), if (_isLoading) const LoadingBox(simple: false) + else if (_searchTerm.isNotEmpty && + _results.isEmpty && + _isLoading) + const NoElements(text: 'nothing_found') else - if (_searchTerm.isNotEmpty && _results.isEmpty && - _isLoading) - const NoElements(text: 'nothing_found') - else - Expanded( - child: ListView.builder( - itemCount: _results.length, - itemBuilder: (BuildContext context, int index) { - final Contact contact = _results[index]; - return FutureBuilder<Contact>( - future: ContactsCache().getContact( - contact.pubKey), - builder: (BuildContext context, - AsyncSnapshot<Contact> snapshot) { - Widget widget; - if (snapshot.hasData) { - widget = - _buildItem( - snapshot.data!, index, context); - } else if (snapshot.hasError) { - widget = - CustomErrorWidget(snapshot.error); - } else { - // Contact without wot - widget = - _buildItem(contact, index, context); - } - return widget; - }); - }), - ) + Expanded( + child: ListView.builder( + itemCount: _results.length, + itemBuilder: (BuildContext context, int index) { + final Contact contact = _results[index]; + return FutureBuilder<Contact>( + future: + ContactsCache().getContact(contact.pubKey), + builder: (BuildContext context, + AsyncSnapshot<Contact> snapshot) { + Widget widget; + if (snapshot.hasData) { + widget = _buildItem( + snapshot.data!, index, context); + } else if (snapshot.hasError) { + widget = CustomErrorWidget(snapshot.error); + } else { + // Contact without wot + widget = + _buildItem(contact, index, context); + } + return widget; + }); + }), + ) ], ), ), @@ -240,8 +239,8 @@ class _PayContactSearchPageState extends State<PayContactSearchPage> { }); } - Future<void> _onKeyScanned(String scannedKey, - PaymentCubit paymentCubit) async { + Future<void> _onKeyScanned( + String scannedKey, PaymentCubit paymentCubit) async { final PaymentState? pay = parseScannedUri(scannedKey); if (pay != null) { logger('Scanned $pay'); @@ -264,6 +263,22 @@ class _PayContactSearchPageState extends State<PayContactSearchPage> { } } + @override + void initState() { + super.initState(); + _handleUri(widget.uri); + } + + Future<void> _handleUri(String? uri) async { + if (uri != null) { + final PaymentCubit paymentCubit = context.read<PaymentCubit>(); + await _onKeyScanned(uri, paymentCubit); + if (mounted) { + Navigator.pop(context); + } + } + } + Widget _buildItem(Contact contact, int index, BuildContext context) { return contactToListItem( contact, @@ -275,9 +290,9 @@ class _PayContactSearchPageState extends State<PayContactSearchPage> { }, trailing: BlocBuilder<ContactsCubit, ContactsState>( builder: (BuildContext context, ContactsState state) { - return ContactFavIcon( - contact: contact, contactsCubit: context.read<ContactsCubit>()); - }), + return ContactFavIcon( + contact: contact, contactsCubit: context.read<ContactsCubit>()); + }), ); } } diff --git a/lib/ui/widgets/second_screen/card_terminal_screen.dart b/lib/ui/widgets/second_screen/card_terminal_screen.dart index 3b9669b28279a498ecd3bebb39acdb25c1176638..8fe554d6af13d007b6497c6424fafbc04e21750d 100644 --- a/lib/ui/widgets/second_screen/card_terminal_screen.dart +++ b/lib/ui/widgets/second_screen/card_terminal_screen.dart @@ -1,11 +1,9 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_nfc_kit/flutter_nfc_kit.dart'; import 'package:qr_flutter/qr_flutter.dart'; import '../../../g1/g1_helper.dart'; import '../../../shared_prefs.dart'; -import '../../nfc_helper.dart'; import '../../tutorial_keys.dart'; import '../../ui_helpers.dart'; import '../connectivity_widget_wrapper_wrapper.dart'; @@ -18,149 +16,139 @@ class CardTerminalScreen extends StatelessWidget { @override Widget build(BuildContext context) { - return FutureBuilder<NFCAvailability>( - future: FlutterNfcKit.nfcAvailability, - builder: - (BuildContext context, AsyncSnapshot<NFCAvailability> snapshot) { - final String duniterUri = getQrUri( - pubKey: SharedPreferencesHelper().getPubKey(), - locale: context.locale.toLanguageTag(), - amount: amount); - - final bool nft = hasNft(snapshot); - if (nft) { - writeNfcUrl(duniterUri); - } - return Card( - key: receiveQrKey, - elevation: 8, - shape: - RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), - child: Container( - width: double.infinity, - height: smallScreen(context) ? 200 : 252, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - gradient: const LinearGradient( + final String duniterUri = getQrUri( + pubKey: SharedPreferencesHelper().getPubKey(), + locale: context.locale.toLanguageTag(), + amount: amount); + final String duniterUriNoSha = getQrUri( + pubKey: extractPublicKey(SharedPreferencesHelper().getPubKey()), + locale: context.locale.toLanguageTag(), + amount: amount); + return Card( + key: receiveQrKey, + elevation: 8, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + child: Container( + width: double.infinity, + height: smallScreen(context) ? 200 : 252, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + gradient: const LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: <Color>[ + Colors.blueGrey, + Colors.white, + ], + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: <Widget>[ + Container( + decoration: const BoxDecoration( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(8), + topRight: Radius.circular(8), + ), + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: <Color>[ + Color(0xFF3B3B3B), + Color(0xFF232323), + ], + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: <Widget>[ + ConnectivityWidgetWrapperWrapper( + offlineWidget: CardTerminalStatus( + online: false, uri: duniterUriNoSha), + child: CardTerminalStatus( + online: true, uri: duniterUriNoSha)), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Text( + amount, + textAlign: TextAlign.right, + style: TextStyle( + fontFamily: 'LCDMono', + color: Colors.white, + fontSize: amount.length < 5 + ? 28 + : amount.length < 10 + ? 20 + : amount.length < 15 + ? 14 + : 12, + shadows: <Shadow>[ + Shadow( + offset: const Offset(1, 1), + blurRadius: 3, + color: Colors.black.withOpacity(0.4), + ), + ], + //softWrap: true, // Agrega esta lÃnea para permitir que el texto se envuelva a la siguiente lÃnea + ), + ), + ), + ])), + Expanded( + child: Column(children: <Widget>[ + if (!amount.contains('+')) + Expanded( + child: GestureDetector( + onTap: () => copyPublicKeyToClipboard(context, duniterUri), + child: QrImage(data: duniterUri), + // size: smallScreen(context) ? 95.0 : 140.0) + )) + ])), + Container( + decoration: const BoxDecoration( + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(8), + bottomRight: Radius.circular(8), + ), + gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: <Color>[ - Colors.blueGrey, - Colors.white, + Color(0xFF232323), + Color(0xFF3B3B3B), ], ), ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, + child: Row( children: <Widget>[ - Container( - decoration: const BoxDecoration( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(8), - topRight: Radius.circular(8), - ), - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: <Color>[ - Color(0xFF3B3B3B), - Color(0xFF232323), - ], - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: <Widget>[ - ConnectivityWidgetWrapperWrapper( - offlineWidget: - const CardTerminalStatus(online: false), - child: const CardTerminalStatus(online: true)), - Padding( - padding: - const EdgeInsets.symmetric(horizontal: 10), - child: Text( - amount, - textAlign: TextAlign.right, + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, vertical: 6), + child: Text.rich( + TextSpan( + children: <TextSpan>[ + TextSpan( + text: amount.isNotEmpty + ? tr('show_qr_to_client_amount') + : tr('show_qr_to_client'), style: TextStyle( - fontFamily: 'LCDMono', - color: Colors.white, - fontSize: amount.length < 5 - ? 28 - : amount.length < 10 - ? 20 - : amount.length < 15 - ? 14 - : 12, - shadows: <Shadow>[ - Shadow( - offset: const Offset(1, 1), - blurRadius: 3, - color: Colors.black.withOpacity(0.4), - ), - ], - //softWrap: true, // Agrega esta lÃnea para permitir que el texto se envuelva a la siguiente lÃnea + fontFamily: 'Roboto Mono', + color: Colors.grey, + fontSize: smallScreen(context) ? 12 : 14, ), ), - ), - ])), - Expanded( - child: Column(children: <Widget>[ - if (!amount.contains('+')) - Expanded( - child: GestureDetector( - onTap: () => - copyPublicKeyToClipboard( - context, duniterUri), - child: QrImage(data: duniterUri), - // size: smallScreen(context) ? 95.0 : 140.0) - )) - ])), - Container( - decoration: const BoxDecoration( - borderRadius: BorderRadius.only( - bottomLeft: Radius.circular(8), - bottomRight: Radius.circular(8), - ), - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: <Color>[ - Color(0xFF232323), - Color(0xFF3B3B3B), - ], - ), - ), - child: Row( - children: <Widget>[ - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 10, vertical: 6), - child: Text.rich( - TextSpan( - children: <TextSpan>[ - TextSpan( - text: amount.isNotEmpty - ? tr('show_qr_to_client_amount') - : tr('show_qr_to_client'), - style: TextStyle( - fontFamily: 'Roboto Mono', - color: Colors.grey, - fontSize: - smallScreen(context) ? 12 : 14, - ), - ), - ], - ), - )), - ) - ], - ), - ), + ], + ), + )), + ) ], ), ), - ); - }); + ], + ), + ), + ); } } diff --git a/lib/ui/widgets/second_screen/card_terminal_status.dart b/lib/ui/widgets/second_screen/card_terminal_status.dart index 154f581141b09d9e2494ea89ed8b4861558c56dc..aa9908164db3b1b1089d2603119d8469ef22ae15 100644 --- a/lib/ui/widgets/second_screen/card_terminal_status.dart +++ b/lib/ui/widgets/second_screen/card_terminal_status.dart @@ -6,9 +6,11 @@ import '../../nfc_helper.dart'; import '../../ui_helpers.dart'; class CardTerminalStatus extends StatelessWidget { - const CardTerminalStatus({super.key, required this.online}); + const CardTerminalStatus( + {super.key, required this.online, required this.uri}); final bool online; + final String uri; @override Widget build(BuildContext context) { @@ -30,11 +32,17 @@ class CardTerminalStatus extends StatelessWidget { )), if (nft || inDevelopment) Tooltip( - message: tr(''), - child: Icon( - Icons.nfc, - color: nft ? Colors.green : Colors.red, - )), + message: tr('pay_with_nfc_tooltip'), + child: GestureDetector( + onTap: () { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(tr('pay_with_nfc_tooltip')))); + writeNfcUrl(uri); + }, + child: Icon( + Icons.nfc, + color: nft ? Colors.green : Colors.red, + ))), ], ), );