| 1 | import 'package:cake_wallet/core/address_resolver/parsed_address.dart'; |
| 2 | import 'package:cake_wallet/core/address_validator.dart'; |
| 3 | import 'package:cake_wallet/new-ui/widgets/send_page/send_address_input.dart'; |
| 4 | import 'package:cake_wallet/utils/show_pop_up.dart'; |
| 5 | import 'package:cw_core/currency.dart'; |
| 6 | import 'package:flutter/material.dart'; |
| 7 | import 'package:flutter_mobx/flutter_mobx.dart'; |
| 8 | import 'package:mobx/mobx.dart'; |
| 9 | import 'package:cake_wallet/generated/i18n.dart'; |
| 10 | import 'package:cake_wallet/core/contact_name_validator.dart'; |
| 11 | import 'package:cake_wallet/core/execution_state.dart'; |
| 12 | import 'package:cake_wallet/view_model/contact_list/contact_view_model.dart'; |
| 13 | import 'package:cw_core/crypto_currency.dart'; |
| 14 | import 'package:cake_wallet/src/screens/base_page.dart'; |
| 15 | import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; |
| 16 | import 'package:cake_wallet/src/widgets/primary_button.dart'; |
| 17 | import 'package:cake_wallet/src/widgets/scrollable_with_bottom_section.dart'; |
| 18 | import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; |
| 19 | import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart'; |
| 20 | |
| 21 | class ContactPage extends BasePage { |
| 22 | ContactPage(this.contactViewModel) |
| 23 | : _formKey = GlobalKey<FormState>(), |
| 24 | _nameController = TextEditingController(), |
| 25 | _addressController = TextEditingController(), |
| 26 | _currencyTypeController = TextEditingController(), |
| 27 | addressFocusNode = FocusNode() { |
| 28 | _nameController.text = contactViewModel.name; |
| 29 | _addressController.text = contactViewModel.address; |
| 30 | _nameController.addListener(() => contactViewModel.name = _nameController.text); |
| 31 | _addressController.addListener(() { |
| 32 | final address = _addressController.text; |
| 33 | |
| 34 | if (address != contactViewModel.address) { |
| 35 | contactViewModel.displayName = ''; |
| 36 | } |
| 37 | |
| 38 | contactViewModel.address = address; |
| 39 | }); |
| 40 | |
| 41 | autorun((_) => _currencyTypeController.text = |
| 42 | "${contactViewModel.currency?.toString() ?? ""} ${contactViewModel.currency?.tag != null ? "(${contactViewModel.currency?.tag})" : ""}"); |
| 43 | } |
| 44 | |
| 45 | @override |
| 46 | String get title => S.current.contact; |
| 47 | |
| 48 | FocusNode addressFocusNode; |
| 49 | |
| 50 | final ContactViewModel contactViewModel; |
| 51 | final GlobalKey<FormState> _formKey; |
| 52 | final TextEditingController _nameController; |
| 53 | final TextEditingController _currencyTypeController; |
| 54 | final TextEditingController _addressController; |
| 55 | bool _isEffectsApplied = false; |
| 56 | |
| 57 | @override |
| 58 | Widget body(BuildContext context) { |
| 59 | final downArrow = Image.asset( |
| 60 | 'assets/images/arrow_bottom_purple_icon.png', |
| 61 | color: Theme.of(context).colorScheme.primary, |
| 62 | height: 8, |
| 63 | ); |
| 64 | |
| 65 | _setEffects(context); |
| 66 | |
| 67 | return Observer( |
| 68 | builder: (_) => ScrollableWithBottomSection( |
| 69 | contentPadding: EdgeInsets.all(24), |
| 70 | content: Form( |
| 71 | key: _formKey, |
| 72 | child: Column( |
| 73 | mainAxisSize: MainAxisSize.min, |
| 74 | children: <Widget>[ |
| 75 | BaseTextFormField( |
| 76 | controller: _nameController, |
| 77 | hintText: S.of(context).contact_name, |
| 78 | validator: ContactNameValidator(), |
| 79 | ), |
| 80 | Padding( |
| 81 | padding: EdgeInsets.only(top: 20), |
| 82 | child: Container( |
| 83 | child: InkWell( |
| 84 | onTap: () => _presentCurrencyPicker(context), |
| 85 | child: IgnorePointer( |
| 86 | child: BaseTextFormField( |
| 87 | controller: _currencyTypeController, |
| 88 | hintText: S.of(context).settings_currency, |
| 89 | suffixIcon: Row( |
| 90 | mainAxisSize: MainAxisSize.min, |
| 91 | mainAxisAlignment: MainAxisAlignment.end, |
| 92 | children: <Widget>[ |
| 93 | downArrow, |
| 94 | SizedBox(width: 16), |
| 95 | ], |
| 96 | ), |
| 97 | ), |
| 98 | ), |
| 99 | ), |
| 100 | ), |
| 101 | ), |
| 102 | if (contactViewModel.currency != null) |
| 103 | Padding( |
| 104 | padding: EdgeInsets.only(top: 20), |
| 105 | child: Observer( |
| 106 | builder: (_) { |
| 107 | return NewSendAddressInput( |
| 108 | focusNode: addressFocusNode, |
| 109 | displayName: contactViewModel.displayName, |
| 110 | addressController: _addressController, |
| 111 | onEditingComplete: () {}, |
| 112 | onPushPasteButton: (context) => _extractParsedAddress(context), |
| 113 | validator: AddressValidator(type: contactViewModel.currency!), |
| 114 | selectedCurrency: contactViewModel.currency!, |
| 115 | ); |
| 116 | }, |
| 117 | ), |
| 118 | ) |
| 119 | ], |
| 120 | ), |
| 121 | ), |
| 122 | bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24), |
| 123 | bottomSection: Row( |
| 124 | children: <Widget>[ |
| 125 | Expanded( |
| 126 | child: PrimaryButton( |
| 127 | onPressed: () { |
| 128 | contactViewModel.reset(); |
| 129 | _nameController.text = ''; |
| 130 | _addressController.text = ''; |
| 131 | }, |
| 132 | text: S.of(context).reset, |
| 133 | color: Theme.of(context).colorScheme.surfaceContainer, |
| 134 | textColor: Theme.of(context).colorScheme.onSecondaryContainer, |
| 135 | ), |
| 136 | ), |
| 137 | SizedBox(width: 20), |
| 138 | Expanded( |
| 139 | child: Observer( |
| 140 | builder: (_) => PrimaryButton( |
| 141 | onPressed: () async { |
| 142 | FocusScope.of(context).unfocus(); |
| 143 | _extractParsedAddress(context); |
| 144 | |
| 145 | if (_formKey.currentState != null && !_formKey.currentState!.validate()) { |
| 146 | return; |
| 147 | } |
| 148 | |
| 149 | await contactViewModel.save(); |
| 150 | }, |
| 151 | text: S.of(context).save, |
| 152 | color: Theme.of(context).colorScheme.primary, |
| 153 | textColor: Theme.of(context).colorScheme.onPrimary, |
| 154 | isDisabled: !contactViewModel.isReady, |
| 155 | ), |
| 156 | ), |
| 157 | ) |
| 158 | ], |
| 159 | )), |
| 160 | ); |
| 161 | } |
| 162 | |
| 163 | Future<void> _extractParsedAddress(BuildContext context) async { |
| 164 | final parsedAddress = await contactViewModel.extractParsedAddress(context); |
| 165 | if (parsedAddress == null) return; |
| 166 | |
| 167 | final confirmed = await showParsedAddressConfirmationAlert(context, parsedAddress); |
| 168 | if (!confirmed) return; |
| 169 | |
| 170 | contactViewModel.applyParsedAddress(parsedAddress); |
| 171 | } |
| 172 | |
| 173 | void _presentCurrencyPicker(BuildContext context) { |
| 174 | showPopUp<void>( |
| 175 | builder: (_) => CurrencyPicker( |
| 176 | selectedAtIndex: contactViewModel.currency != null |
| 177 | ? contactViewModel.currencies.indexOf(contactViewModel.currency!) |
| 178 | : -1, |
| 179 | items: contactViewModel.currencies, |
| 180 | title: S.of(context).please_select, |
| 181 | hintText: S.of(context).search_currency, |
| 182 | onItemSelected: (Currency item) => contactViewModel.currency = item as CryptoCurrency, |
| 183 | ), |
| 184 | context: context, |
| 185 | ); |
| 186 | } |
| 187 | |
| 188 | void _onContactSavingFailure(BuildContext context, String error) { |
| 189 | showPopUp<void>( |
| 190 | context: context, |
| 191 | builder: (BuildContext context) { |
| 192 | return AlertWithOneAction( |
| 193 | alertTitle: S.current.contact, |
| 194 | alertContent: error, |
| 195 | buttonText: S.of(context).ok, |
| 196 | buttonAction: () => Navigator.of(context).pop(), |
| 197 | ); |
| 198 | }); |
| 199 | } |
| 200 | |
| 201 | void _onContactSavedSuccessfully(BuildContext context) => Navigator.of(context).pop(); |
| 202 | |
| 203 | void _setEffects(BuildContext context) { |
| 204 | if (_isEffectsApplied) { |
| 205 | return; |
| 206 | } |
| 207 | |
| 208 | _isEffectsApplied = true; |
| 209 | |
| 210 | reaction((_) => contactViewModel.state, (ExecutionState state) { |
| 211 | if (state is FailureState) { |
| 212 | _onContactSavingFailure(context, state.error); |
| 213 | } |
| 214 | |
| 215 | if (state is ExecutedSuccessfullyState) { |
| 216 | _onContactSavedSuccessfully(context); |
| 217 | } |
| 218 | }); |
| 219 | |
| 220 | reaction((_) => contactViewModel.address, (val) { |
| 221 | if (val != _addressController.text) { |
| 222 | _addressController.text = val; |
| 223 | } |
| 224 | }); |
| 225 | } |
| 226 | |
| 227 | Future<bool> showParsedAddressConfirmationAlert( |
| 228 | BuildContext context, |
| 229 | ParsedAddress parsedAddress, |
| 230 | ) async { |
| 231 | final confirmed = await showPopUp<bool>( |
| 232 | context: context, |
| 233 | builder: (BuildContext context) { |
| 234 | return AlertWithOneAction( |
| 235 | alertTitle: S.of(context).address_detected, |
| 236 | headerTitleText: parsedAddress.profileName.isEmpty ? null : parsedAddress.profileName, |
| 237 | headerImageProfileUrl: parsedAddress.profileImageUrl.isEmpty |
| 238 | ? parsedAddress.addressSource.iconPath |
| 239 | : parsedAddress.profileImageUrl, |
| 240 | alertContent: S.of(context).extracted_address_content( |
| 241 | '${parsedAddress.handle} (${parsedAddress.addressSource.label})', |
| 242 | ), |
| 243 | buttonText: S.of(context).ok, |
| 244 | buttonAction: () => Navigator.of(context).pop(true), |
| 245 | ); |
| 246 | }, |
| 247 | ); |
| 248 | |
| 249 | return confirmed ?? false; |
| 250 | } |
| 251 | } |