dev
dart 400 lines 17.8 KB
Raw
1 import "package:cake_wallet/anonpay/anonpay_donation_link_info.dart";
2 import "package:cake_wallet/bitcoin/bitcoin.dart";
3 import "package:cake_wallet/core/utilities.dart";
4 import "package:cake_wallet/di.dart";
5 import "package:cake_wallet/entities/auto_generate_subaddress_status.dart";
6 import "package:cake_wallet/entities/preferences_key.dart";
7 import "package:cake_wallet/generated/i18n.dart";
8 import "package:cake_wallet/new-ui/widgets/modern_button.dart";
9 import "package:cake_wallet/new-ui/widgets/receive_page/payjoin_copy_modal.dart";
10 import "package:cake_wallet/new-ui/widgets/receive_page/receive_address_type.dart";
11 import "package:cake_wallet/new-ui/widgets/receive_page/receive_address_widget.dart";
12 import "package:cake_wallet/new-ui/widgets/receive_page/receive_amount_display.dart";
13 import "package:cake_wallet/new-ui/widgets/receive_page/receive_amount_modal.dart";
14 import "package:cake_wallet/new-ui/widgets/receive_page/receive_bottom_buttons.dart";
15 import "package:cake_wallet/new-ui/widgets/receive_page/receive_info_box.dart";
16 import "package:cake_wallet/new-ui/widgets/receive_page/receive_label_modal.dart";
17 import "package:cake_wallet/new-ui/widgets/receive_page/receive_label_widget.dart";
18 import "package:cake_wallet/new-ui/widgets/receive_page/receive_large_amount_preview.dart";
19 import "package:cake_wallet/new-ui/widgets/receive_page/receive_qr_code.dart";
20 import "package:cake_wallet/new-ui/widgets/receive_page/receive_token_display.dart";
21 import "package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
22 import "package:cake_wallet/routes.dart";
23 import "package:cake_wallet/src/screens/receive/anonpay_receive_page.dart";
24 import "package:cake_wallet/utils/share_util.dart";
25 import "package:cake_wallet/view_model/dashboard/dashboard_view_model.dart";
26 import "package:cake_wallet/view_model/dashboard/receive_option_view_model.dart";
27 import "package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart";
28 import "package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart";
29 import "package:cake_wallet/zcash/zcash.dart";
30 import "package:cw_core/crypto_currency.dart";
31 import "package:cw_core/payment_uris.dart";
32 import "package:cw_core/receive_page_option.dart";
33 import "package:cw_core/utils/print_verbose.dart";
34 import "package:cw_core/wallet_type.dart";
35 import "package:flutter/cupertino.dart";
36 import "package:flutter/material.dart";
37 import "package:flutter/services.dart";
38 import "package:flutter_mobx/flutter_mobx.dart";
39 import "package:mobx/mobx.dart";
40 import "package:modal_bottom_sheet/modal_bottom_sheet.dart";
41 import "package:shared_preferences/shared_preferences.dart";
42
43 class NewReceivePage extends StatefulWidget {
44 NewReceivePage({
45 required this.addressListViewModel,
46 required this.receiveOptionViewModel,
47 required this.dashboardViewModel,
48 required this.lightningMode,
49 super.key,
50 CryptoCurrency? initialCurrency,
51 }) {
52 if (initialCurrency != null && initialCurrency != addressListViewModel.selectedCurrency) {
53 addressListViewModel.setTokenCurrency(initialCurrency);
54 }
55 }
56
57 final WalletAddressListViewModel addressListViewModel;
58 final ReceiveOptionViewModel receiveOptionViewModel;
59 final DashboardViewModel dashboardViewModel;
60 final bool lightningMode;
61
62 @override
63 State<NewReceivePage> createState() => _NewReceivePageState();
64 }
65
66 class _NewReceivePageState extends State<NewReceivePage> {
67 bool _largeQrMode = false;
68 late WalletAddressListItem? _addressItemWithLabel;
69
70 @override
71 void initState() {
72 super.initState();
73
74 WidgetsBinding.instance.addPostFrameCallback((_) {
75 if (widget.lightningMode) {
76 widget.receiveOptionViewModel.selectReceiveOption(
77 widget.receiveOptionViewModel.options
78 .firstWhereOrNull((item) => item.value.contains("Lightning")) ??
79 ReceivePageOption.mainnet,
80 );
81 widget.addressListViewModel.selectedCurrency = CryptoCurrency.btcln;
82 } else if (widget.addressListViewModel.wallet.type == WalletType.bitcoin) {
83 widget.receiveOptionViewModel.selectReceiveOption(
84 widget.receiveOptionViewModel.options
85 .firstWhereOrNull((item) => item.value.contains("Standard")) ??
86 ReceivePageOption.mainnet,
87 );
88 widget.addressListViewModel.selectedCurrency = CryptoCurrency.btc;
89 }
90 });
91
92 reaction((_) => widget.addressListViewModel.uri, _reloadAddressWithLabel);
93
94 _addressItemWithLabel = widget.addressListViewModel.forceRecomputeItems.firstWhereOrNull(
95 (item) =>
96 item is WalletAddressListItem && item.address == widget.addressListViewModel.uri.address,
97 ) as WalletAddressListItem?;
98
99 reaction((_) => widget.receiveOptionViewModel.selectedReceiveOption, (option) {
100 // The infobox depends on the selected address type, so the page has to be
101 // rebuilt when it changes.
102 if (mounted) setState(() {});
103
104 if (widget.dashboardViewModel.type == WalletType.bitcoin &&
105 bitcoin!.isBitcoinReceivePageOption(option)) {
106 widget.addressListViewModel.setAddressType(bitcoin!.getOptionToType(option));
107 if (option.value.contains("Lightning")) {
108 widget.addressListViewModel.selectedCurrency = CryptoCurrency.btcln;
109 } else {
110 widget.addressListViewModel.selectedCurrency = CryptoCurrency.btc;
111 }
112 return;
113 }
114 if (widget.dashboardViewModel.type == WalletType.zcash) {
115 widget.addressListViewModel.setAddressType(zcash!.getOptionToType(option));
116 return;
117 }
118
119 switch (option) {
120 case ReceivePageOption.anonPayInvoice:
121 Navigator.pushNamed(
122 context,
123 Routes.anonPayInvoicePage,
124 arguments: [widget.addressListViewModel.address.address, option],
125 );
126 break;
127 case ReceivePageOption.anonPayDonationLink:
128 final sharedPreferences = getIt.get<SharedPreferences>();
129 final clearnetUrl = sharedPreferences.getString(PreferencesKey.clearnetDonationLink);
130 final onionUrl = sharedPreferences.getString(PreferencesKey.onionDonationLink);
131 final donationWalletName =
132 sharedPreferences.getString(PreferencesKey.donationLinkWalletName);
133
134 if (clearnetUrl != null &&
135 onionUrl != null &&
136 widget.addressListViewModel.wallet.name == donationWalletName) {
137 Navigator.pushNamed(
138 context,
139 Routes.anonPayReceivePage,
140 arguments: AnonPayReceivePageArgs(
141 invoiceInfo: AnonpayDonationLinkInfo(
142 clearnetUrl: clearnetUrl,
143 onionUrl: onionUrl,
144 address: widget.addressListViewModel.address.address,
145 ),
146 qrImage: widget.addressListViewModel.qrImage,
147 ),
148 );
149 } else {
150 Navigator.pushNamed(
151 context,
152 Routes.anonPayInvoicePage,
153 arguments: [widget.addressListViewModel.address.address, option],
154 );
155 }
156 break;
157 default:
158 if ([WalletType.bitcoin, WalletType.litecoin]
159 .contains(widget.addressListViewModel.type)) {
160 widget.addressListViewModel.setAddressType(bitcoin!.getBitcoinAddressType(option));
161 }
162 if (widget.addressListViewModel.type == WalletType.zcash) {
163 printV("help me i'll kms if that wont work: ${zcash!.getZcashAddressType(option)}");
164 widget.addressListViewModel.setAddressType(zcash!.getZcashAddressType(option));
165 }
166 }
167 });
168 }
169
170 @override
171 Widget build(BuildContext context) {
172 final hasAddressTypeSelector = widget.receiveOptionViewModel.options.length > 1;
173 final hasLabel = _addressItemWithLabel?.name != null && _addressItemWithLabel!.name!.isNotEmpty;
174 final infoboxDismissed = widget.addressListViewModel.wallet.walletInfo.receiveInfoboxDismissed;
175 final infobox = ReceiveInfoBox.forWalletType(
176 widget.addressListViewModel.type,
177 supportedCurrencies:
178 widget.addressListViewModel.tokenCurrencies.whereType<CryptoCurrency>().toList(),
179 onDismissed: () {
180 widget.addressListViewModel.dismissInfobox();
181 setState(() {});
182 },
183 autoGenerateSubaddressStatus: widget.lightningMode
184 ? AutoGenerateSubaddressStatus.disabled
185 : widget.dashboardViewModel.settingsStore.autoGenerateSubaddressStatus,
186 addressRotates: _selectedAddressRotates,
187 );
188
189 return Container(
190 decoration: BoxDecoration(
191 gradient: LinearGradient(
192 colors: [
193 Theme.of(context).colorScheme.surfaceBright,
194 Theme.of(context).colorScheme.surface,
195 ],
196 begin: Alignment.topCenter,
197 end: Alignment.bottomCenter,
198 ),
199 borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
200 ),
201 child: SafeArea(
202 child: Column(
203 mainAxisSize: MainAxisSize.max,
204 mainAxisAlignment: MainAxisAlignment.start,
205 children: [
206 ModalTopBar(
207 title: _largeQrMode ? "" : S.of(context).receive,
208 leadingIcon: const Icon(Icons.close),
209 leadingSemanticLabel: S.of(context).close,
210 onLeadingPressed: () {
211 Navigator.of(context, rootNavigator: true).pop();
212 },
213 trailingWidget: Observer(
214 builder: (_) => AnimatedSwitcher(
215 duration: const Duration(milliseconds: 300),
216 child: _largeQrMode ||
217 widget.addressListViewModel.hasAddressRotation
218 /* TODO rotating is broken on mweb, disabling for now, fix after mvp*/
219 &&
220 !(widget.receiveOptionViewModel.selectedReceiveOption.description ??
221 "")
222 .toLowerCase()
223 .contains("mweb")
224 ? ModernButton(
225 key: ValueKey(_largeQrMode),
226 size: 36,
227 icon: _largeQrMode
228 ? const Icon(Icons.share)
229 : widget.addressListViewModel.isRotatingAddress
230 ? const CupertinoActivityIndicator()
231 : const Icon(Icons.refresh),
232 semanticLabel: _largeQrMode
233 ? S.of(context).share_address
234 : S.of(context).rotate_address,
235 onPressed: () {
236 if (_largeQrMode) {
237 ShareUtil.share(
238 text: widget.addressListViewModel.uri.toString(),
239 context: context,
240 );
241 } else {
242 if (widget.addressListViewModel.hasAddressRotation) {
243 widget.addressListViewModel.rotateAddress();
244 }
245 }
246 },
247 )
248 : const SizedBox.shrink(),
249 ),
250 ),
251 ),
252 Expanded(
253 child: Column(
254 mainAxisAlignment: MainAxisAlignment.spaceEvenly,
255 mainAxisSize: MainAxisSize.max,
256 children: [
257 ReceiveAmountDisplay(
258 walletAddressListViewModel: widget.addressListViewModel,
259 largeQrMode: _largeQrMode,
260 ),
261 ReceiveQrCode(
262 addressListViewModel: widget.addressListViewModel,
263 onTap: () {
264 setState(() {
265 _largeQrMode = !_largeQrMode;
266 // _infoboxDimissed = true;
267 widget.addressListViewModel.dismissInfobox();
268 });
269 },
270 largeQrMode: _largeQrMode,
271 ),
272 if (widget.addressListViewModel.tokenCurrency != null)
273 ReceiveTokenDisplay(addressListViewModel: widget.addressListViewModel),
274 if (hasAddressTypeSelector)
275 ReceiveAddressTypeDisplay(
276 lightningMode: widget.lightningMode,
277 receiveOptionViewModel: widget.receiveOptionViewModel,
278 largeQrMode: _largeQrMode,
279 ),
280 ReceiveAddressWidget(
281 addressListViewModel: widget.addressListViewModel,
282 ),
283 // The label chip animates to zero height when there is no
284 // label (or in large QR mode); keep it out of the semantics
285 // tree entirely while it is collapsed.
286 ExcludeSemantics(
287 excluding: _largeQrMode || !hasLabel,
288 child: MergeSemantics(
289 child: Semantics(
290 button: true,
291 hint: S.of(context).set_label,
292 child: GestureDetector(
293 onTap: _showLabelModal,
294 child: ReceiveLabelWidget(
295 name: _addressItemWithLabel?.name ?? "",
296 largeQrMode: _largeQrMode,
297 ),
298 ),
299 ),
300 ),
301 ),
302 Observer(
303 builder: (_) => ReceiveBottomButtons(
304 key: const ValueKey(0),
305 largeQrMode: _largeQrMode,
306 showAccountsButton: widget.addressListViewModel.hasAddressList,
307 showLabelButton: widget.addressListViewModel.hasAddressList && !hasLabel,
308 copyData: widget.addressListViewModel.hasPayjoin
309 ? null
310 : ClipboardData(
311 text: widget.addressListViewModel.displayAmount.isEmpty
312 ? widget.addressListViewModel.uri.address
313 : widget.addressListViewModel.uri.toString(),
314 ),
315 onCopyButtonPressed: () {
316 if (widget.addressListViewModel.hasPayjoin) {
317 showModalBottomSheet(
318 isScrollControlled: true,
319 context: context,
320 builder: (context) =>
321 PayjoinCopyModal(uri: widget.addressListViewModel.uri),
322 );
323 }
324 },
325 onAmountButtonPressed: () {
326 showMaterialModalBottomSheet(
327 context: context,
328 backgroundColor: Colors.transparent,
329 barrierColor: Colors.black.withAlpha(80),
330 builder: (context) => ReceiveAmountModal(
331 walletAddressListViewModel: widget.addressListViewModel,
332 onSubmitted: (amount) {},
333 ),
334 );
335 },
336 onLabelButtonPressed: _showLabelModal,
337 onAccountsButtonPressed: () {
338 Navigator.of(context).pushNamed(
339 Routes.receiveAddresses,
340 arguments: false,
341 );
342 },
343 ),
344 ),
345 ReceiveLargeAmountPreview(
346 amount: widget.addressListViewModel.displayAmount,
347 currency: widget.addressListViewModel.cryptoCurrencySymbol,
348 largeQrMode: _largeQrMode,
349 ),
350 if (infobox != null && !widget.addressListViewModel.isLightning)
351 ClipRect(
352 child: AnimatedAlign(
353 duration: const Duration(milliseconds: 200),
354 curve: Curves.easeOutCubic,
355 heightFactor: infoboxDismissed ? 0 : 1,
356 alignment: Alignment.center,
357 child: AnimatedOpacity(
358 duration: const Duration(milliseconds: 200),
359 opacity: infoboxDismissed ? 0 : 1,
360 curve: Curves.easeOutCubic,
361 child: infobox,
362 ),
363 ),
364 ),
365 ],
366 ),
367 ),
368 ],
369 ),
370 ),
371 );
372 }
373
374 /// Zcash is the only wallet type that also offers static address types, so
375 /// the rotation notice must not be shown for it unless the disposable
376 /// transparent type is the one currently selected.
377 bool get _selectedAddressRotates =>
378 widget.addressListViewModel.type != WalletType.zcash ||
379 zcash!.isRotatingAddressOption(widget.receiveOptionViewModel.selectedReceiveOption);
380
381 void _showLabelModal() {
382 showMaterialModalBottomSheet(
383 context: context,
384 backgroundColor: Colors.transparent,
385 barrierColor: Colors.black.withAlpha(80),
386 builder: (_) => getIt.get<ReceiveLabelModal>(param1: _addressItemWithLabel),
387 ).then((value) {
388 _reloadAddressWithLabel(widget.addressListViewModel.uri);
389 });
390 }
391
392 void _reloadAddressWithLabel(PaymentURI newAddress) {
393 // FIXME: viewmodel doesn't want to load address name here, so we make it. investigate why later
394 setState(() {
395 _addressItemWithLabel = widget.addressListViewModel.forceRecomputeItems.firstWhereOrNull(
396 (item) => item is WalletAddressListItem && item.address == newAddress.address,
397 ) as WalletAddressListItem?;
398 });
399 }
400 }