| 1 | import 'package:cake_wallet/core/address_validator.dart'; |
| 2 | import 'package:cake_wallet/nano/nano.dart'; |
| 3 | import 'package:cake_wallet/src/widgets/address_text_field.dart'; |
| 4 | import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; |
| 5 | import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; |
| 6 | import 'package:cake_wallet/store/settings_store.dart'; |
| 7 | import 'package:cake_wallet/utils/payment_request.dart'; |
| 8 | import 'package:cake_wallet/utils/show_pop_up.dart'; |
| 9 | import 'package:cw_core/crypto_currency.dart'; |
| 10 | import 'package:cw_core/wallet_base.dart'; |
| 11 | import 'package:cw_core/n2_node.dart'; |
| 12 | import 'package:flutter/material.dart'; |
| 13 | import 'package:flutter_mobx/flutter_mobx.dart'; |
| 14 | import 'package:cake_wallet/generated/i18n.dart'; |
| 15 | import 'package:cake_wallet/src/widgets/primary_button.dart'; |
| 16 | import 'package:cake_wallet/src/screens/base_page.dart'; |
| 17 | import 'package:cake_wallet/src/widgets/scrollable_with_bottom_section.dart'; |
| 18 | |
| 19 | class NanoChangeRepPage extends BasePage { |
| 20 | NanoChangeRepPage({required SettingsStore settingsStore, required WalletBase wallet}) |
| 21 | : _wallet = wallet, |
| 22 | _settingsStore = settingsStore, |
| 23 | _addressController = TextEditingController(), |
| 24 | _formKey = GlobalKey<FormState>() {} |
| 25 | |
| 26 | final TextEditingController _addressController; |
| 27 | final WalletBase _wallet; |
| 28 | final SettingsStore _settingsStore; |
| 29 | |
| 30 | final GlobalKey<FormState> _formKey; |
| 31 | |
| 32 | @override |
| 33 | String get title => S.current.change_rep; |
| 34 | |
| 35 | N2Node getCurrentRepNode(List<N2Node> nodes) { |
| 36 | final currentRepAccount = nano!.getRepresentative(_wallet); |
| 37 | final currentNode = nodes.firstWhere( |
| 38 | (node) => node.account == currentRepAccount, |
| 39 | orElse: () => N2Node( |
| 40 | account: currentRepAccount, |
| 41 | score: 0, |
| 42 | uptime: "???", |
| 43 | weight: 0, |
| 44 | ), |
| 45 | ); |
| 46 | return currentNode; |
| 47 | } |
| 48 | |
| 49 | @override |
| 50 | Widget body(BuildContext context) { |
| 51 | return Form( |
| 52 | key: _formKey, |
| 53 | child: FutureBuilder( |
| 54 | future: nano!.getN2Reps(_wallet), |
| 55 | builder: (context, snapshot) { |
| 56 | final reps = snapshot.data ?? []; |
| 57 | |
| 58 | return Container( |
| 59 | padding: EdgeInsets.only(left: 24, right: 24), |
| 60 | child: ScrollableWithBottomSection( |
| 61 | topSectionPadding: EdgeInsets.only(bottom: 24), |
| 62 | topSection: Column( |
| 63 | children: [ |
| 64 | Row( |
| 65 | children: <Widget>[ |
| 66 | Expanded( |
| 67 | child: AddressTextField( |
| 68 | controller: _addressController, |
| 69 | onURIScanned: (uri) { |
| 70 | final paymentRequest = PaymentRequest.fromUri(uri); |
| 71 | _addressController.text = paymentRequest.address; |
| 72 | }, |
| 73 | options: [ |
| 74 | AddressTextFieldOption.paste, |
| 75 | AddressTextFieldOption.qrCode, |
| 76 | ], |
| 77 | buttonColor: Theme.of(context).colorScheme.surfaceContainerHighest, |
| 78 | validator: AddressValidator(type: CryptoCurrency.nano), |
| 79 | ), |
| 80 | ) |
| 81 | ], |
| 82 | ), |
| 83 | Column( |
| 84 | children: [ |
| 85 | Container( |
| 86 | margin: EdgeInsets.only(top: 12), |
| 87 | child: Text( |
| 88 | S.current.nano_current_rep, |
| 89 | style: Theme.of(context).textTheme.bodyMedium!.copyWith( |
| 90 | fontSize: 16, |
| 91 | fontWeight: FontWeight.w700, |
| 92 | ), |
| 93 | ), |
| 94 | ), |
| 95 | _buildSingleRepresentative( |
| 96 | context, |
| 97 | getCurrentRepNode(reps), |
| 98 | isList: false, |
| 99 | divider: false, |
| 100 | ), |
| 101 | if (reps.isNotEmpty) ...[ |
| 102 | Divider(height: 20), |
| 103 | Container( |
| 104 | margin: EdgeInsets.only(top: 12), |
| 105 | child: Text( |
| 106 | S.current.nano_pick_new_rep, |
| 107 | style: Theme.of(context).textTheme.bodyMedium!.copyWith( |
| 108 | fontSize: 16, |
| 109 | fontWeight: FontWeight.w700, |
| 110 | ), |
| 111 | ), |
| 112 | ), |
| 113 | Divider(height: 20), |
| 114 | ], |
| 115 | ], |
| 116 | ), |
| 117 | ], |
| 118 | ), |
| 119 | contentPadding: EdgeInsets.only(bottom: 24), |
| 120 | content: Container( |
| 121 | child: reps.isNotEmpty |
| 122 | ? Column( |
| 123 | children: _getRepresentativeWidgets(context, reps), |
| 124 | ) |
| 125 | : SizedBox(), |
| 126 | ), |
| 127 | bottomSectionPadding: EdgeInsets.only(bottom: 24), |
| 128 | bottomSection: Observer( |
| 129 | builder: (_) => Row( |
| 130 | mainAxisAlignment: MainAxisAlignment.center, |
| 131 | children: <Widget>[ |
| 132 | Flexible( |
| 133 | child: Container( |
| 134 | padding: EdgeInsets.only(right: 8.0), |
| 135 | child: LoadingPrimaryButton( |
| 136 | onPressed: () => _onSubmit(context), |
| 137 | text: S.of(context).change, |
| 138 | color: Theme.of(context).colorScheme.primary, |
| 139 | textColor: Theme.of(context).colorScheme.onPrimary, |
| 140 | ), |
| 141 | )), |
| 142 | ], |
| 143 | )), |
| 144 | ), |
| 145 | ); |
| 146 | }, |
| 147 | ), |
| 148 | ); |
| 149 | } |
| 150 | |
| 151 | Future<void> _onSubmit(BuildContext context) async { |
| 152 | if (_formKey.currentState != null && !_formKey.currentState!.validate()) { |
| 153 | return; |
| 154 | } |
| 155 | |
| 156 | final confirmed = await showPopUp<bool>( |
| 157 | context: context, |
| 158 | builder: (BuildContext context) { |
| 159 | return AlertWithTwoActions( |
| 160 | alertTitle: S.of(context).change_rep, |
| 161 | alertContent: S.of(context).change_rep_message, |
| 162 | rightButtonText: S.of(context).change, |
| 163 | leftButtonText: S.of(context).cancel, |
| 164 | actionRightButton: () => Navigator.pop(context, true), |
| 165 | actionLeftButton: () => Navigator.pop(context, false)); |
| 166 | }) ?? |
| 167 | false; |
| 168 | |
| 169 | if (confirmed) { |
| 170 | try { |
| 171 | _settingsStore.defaultNanoRep = _addressController.text; |
| 172 | |
| 173 | await nano!.changeRep(_wallet, _addressController.text); |
| 174 | |
| 175 | // reset this flag whenever we successfully change reps: |
| 176 | _settingsStore.shouldShowRepWarning = true; |
| 177 | |
| 178 | await showPopUp<void>( |
| 179 | context: context, |
| 180 | builder: (BuildContext context) { |
| 181 | return AlertWithOneAction( |
| 182 | alertTitle: S.of(context).successful, |
| 183 | alertContent: S.of(context).change_rep_successful, |
| 184 | buttonText: S.of(context).ok, |
| 185 | buttonAction: () => Navigator.pop(context)); |
| 186 | }); |
| 187 | } catch (e) { |
| 188 | await showPopUp<void>( |
| 189 | context: context, |
| 190 | builder: (BuildContext context) { |
| 191 | return AlertWithOneAction( |
| 192 | alertTitle: S.of(context).error, |
| 193 | alertContent: e.toString(), |
| 194 | buttonText: S.of(context).ok, |
| 195 | buttonAction: () => Navigator.pop(context)); |
| 196 | }); |
| 197 | } |
| 198 | } |
| 199 | } |
| 200 | |
| 201 | List<Widget> _getRepresentativeWidgets(BuildContext context, List<N2Node>? list) { |
| 202 | if (list == null) { |
| 203 | return []; |
| 204 | } |
| 205 | final List<Widget> ret = []; |
| 206 | for (final N2Node node in list) { |
| 207 | if (node.alias != null && node.alias!.trim().isNotEmpty) { |
| 208 | bool divider = node != list.first; |
| 209 | ret.add(_buildSingleRepresentative(context, node, divider: divider, isList: true)); |
| 210 | } |
| 211 | } |
| 212 | return ret; |
| 213 | } |
| 214 | |
| 215 | Widget _buildSingleRepresentative( |
| 216 | BuildContext context, |
| 217 | N2Node rep, { |
| 218 | bool isList = true, |
| 219 | bool divider = false, |
| 220 | }) { |
| 221 | return Column( |
| 222 | children: <Widget>[ |
| 223 | if (divider) Divider(height: 2), |
| 224 | TextButton( |
| 225 | style: TextButton.styleFrom( |
| 226 | padding: EdgeInsets.zero, |
| 227 | ), |
| 228 | onPressed: () async { |
| 229 | if (!isList) { |
| 230 | return; |
| 231 | } |
| 232 | _addressController.text = rep.account!; |
| 233 | }, |
| 234 | child: Container( |
| 235 | margin: const EdgeInsets.symmetric(vertical: 20), |
| 236 | child: Row( |
| 237 | mainAxisAlignment: MainAxisAlignment.spaceBetween, |
| 238 | crossAxisAlignment: CrossAxisAlignment.center, |
| 239 | children: <Widget>[ |
| 240 | Container( |
| 241 | margin: const EdgeInsetsDirectional.only(start: 24), |
| 242 | width: MediaQuery.of(context).size.width * 0.50, |
| 243 | child: Column( |
| 244 | mainAxisAlignment: MainAxisAlignment.center, |
| 245 | crossAxisAlignment: CrossAxisAlignment.start, |
| 246 | children: <Widget>[ |
| 247 | Text( |
| 248 | rep.alias ?? rep.account!, |
| 249 | style: Theme.of(context).textTheme.bodyMedium!.copyWith( |
| 250 | color: Theme.of(context).colorScheme.onSurface, |
| 251 | fontWeight: FontWeight.w700, |
| 252 | fontSize: rep.alias == null ? 14 : 18, |
| 253 | ), |
| 254 | ), |
| 255 | Container( |
| 256 | margin: const EdgeInsets.only(top: 7), |
| 257 | child: RichText( |
| 258 | text: TextSpan( |
| 259 | text: "${S.current.voting_weight}: ${rep.weight.toString()}%", |
| 260 | style: Theme.of(context).textTheme.bodyMedium!.copyWith( |
| 261 | color: Theme.of(context).colorScheme.onSurfaceVariant, |
| 262 | fontWeight: FontWeight.w700, |
| 263 | ), |
| 264 | ), |
| 265 | ), |
| 266 | ), |
| 267 | Container( |
| 268 | margin: const EdgeInsets.only(top: 4), |
| 269 | child: RichText( |
| 270 | text: TextSpan( |
| 271 | text: '', |
| 272 | children: [ |
| 273 | TextSpan( |
| 274 | text: "${S.current.uptime}: ", |
| 275 | style: Theme.of(context).textTheme.bodyMedium!.copyWith( |
| 276 | color: Theme.of(context).colorScheme.onSurfaceVariant, |
| 277 | fontWeight: FontWeight.w700, |
| 278 | ), |
| 279 | ), |
| 280 | TextSpan( |
| 281 | text: rep.uptime, |
| 282 | style: Theme.of(context).textTheme.bodyMedium!.copyWith( |
| 283 | color: Theme.of(context).colorScheme.onSurfaceVariant, |
| 284 | fontWeight: FontWeight.w900, |
| 285 | ), |
| 286 | ), |
| 287 | ], |
| 288 | ), |
| 289 | ), |
| 290 | ), |
| 291 | ], |
| 292 | ), |
| 293 | ), |
| 294 | Container( |
| 295 | margin: const EdgeInsetsDirectional.only(end: 24, start: 14), |
| 296 | child: Stack( |
| 297 | children: <Widget>[ |
| 298 | Icon( |
| 299 | Icons.verified, |
| 300 | color: Theme.of(context).colorScheme.primary, |
| 301 | size: 50, |
| 302 | ), |
| 303 | Positioned.fill( |
| 304 | child: Container( |
| 305 | margin: EdgeInsets.all(13), |
| 306 | color: Theme.of(context).colorScheme.primary, |
| 307 | ), |
| 308 | ), |
| 309 | Container( |
| 310 | alignment: const AlignmentDirectional(-0.03, 0.03), |
| 311 | width: 50, |
| 312 | height: 50, |
| 313 | child: Text( |
| 314 | (rep.score).toString(), |
| 315 | textAlign: TextAlign.center, |
| 316 | style: Theme.of(context).textTheme.bodyMedium!.copyWith( |
| 317 | fontWeight: FontWeight.w800, |
| 318 | ), |
| 319 | ), |
| 320 | ), |
| 321 | ], |
| 322 | ), |
| 323 | ), |
| 324 | ], |
| 325 | ), |
| 326 | ), |
| 327 | ), |
| 328 | ], |
| 329 | ); |
| 330 | } |
| 331 | } |