diff --git a/.metadata b/.metadata index 9c042f29fe47678f82cc238558e6a9e6fe10b25e..7772fa1421ed163c00072cb5e69056fe6895b6a2 100644 --- a/.metadata +++ b/.metadata @@ -4,7 +4,7 @@ # This file should be version controlled. version: - revision: 2ad6cd72c040113b47ee9055e722606a490ef0da + revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf channel: stable project_type: app @@ -13,11 +13,17 @@ project_type: app migration: platforms: - platform: root - create_revision: 2ad6cd72c040113b47ee9055e722606a490ef0da - base_revision: 2ad6cd72c040113b47ee9055e722606a490ef0da + create_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf + base_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf - platform: linux - create_revision: 2ad6cd72c040113b47ee9055e722606a490ef0da - base_revision: 2ad6cd72c040113b47ee9055e722606a490ef0da + create_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf + base_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf + - platform: macos + create_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf + base_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf + - platform: windows + create_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf + base_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf # User provided section diff --git a/lib/data/models/payment_cubit.dart b/lib/data/models/payment_cubit.dart index ab2abc018ac44a901afd1cb16bb14bc5c7caa9fb..372a4b1ad2ce5605d07a4181edbea3acafa093fa 100644 --- a/lib/data/models/payment_cubit.dart +++ b/lib/data/models/payment_cubit.dart @@ -36,7 +36,8 @@ class PaymentCubit extends HydratedCubit<PaymentState> { } void sent() { - emit(state.copyWith(status: PaymentStatus.isSent)); + const PaymentState newState = PaymentState(); + emit(newState); } void notSent() { diff --git a/lib/ui/contacts_cache.dart b/lib/ui/contacts_cache.dart index 4f29c7a0ffd7d55e0282a7e248f3d8bd062a677f..30a2c84ba2fe4be87582631e82ecbb0444c4f19b 100644 --- a/lib/ui/contacts_cache.dart +++ b/lib/ui/contacts_cache.dart @@ -65,7 +65,7 @@ class ContactsCache { return _box!; } - Future<Contact> getContact(String pubKey, [bool debug = true]) async { + Future<Contact> getContact(String pubKey, [bool debug = false]) async { Contact? cachedContact; try { cachedContact = await _retrieveContact(pubKey); diff --git a/lib/ui/widgets/first_screen/first_tutorial.dart b/lib/ui/widgets/first_screen/first_tutorial.dart index 984d6b0ef3e41066ccf45e5a536c3c1419d66740..91263de2c4f899b82401a8382ef01eda4513e35b 100644 --- a/lib/ui/widgets/first_screen/first_tutorial.dart +++ b/lib/ui/widgets/first_screen/first_tutorial.dart @@ -32,11 +32,11 @@ class FirstTutorial extends Tutorial { keyTarget: payAmountKey, align: ContentAlign.top, shape: ShapeLightFocus.RRect)); - /* targets.add(TutorialTarget( - identify: 'paySentKey', - keyTarget: paySentKey, - align: ContentAlign.top, - shape: ShapeLightFocus.RRect)); */ + targets.add(TutorialTarget( + identify: 'paySentKey', + keyTarget: paySentKey, + align: ContentAlign.top, + )); return targets; } } diff --git a/lib/ui/widgets/first_screen/g1_textfield.dart b/lib/ui/widgets/first_screen/g1_textfield.dart index 85aca5c74d391ad85eb25cef7c59181c0ec0d538..56cdc13f074be2b36194ce3fa588e6b53c25c3fb 100644 --- a/lib/ui/widgets/first_screen/g1_textfield.dart +++ b/lib/ui/widgets/first_screen/g1_textfield.dart @@ -1,10 +1,12 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_svg/flutter_svg.dart'; +import 'package:toggle_switch/toggle_switch.dart'; +import '../../../data/models/app_cubit.dart'; import '../../../data/models/payment_cubit.dart'; import '../../../data/models/payment_state.dart'; +import '../../logger.dart'; import '../../ui_helpers.dart'; class G1PayAmountField extends StatefulWidget { @@ -31,48 +33,64 @@ class _G1PayAmountFieldState extends State<G1PayAmountField> { TextPosition(offset: _controller.text.length)); } } + final bool expertMode = context.read<AppCubit>().isExpertMode; return Form( key: _formKey, child: TextFormField( - controller: _controller, - keyboardType: const TextInputType.numberWithOptions( - decimal: true, - ), - validator: validateDecimal, - // Disallow autocomplete - autofillHints: const <String>[], - onEditingComplete: () {}, - onChanged: (String? value) { - final bool? validate = _formKey.currentState?.validate(); - if (validate != null && - value != null && - value.isNotEmpty && - validate) { - context.read<PaymentCubit>().selectAmount( - parseToDoubleLocalized( - locale: context.locale.toLanguageTag(), - number: value)); - } else { - context.read<PaymentCubit>().selectAmount( - value == null ? null : double.tryParse(value)); - } - }, - decoration: InputDecoration( - labelText: tr('g1_amount'), - hintText: tr('g1_amount_hint'), - prefixIcon: Padding( - padding: const EdgeInsets.all(10.0), - child: SvgPicture.asset( - colorFilter: ColorFilter.mode( - Colors.purple.shade600, BlendMode.srcIn), - 'assets/img/gbrevedot.svg', - width: 20.0, - height: 20.0, - ), + keyboardType: const TextInputType.numberWithOptions( + decimal: true, ), - border: const OutlineInputBorder(), - ), - )); + validator: validateDecimal, + // Disallow autocomplete + autofillHints: const <String>[], + onEditingComplete: () {}, + onChanged: (String? value) { + final bool? validate = _formKey.currentState?.validate(); + if (validate != null && + value != null && + value.isNotEmpty && + validate) { + context.read<PaymentCubit>().selectAmount( + parseToDoubleLocalized( + locale: context.locale.toLanguageTag(), + number: value)); + } else { + context.read<PaymentCubit>().selectAmount( + value == null ? null : double.tryParse(value)); + } + }, + decoration: InputDecoration( + labelText: tr('g1_amount'), + hintText: tr('g1_amount_hint'), + contentPadding: const EdgeInsets.fromLTRB(16, 0, 10, 0), + border: const OutlineInputBorder(), + suffix: Padding( + padding: const EdgeInsets.only(bottom: 20), + child: ToggleSwitch( + minWidth: 40.0, + // animate: true, + radiusStyle: true, + // initialLabelIndex: 0, + cornerRadius: 20.0, + activeFgColor: Colors.white, + inactiveBgColor: Colors.grey[400], + inactiveFgColor: Colors.white, + totalSwitches: expertMode ? 2 : 1, + labels: expertMode && inDevelopment + ? const <String>['Äž1', 'DU'] + : const <String>['Äž1'], + iconSize: 30.0, + borderWidth: 2.0, + // borderColor: [Colors.blueGrey], + activeBgColors: <List<Color>>[ + <Color>[Theme.of(context).primaryColor], + <Color>[Theme.of(context).primaryColor], + ], + onToggle: (int? index) { + logger('switched to: $index'); + }, + ), + )))); }); String? validateDecimal(String? value) { 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 3460477777da73ee022d798aa42a83b64cbf5bd8..cad7f2b224fc9e880e595a37316fc4e32eb14690 100644 --- a/lib/ui/widgets/first_screen/pay_contact_search_page.dart +++ b/lib/ui/widgets/first_screen/pay_contact_search_page.dart @@ -58,20 +58,20 @@ class _PayContactSearchPageState extends State<PayContactSearchPage> { final Response cPlusResponse = await searchCPlusUser(_searchTerm); if (cPlusResponse.statusCode != 404) { - setState(() async { - // 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>; - for (final dynamic hit in hits) { - final Contact c = - await contactFromResultSearch(hit as Map<String, dynamic>); - logger('Contact retrieved in c+ search $c'); - ContactsCache().addContact(c); + // 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>; + for (final dynamic hit in hits) { + final Contact c = + await contactFromResultSearch(hit as Map<String, dynamic>); + logger('Contact retrieved in c+ search $c'); + ContactsCache().addContact(c); + setState(() { _addIfNotPresent(c); - } - logger('Found: ${_results.length}'); - }); + }); + } + logger('Found: ${_results.length}'); } final List<Contact> wotResults = await searchWot(_searchTerm); @@ -82,11 +82,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); } } @@ -120,7 +120,10 @@ class _PayContactSearchPageState extends State<PayContactSearchPage> { return Scaffold( appBar: AppBar( title: Text(tr('search_user_title')), - backgroundColor: Theme.of(context).colorScheme.primary, + backgroundColor: Theme + .of(context) + .colorScheme + .primary, foregroundColor: Colors.white, actions: <Widget>[ IconButton( @@ -195,32 +198,33 @@ class _PayContactSearchPageState extends State<PayContactSearchPage> { ), if (_isLoading) const LoadingBox(simple: false) - 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; - }); - }), - ) + 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; + }); + }), + ) ], ), ), @@ -238,9 +242,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/first_screen/pay_form.dart b/lib/ui/widgets/first_screen/pay_form.dart index 05b4049e3201cf063f408f42225b46e6013b6b68..3b19f2036c7880bfb5a50348cf35e5597ed50b8b 100644 --- a/lib/ui/widgets/first_screen/pay_form.dart +++ b/lib/ui/widgets/first_screen/pay_form.dart @@ -10,7 +10,6 @@ import '../../logger.dart'; import '../../pay_helper.dart'; import '../../tutorial_keys.dart'; import '../../ui_helpers.dart'; -import '../connectivity_widget_wrapper_wrapper.dart'; import 'g1_textfield.dart'; class PayForm extends StatefulWidget { @@ -23,7 +22,7 @@ class PayForm extends StatefulWidget { class _PayFormState extends State<PayForm> { final GlobalKey<FormState> _formKey = GlobalKey<FormState>(); final GlobalKey<FormFieldState<String>> _formCommentKey = - GlobalKey<FormFieldState<String>>(); + GlobalKey<FormFieldState<String>>(); final TextEditingController _commentController = TextEditingController(); final ValueNotifier<String> _feedbackNotifier = ValueNotifier<String>(''); @@ -38,134 +37,143 @@ class _PayFormState extends State<PayForm> { Widget build(BuildContext context) { return BlocBuilder<PaymentCubit, PaymentState>( builder: (BuildContext context, PaymentState state) { - if (state.comment != null && _commentController.text != state.comment) { - _commentController.text = state.comment; - } + if (state.comment != null && + _commentController.text != state.comment) { + _commentController.text = state.comment; + } - if (state.amount == null || state.amount == 0) { - _feedbackNotifier.value = ''; - } - final ButtonStyle payBtnStyle = ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 25), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(30.0), - ), - foregroundColor: Colors.white, - backgroundColor: Theme.of(context).colorScheme.primary, - textStyle: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - ), - ); - final Widget payBtnText = Text(tr( - 'g1_form_pay_send')); // + (!kReleaseMode ? ' ${state.status}' : '')); - return Form( - key: _formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: <Widget>[ - G1PayAmountField(key: payAmountKey), - const SizedBox(height: 10.0), - TextFormField( - key: _formCommentKey, - inputFormatters: <TextInputFormatter>[ - NoNewLineTextInputFormatter() - ], - controller: _commentController, - onChanged: (String? value) { - context.read<PaymentCubit>().setComment(value ?? ''); - }, - decoration: InputDecoration( - labelText: tr('g1_form_pay_desc'), - hintText: tr('g1_form_pay_hint'), - border: const OutlineInputBorder(), - ), - validator: (String? value) { - if (value != null && !basicEnglishCharsRegExp.hasMatch(value)) { - return tr('valid_comment'); - } - return null; - }, - // Disallow autocomplete - autofillHints: const <String>[], - ), - const SizedBox(height: 10.0), - ConnectivityWidgetWrapperWrapper( - stacked: false, - offlineWidget: ElevatedButton( - onPressed: null, - style: payBtnStyle, - child: _buildBtn(Text(tr('offline'))), - ), - child: ElevatedButton( - key: paySentKey, - onPressed: (!state.canBeSent() || - state.amount == null || - !_commentValidate() || - !_weHaveBalance(context, state.amount!)) - ? null - : () async { - try { - await payWithRetry( - context: context, - to: state.contact!, - amount: state.amount!, - comment: state.comment); - } on RetryException { - // Here the transactions can be lost, so we must implement some manual retry use - await payWithRetry( - context: context, - to: state.contact!, - amount: state.amount!, - comment: state.comment, - useMempool: true); + if (state.amount == null || state.amount == 0) { + _feedbackNotifier.value = ''; + } + final bool sentDisabled = _onPressed(state, context) == null; + final Color sentColor = + sentDisabled ? Colors.grey : Theme + .of(context) + .primaryColor; + return Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: <Widget>[ + const SizedBox(height: 10.0), + G1PayAmountField(key: payAmountKey), + const SizedBox(height: 10.0), + Row(children: <Widget>[ + Expanded( + child: TextFormField( + key: _formCommentKey, + inputFormatters: <TextInputFormatter>[ + NoNewLineTextInputFormatter() + ], + controller: _commentController, + onChanged: (String? value) { + context.read<PaymentCubit>().setComment(value ?? ''); + }, + decoration: InputDecoration( + labelText: tr('g1_form_pay_desc'), + hintText: tr('g1_form_pay_hint'), + border: const OutlineInputBorder(), + ), + validator: (String? value) { + if (value != null && + !basicEnglishCharsRegExp.hasMatch(value)) { + return tr('valid_comment'); } + return null; }, - style: payBtnStyle, - child: _buildBtn(payBtnText), - )), - const SizedBox(height: 8), - ValueListenableBuilder<String>( - valueListenable: _feedbackNotifier, - builder: (BuildContext context, String value, Widget? child) { - if (value.isNotEmpty) { - return Row( + // Disallow autocomplete + autofillHints: const <String>[], + )), + const SizedBox(width: 5.0), + Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ - const Icon(Icons.error_outline, color: Colors.red), - const SizedBox(width: 4), + Material( + color: Colors.transparent, + child: IgnorePointer( + ignoring: sentDisabled, + child: IconTheme( + data: const IconThemeData(size: 40.0), + child: IconButton( + key: paySentKey, + tooltip: tr('g1_form_pay_send'), + icon: Icon( + Icons.send, + color: sentColor, + ), + onPressed: _onPressed(state, context), + splashRadius: 20, + splashColor: Colors.white.withOpacity(0.5), + highlightColor: Colors.transparent, + ), + ), + ), + ), Text( - capitalize(value), - style: const TextStyle(color: Colors.red), + tr('g1_form_pay_send'), + style: TextStyle(fontSize: 12, color: sentColor), ), ], - ); - } else { - return const SizedBox.shrink(); - } - }, + ), + + ]), + const SizedBox(height: 10.0), + ValueListenableBuilder<String>( + valueListenable: _feedbackNotifier, + builder: (BuildContext context, String value, Widget? child) { + if (value.isNotEmpty) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: <Widget>[ + const Icon(Icons.error_outline, color: Colors.red), + const SizedBox(width: 4), + Text( + capitalize(value), + style: const TextStyle(color: Colors.red), + ), + ], + ); + } else { + return const SizedBox.shrink(); + } + }, + ), + ], ), - ], - ), - ); - }); + ); + }); } - Row _buildBtn(Widget payBtnText) { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: <Widget>[ - const Icon(Icons.send), - const SizedBox(width: 10), - payBtnText, - ], - ); + Future<void> Function()? _onPressed(PaymentState state, + BuildContext context) { + return (!state.canBeSent() || + state.amount == null || + !_commentValidate() || + !_weHaveBalance(context, state.amount!)) + ? null + : () async { + try { + await payWithRetry( + context: context, + to: state.contact!, + amount: state.amount!, + comment: state.comment); + } on RetryException { + // Here the transactions can be lost, so we must implement some manual retry use + await payWithRetry( + context: context, + to: state.contact!, + amount: state.amount!, + comment: state.comment, + useMempool: true); + } + }; } bool _commentValidate() { final String currentComment = _commentController.value.text; final bool val = (currentComment != null && - basicEnglishCharsRegExp.hasMatch(currentComment)) || + basicEnglishCharsRegExp.hasMatch(currentComment)) || currentComment.isEmpty; logger('Validating comment: $val'); if (_formKey.currentState != null) { @@ -188,7 +196,9 @@ class _PayFormState extends State<PayForm> { } double getBalance(BuildContext context) => - context.read<TransactionCubit>().balance; + context + .read<TransactionCubit>() + .balance; } class RetryException implements Exception { @@ -197,12 +207,12 @@ class RetryException implements Exception { class NoNewLineTextInputFormatter extends TextInputFormatter { @override - TextEditingValue formatEditUpdate( - TextEditingValue oldValue, TextEditingValue newValue) { + TextEditingValue formatEditUpdate(TextEditingValue oldValue, + TextEditingValue newValue) { final int cursorPosition = newValue.selection.baseOffset; final String newText = newValue.text.replaceAll('\n', ''); final TextSelection newSelection = - TextSelection.collapsed(offset: cursorPosition); + TextSelection.collapsed(offset: cursorPosition); return TextEditingValue( text: newText, selection: newSelection, diff --git a/pubspec.lock b/pubspec.lock index ecce57fc745d89d7905e2c205d006c2895f64ccb..7bf66f7ba8b7afa814a28ce65213e64c05bb9a05 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1411,6 +1411,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + toggle_switch: + dependency: "direct main" + description: + name: toggle_switch + sha256: "9e6af1f0c5a97d9de41109dc7b9e1b3bbe73417f89b10e0e44dc834fb493d4cb" + url: "https://pub.dev" + source: hosted + version: "2.1.0" tuple: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index cd9a77cfeac507362a08bf05c732d27bbc1cda0a..e8ccd26228a3065ce0006bce8e314fd4fce0f7a5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,7 +15,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 0.1.5 +version: 0.1.6-SNAPSHOT environment: sdk: ">=2.17.1 <3.0.0" @@ -87,6 +87,7 @@ dependencies: connectivity_wrapper: ^1.1.3 rxdart: ^0.27.7 fast_image_resizer: ^0.0.2 + toggle_switch: ^2.1.0 dev_dependencies: flutter_test: