| 1 | import 'package:cake_wallet/entities/qr_view_data.dart'; |
| 2 | import 'package:cake_wallet/src/widgets/primary_button.dart'; |
| 3 | import 'package:cake_wallet/routes.dart'; |
| 4 | import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart'; |
| 5 | import 'package:cake_wallet/src/screens/receive/widgets/currency_input_field.dart'; |
| 6 | import 'package:cake_wallet/generated/i18n.dart'; |
| 7 | import 'package:cake_wallet/src/screens/receive/widgets/qr_image.dart'; |
| 8 | import 'package:cake_wallet/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart'; |
| 9 | import 'package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart'; |
| 10 | import 'package:cake_wallet/utils/address_formatter.dart'; |
| 11 | import 'package:cake_wallet/utils/brightness_util.dart'; |
| 12 | import 'package:cake_wallet/utils/responsive_layout_util.dart'; |
| 13 | import 'package:cake_wallet/utils/show_bar.dart'; |
| 14 | import 'package:cake_wallet/utils/show_pop_up.dart'; |
| 15 | import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart'; |
| 16 | import 'package:cw_core/crypto_currency.dart'; |
| 17 | import 'package:cw_core/payment_uris.dart'; |
| 18 | import 'package:flutter/material.dart'; |
| 19 | import 'package:flutter/services.dart'; |
| 20 | import 'package:flutter_mobx/flutter_mobx.dart'; |
| 21 | import 'package:url_launcher/url_launcher.dart'; |
| 22 | |
| 23 | class QRWidget extends StatelessWidget { |
| 24 | QRWidget({ |
| 25 | required this.addressListViewModel, |
| 26 | this.qrVersion, |
| 27 | this.heroTag, |
| 28 | required this.amountController, |
| 29 | required this.formKey, |
| 30 | this.amountTextFieldFocusNode, |
| 31 | }); |
| 32 | |
| 33 | final WalletAddressListViewModel addressListViewModel; |
| 34 | final TextEditingController amountController; |
| 35 | final FocusNode? amountTextFieldFocusNode; |
| 36 | final GlobalKey<FormState> formKey; |
| 37 | final int? qrVersion; |
| 38 | final String? heroTag; |
| 39 | |
| 40 | PaymentURI get addressUri { |
| 41 | return addressListViewModel.uri; |
| 42 | } |
| 43 | |
| 44 | @override |
| 45 | Widget build(BuildContext context) { |
| 46 | final copyImage = Image.asset( |
| 47 | 'assets/images/copy_address.png', |
| 48 | color: Theme.of(context).colorScheme.onSurfaceVariant, |
| 49 | ); |
| 50 | |
| 51 | // This magic number for wider screen sets the text input focus at center of the inputfield |
| 52 | final _width = |
| 53 | responsiveLayoutUtil.shouldRenderMobileUI ? MediaQuery.of(context).size.width : 500; |
| 54 | |
| 55 | return Center( |
| 56 | child: SingleChildScrollView( |
| 57 | child: Column( |
| 58 | mainAxisSize: MainAxisSize.min, |
| 59 | mainAxisAlignment: MainAxisAlignment.center, |
| 60 | crossAxisAlignment: CrossAxisAlignment.center, |
| 61 | children: <Widget>[ |
| 62 | Column( |
| 63 | children: [ |
| 64 | Row( |
| 65 | children: <Widget>[ |
| 66 | Spacer(flex: 3), |
| 67 | Observer( |
| 68 | builder: (_) => Flexible( |
| 69 | flex: 9, |
| 70 | child: GestureDetector( |
| 71 | onTap: () { |
| 72 | BrightnessUtil.changeBrightnessForFunction( |
| 73 | () async { |
| 74 | await Navigator.pushNamed( |
| 75 | context, |
| 76 | Routes.fullscreenQR, |
| 77 | arguments: QrViewData( |
| 78 | embeddedImagePath: addressListViewModel.qrImage, |
| 79 | data: addressUri.toString(), |
| 80 | heroTag: heroTag, |
| 81 | ), |
| 82 | ); |
| 83 | }, |
| 84 | ); |
| 85 | }, |
| 86 | child: Hero( |
| 87 | tag: Key(heroTag ?? addressUri.toString()), |
| 88 | child: Center( |
| 89 | child: Container( |
| 90 | padding: EdgeInsets.zero, |
| 91 | decoration: BoxDecoration( |
| 92 | border: Border(top: BorderSide.none), |
| 93 | borderRadius: BorderRadius.all(Radius.circular(5)), |
| 94 | color: Theme.of(context).colorScheme.surface, |
| 95 | ), |
| 96 | child: Column( |
| 97 | children: [ |
| 98 | ClipRRect( |
| 99 | borderRadius: BorderRadius.circular(12.5), |
| 100 | child: AspectRatio( |
| 101 | aspectRatio: 1.0, |
| 102 | child: QrImage( |
| 103 | embeddedImagePath: addressListViewModel.qrImage, |
| 104 | data: addressUri.toString(), |
| 105 | size: 230, |
| 106 | ), |
| 107 | ), |
| 108 | ), |
| 109 | if (addressListViewModel.isPayjoinUnavailable && |
| 110 | !addressListViewModel.isSilentPayments && |
| 111 | !addressListViewModel.isLightning && |
| 112 | !addressListViewModel.isBitcoinViewOnly) ...[ |
| 113 | GestureDetector( |
| 114 | onTap: () => _onPayjoinInactivePressed(context), |
| 115 | child: Row( |
| 116 | mainAxisAlignment: MainAxisAlignment.center, |
| 117 | children: [ |
| 118 | Padding( |
| 119 | padding: EdgeInsets.only( |
| 120 | top: 4, |
| 121 | bottom: 4, |
| 122 | right: 4, |
| 123 | ), |
| 124 | child: Image.asset( |
| 125 | 'assets/images/payjoin.png', |
| 126 | width: 20, |
| 127 | ), |
| 128 | ), |
| 129 | Text( |
| 130 | S.of(context).payjoin_unavailable, |
| 131 | style: |
| 132 | Theme.of(context).textTheme.bodySmall?.copyWith( |
| 133 | fontWeight: FontWeight.w600, |
| 134 | color: Theme.of(context) |
| 135 | .colorScheme |
| 136 | .onSurfaceVariant, |
| 137 | ), |
| 138 | ), |
| 139 | Padding( |
| 140 | padding: EdgeInsets.only( |
| 141 | top: 4, |
| 142 | bottom: 4, |
| 143 | left: 4, |
| 144 | ), |
| 145 | child: CircleAvatar( |
| 146 | radius: 7, |
| 147 | backgroundColor: Colors.black, |
| 148 | child: Icon( |
| 149 | Icons.question_mark, |
| 150 | size: 10, |
| 151 | color: Colors.white, |
| 152 | ), |
| 153 | ), |
| 154 | ), |
| 155 | ], |
| 156 | ), |
| 157 | ), |
| 158 | ], |
| 159 | if (addressListViewModel.isPayjoinAvailable) ...[ |
| 160 | Row( |
| 161 | mainAxisAlignment: MainAxisAlignment.center, |
| 162 | children: [ |
| 163 | Padding( |
| 164 | padding: EdgeInsets.only( |
| 165 | top: 4, |
| 166 | bottom: 4, |
| 167 | right: 4, |
| 168 | ), |
| 169 | child: Image.asset( |
| 170 | 'assets/images/payjoin.png', |
| 171 | width: 20, |
| 172 | ), |
| 173 | ), |
| 174 | Text( |
| 175 | S.of(context).payjoin_enabled, |
| 176 | style: Theme.of(context).textTheme.bodySmall?.copyWith( |
| 177 | fontWeight: FontWeight.w600, |
| 178 | ), |
| 179 | ), |
| 180 | ], |
| 181 | ), |
| 182 | ] |
| 183 | ], |
| 184 | ), |
| 185 | ), |
| 186 | ), |
| 187 | ), |
| 188 | ), |
| 189 | ), |
| 190 | ), |
| 191 | Spacer(flex: 3) |
| 192 | ], |
| 193 | ), |
| 194 | ], |
| 195 | ), |
| 196 | Observer( |
| 197 | builder: (_) => Row( |
| 198 | children: <Widget>[ |
| 199 | Expanded( |
| 200 | child: Form( |
| 201 | key: formKey, |
| 202 | child: CurrencyAmountTextField( |
| 203 | hasUnderlineBorder: true, |
| 204 | borderWidth: 0.0, |
| 205 | selectedCurrency: _currencyName, |
| 206 | selectedCurrencyDecimals: |
| 207 | addressListViewModel.selectedCurrencyDecimals, |
| 208 | amountFocusNode: amountTextFieldFocusNode, |
| 209 | amountController: amountController, |
| 210 | padding: EdgeInsets.only(top: 20, left: _width / 4), |
| 211 | isAmountEditable: true, |
| 212 | tag: addressListViewModel.selectedCurrency.tag, |
| 213 | onTapPicker: () => _presentPicker(context), |
| 214 | isPickerEnable: true, |
| 215 | ), |
| 216 | ), |
| 217 | ), |
| 218 | ], |
| 219 | )), |
| 220 | Divider(height: 1, color: Theme.of(context).colorScheme.outlineVariant), |
| 221 | Padding( |
| 222 | padding: EdgeInsets.only(top: 12, bottom: 8), |
| 223 | child: Builder( |
| 224 | builder: (context) => Observer( |
| 225 | builder: (context) => GestureDetector( |
| 226 | onTap: () { |
| 227 | Clipboard.setData(ClipboardData(text: addressUri.address)); |
| 228 | showBar<void>(context, S.of(context).copied_to_clipboard); |
| 229 | }, |
| 230 | child: Row( |
| 231 | mainAxisSize: MainAxisSize.max, |
| 232 | crossAxisAlignment: CrossAxisAlignment.start, |
| 233 | children: <Widget>[ |
| 234 | Expanded( |
| 235 | child: AddressFormatter.buildSegmentedAddress( |
| 236 | address: addressUri.address, |
| 237 | walletType: addressListViewModel.type, |
| 238 | textAlign: TextAlign.center, |
| 239 | evenTextStyle: Theme.of(context).textTheme.bodyMedium!.copyWith( |
| 240 | fontSize: 15, |
| 241 | fontWeight: FontWeight.w500, |
| 242 | color: Theme.of(context).colorScheme.onSurface, |
| 243 | ), |
| 244 | ), |
| 245 | ), |
| 246 | Padding( |
| 247 | padding: EdgeInsets.only(left: 12), |
| 248 | child: copyImage, |
| 249 | ) |
| 250 | ], |
| 251 | ), |
| 252 | ), |
| 253 | ), |
| 254 | ), |
| 255 | ), |
| 256 | Observer( |
| 257 | builder: (_) => Offstage( |
| 258 | offstage: !addressListViewModel.isPayjoinAvailable, |
| 259 | child: Padding( |
| 260 | padding: EdgeInsets.only(top: 12), |
| 261 | child: PrimaryImageButton( |
| 262 | onPressed: () { |
| 263 | Clipboard.setData(ClipboardData(text: addressUri.toString())); |
| 264 | showBar<void>(context, S.of(context).copied_to_clipboard); |
| 265 | }, |
| 266 | image: Image.asset( |
| 267 | 'assets/images/payjoin.png', |
| 268 | width: 25, |
| 269 | ), |
| 270 | text: S.of(context).copy_payjoin_address, |
| 271 | color: Theme.of(context).colorScheme.surfaceContainer, |
| 272 | textColor: Theme.of(context).colorScheme.onSurface, |
| 273 | ), |
| 274 | ), |
| 275 | ), |
| 276 | ), |
| 277 | ], |
| 278 | ), |
| 279 | ), |
| 280 | ); |
| 281 | } |
| 282 | |
| 283 | String get _currencyName { |
| 284 | if (addressListViewModel.selectedCurrency is CryptoCurrency) { |
| 285 | if (addressListViewModel.useSatoshi) return "SATS"; |
| 286 | return (addressListViewModel.selectedCurrency as CryptoCurrency).title.toUpperCase(); |
| 287 | } |
| 288 | return addressListViewModel.selectedCurrency.name.toUpperCase(); |
| 289 | } |
| 290 | |
| 291 | void _presentPicker(BuildContext context) async { |
| 292 | await showPopUp<void>( |
| 293 | builder: (_) => CurrencyPicker( |
| 294 | selectedAtIndex: addressListViewModel.selectedCurrencyIndex, |
| 295 | items: addressListViewModel.currencies, |
| 296 | hintText: S.of(context).search_currency, |
| 297 | onItemSelected: addressListViewModel.selectCurrency, |
| 298 | ), |
| 299 | context: context, |
| 300 | ); |
| 301 | // update amount if currency changed |
| 302 | addressListViewModel.changeAmount(amountController.text); |
| 303 | } |
| 304 | |
| 305 | void _onPayjoinInactivePressed(BuildContext context) { |
| 306 | showModalBottomSheet( |
| 307 | context: context, |
| 308 | builder: (context) => InfoBottomSheet( |
| 309 | titleText: S.of(context).payjoin_unavailable_sheet_title, |
| 310 | content: S.of(context).payjoin_unavailable_sheet_content, |
| 311 | footerType: FooterType.doubleActionButton, |
| 312 | doubleActionLeftButtonText: S.of(context).learn_more, |
| 313 | onLeftActionButtonPressed: () => launchUrl( |
| 314 | Uri.parse("https://docs.cakewallet.com/cryptos/bitcoin/#payjoin"), |
| 315 | mode: LaunchMode.externalApplication), |
| 316 | doubleActionRightButtonText: S.of(context).ok, |
| 317 | onRightActionButtonPressed: () => Navigator.of(context).pop(), |
| 318 | ), |
| 319 | ); |
| 320 | } |
| 321 | } |