diff --git a/assets/images/radar.png b/assets/images/radar.png new file mode 100644 index 000000000..386511418 Binary files /dev/null and b/assets/images/radar.png differ diff --git a/cw_core/lib/payment_uris.dart b/cw_core/lib/payment_uris.dart index daf56af2f..9e4191072 100644 --- a/cw_core/lib/payment_uris.dart +++ b/cw_core/lib/payment_uris.dart @@ -7,6 +7,13 @@ abstract class PaymentURI { final String address; } +class ExternalAddressURI extends PaymentURI { + ExternalAddressURI({required super.amount, required super.address}); + + @override + String toString() => address; +} + class MoneroURI extends PaymentURI { MoneroURI({required super.amount, required super.address}); diff --git a/lib/new-ui/widgets/send_page/l2_send_external_modal.dart b/lib/new-ui/widgets/send_page/l2_send_external_modal.dart index 4ba56fb34..d41ab0793 100644 --- a/lib/new-ui/widgets/send_page/l2_send_external_modal.dart +++ b/lib/new-ui/widgets/send_page/l2_send_external_modal.dart @@ -5,13 +5,12 @@ import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart'; import 'package:cake_wallet/src/screens/receive/widgets/qr_image.dart'; import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; import 'package:cake_wallet/utils/address_formatter.dart'; +import 'package:cake_wallet/utils/clipboard_util.dart'; import 'package:cake_wallet/view_model/send/send_view_model.dart'; import 'package:cw_core/payment_uris.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import "package:cw_core/wallet_type.dart"; -import 'package:flutter/services.dart'; -import 'package:flutter_svg/flutter_svg.dart'; class L2SendExternalModal extends StatefulWidget { const L2SendExternalModal({super.key, required this.sendViewModel}); @@ -180,11 +179,7 @@ class _L2SendExternalModalState extends State { children: [ Flexible( child: NewPrimaryButton( - onPressed:(){ - Clipboard.setData( - ClipboardData(text: output.address), - ); - }, + onPressed: () => ClipboardUtil.copyToClipboard(context, output.address), text: S.of(context).copy, color: Theme.of(context).colorScheme.primary, textColor: Theme.of(context).colorScheme.onPrimary), diff --git a/lib/new-ui/widgets/swap_page/swap_send_external_modal.dart b/lib/new-ui/widgets/swap_page/swap_send_external_modal.dart index e85620aa3..61d74cb97 100644 --- a/lib/new-ui/widgets/swap_page/swap_send_external_modal.dart +++ b/lib/new-ui/widgets/swap_page/swap_send_external_modal.dart @@ -4,11 +4,11 @@ import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart'; import 'package:cake_wallet/new-ui/widgets/swap_page/swap_modal_header.dart'; import 'package:cake_wallet/src/screens/receive/widgets/qr_image.dart'; import 'package:cake_wallet/utils/address_formatter.dart'; +import 'package:cake_wallet/utils/clipboard_util.dart'; import 'package:cake_wallet/view_model/exchange/exchange_trade_view_model.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/payment_uris.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; class SwapSendExternalModal extends StatefulWidget { const SwapSendExternalModal( @@ -45,7 +45,7 @@ class _SwapSendExternalModalState extends State { @override Widget build(BuildContext context) { - if (uri == null) return SizedBox.shrink(); + if (widget.address.isEmpty) return SizedBox.shrink(); final resolvedSize = MediaQuery.of(context).size.width * (largeQrMode ? 0.8 : 0.54); return PopScope( @@ -139,7 +139,7 @@ class _SwapSendExternalModalState extends State { child: QrImage( size: animatedSize, embeddedImagePath: widget.from.iconPath, - data: uri.toString(), + data: uri?.toString() ?? widget.address, ), ), ); @@ -175,7 +175,7 @@ class _SwapSendExternalModalState extends State { children: [ Flexible( child: NewPrimaryButton( - onPressed: ()=> Clipboard.setData(ClipboardData(text:widget.address)), + onPressed: () => ClipboardUtil.copyToClipboard(context, widget.address), text: S.of(context).copy, color: Theme.of(context).colorScheme.primary, textColor: Theme.of(context).colorScheme.onPrimary), diff --git a/lib/src/screens/dashboard/pages/cake_features_page.dart b/lib/src/screens/dashboard/pages/cake_features_page.dart index b74ee87df..1ace58a55 100644 --- a/lib/src/screens/dashboard/pages/cake_features_page.dart +++ b/lib/src/screens/dashboard/pages/cake_features_page.dart @@ -148,6 +148,15 @@ class CakeFeaturesPage extends StatelessWidget { subTitle: "Turn your old phone into your new hardware wallet with our new app", image: 'assets/images/cupcake.png', ), + AppsWidget( + isWide: true, + isLink: true, + isCake: true, + onTap: () => _launchUrl("radar.chat"), + title: "Radar", + subTitle: "Your messages. Your Bitcoin. Together, at last", + image: 'assets/images/radar.png', + ), const SizedBox(height: 12), Padding( padding: const EdgeInsets.only(left: 24, top: 16, bottom: 8), diff --git a/lib/utils/clipboard_util.dart b/lib/utils/clipboard_util.dart index a09200f27..c6925e45c 100644 --- a/lib/utils/clipboard_util.dart +++ b/lib/utils/clipboard_util.dart @@ -1,6 +1,8 @@ -import 'package:flutter/services.dart'; - +import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/utils/device_info.dart'; +import 'package:cake_wallet/utils/show_bar.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:sensitive_clipboard/sensitive_clipboard.dart'; class ClipboardUtil { @@ -12,4 +14,18 @@ class ClipboardUtil { return Clipboard.setData(data); } + + /// Copies [text] to the clipboard and shows a "Copied to Clipboard" + /// confirmation toast. Prefer this for any user-facing copy action so the + /// feedback stays consistent across the app. + static Future copyToClipboard( + BuildContext context, + String text, { + bool isSensitive = false, + }) async { + await setSensitiveDataToClipboard(ClipboardData(text: text), isSensitive: isSensitive); + if (context.mounted) { + showBar(context, S.of(context).copied_to_clipboard); + } + } } diff --git a/lib/utils/exception_handler.dart b/lib/utils/exception_handler.dart index e46650517..eee768c61 100644 --- a/lib/utils/exception_handler.dart +++ b/lib/utils/exception_handler.dart @@ -299,14 +299,15 @@ class ExceptionHandler { "FocusScopeNode was used after being disposed", "_getDismissibleFlushbar", "_QueuedFuture.execute (package:universal_ble/src/queue.dart:65)", + "Pending Request Canceled | RequestQueue disposed", "reown_core/relay_client/websocket/websocket_handler.dart", "Image upload failed due to loss of GPU access", "transport error", "SdkError.sparkError(field0: Operator RPC error: Connection error: status: Unavailable, message: \"dns error\", details: []", "the timeout of the request was reached", - "support for coin removed, your seedphrase:" - "Exception: Invalid image data" + "support for coin removed, your seedphrase:", + "Exception: Invalid image data", ]; static Future _addDeviceInfo(File file) async { @@ -441,8 +442,20 @@ class ExceptionHandler { } static bool _flutterErrorIgnore(FlutterErrorDetails errorDetails) { - // most probably a flutter context error so just ignore it if there is no stack we can debug with - return errorDetails.exception.toString().contains("Null check operator used on a null value") && - errorDetails.stack == null; + if (errorDetails.exception.toString().contains("Null check operator used on a null value")) { + // Most probably a flutter context error so just ignore it if there is no + // stack we can debug with. + if (errorDetails.stack == null) { + return true; + } + + final stack = errorDetails.stack.toString(); + if (stack.contains("handleFocusHighlightModeChange") || + stack.contains("_HighlightModeManager")) { + return true; + } + } + + return false; } } diff --git a/lib/view_model/exchange/exchange_trade_view_model.dart b/lib/view_model/exchange/exchange_trade_view_model.dart index 49514bfa6..2579b4644 100644 --- a/lib/view_model/exchange/exchange_trade_view_model.dart +++ b/lib/view_model/exchange/exchange_trade_view_model.dart @@ -415,9 +415,13 @@ abstract class ExchangeTradeViewModelBase with Store { return null; } - // Using trade currency here so the external-send QR encodes the correct scheme - final uriWalletType = - cryptoCurrencyOrTokenToWalletType(fromCurrency) ?? wallet.type; + // Using the trade's `from` currency so the external-send QR encodes the correct scheme. + final uriWalletType = cryptoCurrencyOrTokenToWalletType(fromCurrency); + + // for other currencies that we don't have wallets for + if (uriWalletType == null) { + return ExternalAddressURI(address: inputAddress, amount: amount); + } printV(uriWalletType);