From 86ea17d5bfcee797751841e0be3352e544e8372a Mon Sep 17 00:00:00 2001 From: malik1004x Date: Wed, 3 Jun 2026 18:13:22 +0200 Subject: [PATCH] improved scan screen [cw-1507] (#3281) * (wip) new scan screen * improved scan screen * fixes for ui * switch to modal sheet * add urqr (cupcake) progress indicator * fixes * Update lib/new-ui/pages/scan_page.dart --------- Co-authored-by: Omar Hatem --- .../list_item/list_item_regular_row.dart | 2 + lib/entities/qr_scanner.dart | 29 +- lib/new-ui/pages/scan_page.dart | 592 +++++++++++++++++- .../action_row/coin_action_row.dart | 13 +- .../widgets/scan_page/network_list.dart | 105 ++++ lib/src/screens/ur/animated_ur_page.dart | 6 +- .../list_item_regular_row_widget.dart | 5 + .../new_list_row/new_list_section.dart | 1 + res/pictures/camera_flash.svg | 3 + res/pictures/camera_flip.svg | 10 + res/pictures/external_link.svg | 10 + res/pictures/node_minimal.svg | 3 + res/pictures/ocp_min.svg | 5 + res/pictures/payment_invoices.svg | 3 + res/pictures/scan_service.svg | 14 + res/pictures/wc_min.svg | 5 + res/values/strings_en.arb | 10 + 17 files changed, 789 insertions(+), 27 deletions(-) create mode 100644 lib/new-ui/widgets/scan_page/network_list.dart create mode 100644 res/pictures/camera_flash.svg create mode 100644 res/pictures/camera_flip.svg create mode 100644 res/pictures/external_link.svg create mode 100644 res/pictures/node_minimal.svg create mode 100644 res/pictures/ocp_min.svg create mode 100644 res/pictures/payment_invoices.svg create mode 100644 res/pictures/scan_service.svg create mode 100644 res/pictures/wc_min.svg diff --git a/lib/entities/new_ui_entities/list_item/list_item_regular_row.dart b/lib/entities/new_ui_entities/list_item/list_item_regular_row.dart index e5fdc4595..b86632124 100644 --- a/lib/entities/new_ui_entities/list_item/list_item_regular_row.dart +++ b/lib/entities/new_ui_entities/list_item/list_item_regular_row.dart @@ -19,6 +19,7 @@ class ListItemRegularRow extends ListItem { this.copyableText, this.leadingIconErrorWidget, this.leadingIconSize, + this.iconColor, }); final String? subtitle; @@ -35,4 +36,5 @@ class ListItemRegularRow extends ListItem { final double? trailingIconSize; final Widget? leadingIconErrorWidget; final double? leadingIconSize; + final Color? iconColor; } diff --git a/lib/entities/qr_scanner.dart b/lib/entities/qr_scanner.dart index e62a625b7..139e39f99 100644 --- a/lib/entities/qr_scanner.dart +++ b/lib/entities/qr_scanner.dart @@ -1,26 +1,39 @@ import 'dart:math'; import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/new-ui/pages/scan_page.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:fast_scanner/fast_scanner.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; +import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; import 'package:ur/ur_decoder.dart'; var isQrScannerShown = false; -Future presentQRScanner(BuildContext context) async { +Future presentQRScanner(BuildContext context, {bool showHelp = false, bool showManualInput = true, bool useModal = false}) async { isQrScannerShown = true; try { - final result = await Navigator.of(context).push( - MaterialPageRoute( - builder: (context) { - return BarcodeScannerSimple(); - }, - ), - ); + final result = useModal + ? await CupertinoScaffold.showCupertinoModalBottomSheet( + context: context, + builder: (context) => ScanPage( + showHelp: showHelp, + showManualInput: showManualInput, + )) + : await Navigator.of(context).push( + CupertinoPageRoute( + builder: (context) { + return ScanPage( + showHelp: showHelp, + showManualInput: showManualInput, + ); + }, + ), + ); isQrScannerShown = false; return result; } catch (e) { diff --git a/lib/new-ui/pages/scan_page.dart b/lib/new-ui/pages/scan_page.dart index 75bbddaa3..8b47d33dc 100644 --- a/lib/new-ui/pages/scan_page.dart +++ b/lib/new-ui/pages/scan_page.dart @@ -1,10 +1,594 @@ -import 'package:flutter/material.dart'; +import 'dart:math'; +import 'dart:ui'; -class ScanPage extends StatelessWidget { - const ScanPage({super.key}); +import 'package:cake_wallet/entities/qr_scanner.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/new-ui/widgets/modern_button.dart'; +import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart'; +import 'package:cake_wallet/new-ui/widgets/scan_page/network_list.dart'; +import 'package:cake_wallet/new-ui/widgets/send_page/floating_icon_button.dart'; +import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:fast_scanner/fast_scanner.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:flutter/services.dart'; +import 'package:ur/ur_decoder.dart'; + +class ScanPage extends StatefulWidget { + const ScanPage({super.key, this.showHelp = false, this.showManualInput = true}); + + final bool showHelp; + final bool showManualInput; + + @override + State createState() => _ScanPageState(); +} + +class _ScanPageState extends State { + final MobileScannerController controller = MobileScannerController(); + int? _numCameras; + bool _frontFlashMode = false; + bool _textInputMode = false; + final TextEditingController textController = TextEditingController(); + final FocusNode textFocusNode = FocusNode(); + List urCodes = []; + late var ur = URQRToURQRData(urCodes); + final decoder = URDecoder(); + bool popped = false; + Barcode? _barcode; + + @override + void initState() { + super.initState(); + controller.addListener(() => if (mounted) setState(() { + _numCameras = controller.value.availableCameras; + })); + } + + @override + void dispose() { + controller.dispose(); + textController.dispose(); + textFocusNode.dispose(); + super.dispose(); + } @override Widget build(BuildContext context) { - return const Placeholder(); + final double cutoutSize = MediaQuery.of(context).size.width * 0.8; + const double cutoutRadius = 24.0; + const Duration textModeSwitchDuration = Duration(milliseconds: 300); + final buttonColor = _frontFlashMode ? Colors.black.withAlpha(40) : Colors.white.withAlpha(40); + final buttonIconColor = _frontFlashMode ? Colors.black : Colors.white; + final isScanningURQR = decoder.processedPartsCount() > 0; + final double targetRadius = isScanningURQR ? (cutoutSize / 2) : cutoutRadius; + + return Material( + child: Stack( + children: [ + MobileScanner( + controller: controller, + onDetect: _handleBarcode, + ), + Positioned.fill( + child: GestureDetector( + onTap: () => setState(() { + _textInputMode = false; + }), + child: RepaintBoundary( + child: AnimatedSwitcher( + switchInCurve: Curves.easeOutCubic, + switchOutCurve: Curves.easeInQuad, + duration: textModeSwitchDuration, + child: _textInputMode + ? BackdropFilter( + key: ValueKey(1), + filter: ImageFilter.blur(sigmaX: 8.0, sigmaY: 8.0), + child: Container( + color: _frontFlashMode ? Colors.white : Colors.black.withAlpha(153), + ), + ) + : TweenAnimationBuilder( + key: const ValueKey(0), + tween: Tween(begin: 0.0, end: targetRadius), + duration: const Duration(milliseconds: 500), + curve: Curves.easeOutCubic, + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 8.0, sigmaY: 8.0), + child: Container( + color: _frontFlashMode ? Colors.white : Colors.black.withAlpha(153), + ), + ), + builder: (context, radius, child) { + return ClipPath( + clipper: HoleClipper( + width: cutoutSize, + height: cutoutSize, + radius: radius, + ), + child: child, + ); + }, + ), + ), + ), + ), + ), + AnimatedOpacity( + opacity: _textInputMode || isScanningURQR ? 0 : 1, + duration: textModeSwitchDuration, + child: TweenAnimationBuilder( + tween: Tween(begin: 0.0, end: targetRadius), + duration: const Duration(milliseconds: 500), + curve: Curves.easeOutCubic, + builder: (context, radius, child) => Center( + child: Container( + width: cutoutSize, + height: cutoutSize, + decoration: BoxDecoration( + border: Border.all(color: Colors.white, width: 4.0), + borderRadius: BorderRadius.circular(radius), + ), + ), + ), + ), + ), + SafeArea( + child: Column( + children: [ + ModalTopBar( + title: "", + trailingWidget: AnimatedOpacity( + duration: textModeSwitchDuration, + opacity: _textInputMode ? 0 : 1, + child: Row( + spacing: 8, + children: [ + if ((_numCameras ?? 0) > 1) + ModernButton.svg( + size: 36, + iconSize: 24, + svgPath: "assets/new-ui/camera_flip.svg", + onPressed: () { + controller.switchCamera(); + setState(() { + _frontFlashMode = false; + }); + }, + iconColor: buttonIconColor, + backgroundColor: buttonColor, + ), + ModernButton( + size: 36, + iconSize: 24, + icon: Icon( + (controller.value.torchState == TorchState.on || _frontFlashMode) + ? Icons.flash_off_outlined + : Icons.flash_on), + onPressed: () { + if (controller.value.cameraDirection == CameraFacing.front) { + setState(() { + _frontFlashMode = !_frontFlashMode; + }); + } else { + controller.toggleTorch(); + } + }, + iconColor: buttonIconColor, + backgroundColor: buttonColor, + ), + ], + ), + ), + leadingWidget: Row( + textBaseline: TextBaseline.ideographic, + spacing: 24, + children: [ + ModernButton( + size: 36, + iconSize: 18, + icon: Icon(Icons.arrow_back_ios_new), + onPressed: () { + if (_textInputMode) { + setState(() { + _textInputMode = false; + }); + } else { + Navigator.of(context).pop(); + } + }, + iconColor: buttonIconColor, + backgroundColor: buttonColor, + ), + Text( + S.of(context).scan, + style: TextStyle( + fontSize: 18, fontWeight: FontWeight.w600, color: buttonIconColor), + ) + ], + ), + ) + ], + ), + ), + Positioned( + bottom: 18 + + max(MediaQuery.of(context).viewInsets.bottom, + MediaQuery.of(context).viewPadding.bottom), + left: 16, + right: 16, + child: RepaintBoundary( + child: AnimatedOpacity( + opacity: _textInputMode ? 1 : 0, + duration: textModeSwitchDuration, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(18)), + child: Row( + spacing: 10, + children: [ + Expanded( + child: TextField( + enabled: _textInputMode, + controller: textController, + focusNode: textFocusNode, + onSubmitted: (val) { + if (val.isNotEmpty) { + Navigator.of(context).pop(val); + } else { + setState(() { + _textInputMode = false; + }); + } + }, + decoration: InputDecoration(hintText: S.of(context).enter_code), + ), + ), + FloatingIconButton( + iconPath: "assets/new-ui/paste.svg", + onPressed: () async { + final data = await Clipboard.getData("text/plain"); + if (data?.text != null) { + textController.text = data!.text!; + } + }), + SizedBox( + width: 2, + ) + ], + ), + ), + ), + ), + ), + Positioned( + bottom: 120, + left: 0, + right: 0, + child: RepaintBoundary( + child: AnimatedOpacity( + duration: textModeSwitchDuration, + opacity: _textInputMode ? 0 : 1, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + spacing: 8, + children: [ + // "not mvp" + // ScanPageButton( + // onTap: () async { + // FilePickerResult? res = await FilePicker.platform.pickFiles( + // type: FileType.image, + // allowMultiple: false, + // withData: false, + // ); + // + // if (res != null && res.paths.isNotEmpty && res.paths.first != null) { + // final capture = await controller.analyzeImage(res.paths.first!); + // if (capture != null) { + // _handleBarcode(capture); + // } + // } + // }, + // icon: Icons.photo_outlined, + // label: S.of(context).gallery, + // buttonColor: buttonColor, + // buttonIconColor: buttonIconColor), + if (widget.showManualInput) + ScanPageButton( + onTap: () { + setState(() { + _textInputMode = true; + }); + Future.delayed(textModeSwitchDuration) + .then((val) => textFocusNode.requestFocus()); + }, + icon: Icons.edit_outlined, + label: S.of(context).manual_input, + buttonColor: buttonColor, + buttonIconColor: buttonIconColor), + if (widget.showHelp) + ScanPageButton( + onTap: () async { + if(_textInputMode) return; + try { + controller.stop(); + await showModalBottomSheet( + context: context, + isScrollControlled: true, + useSafeArea: true, + backgroundColor: Theme.of(context).colorScheme.surface, + builder: (context) => ScanPageNetworkList()); + + } finally { + controller.start(); + } + + }, + icon: Icons.question_mark, + buttonColor: buttonColor, + buttonIconColor: buttonIconColor) + ], + ), + ), + )), + AnimatedOpacity( + duration: Duration(milliseconds: 500), + opacity: isScanningURQR ? 1 : 0, + child: Center( + child: SegmentedCircularProgress( + progress: URQrProgress( + expectedPartCount: decoder.expectedPartCount() ?? 0, + processedPartsCount: decoder.processedPartsCount(), + receivedPartIndexes: decoder.receivedPartIndexes().toList(), + percentage: decoder.estimatedPercentComplete(), + ), + size: cutoutSize + 20, + activeColor: Theme.of(context).colorScheme.primary, + inactiveColor: Theme.of(context).colorScheme.onSurfaceVariant), + ), + ), + Positioned( + bottom: 120, + left: 0, + right: 0, + child: AnimatedOpacity( + duration: Duration(milliseconds: 500), + opacity: isScanningURQR ? 1 : 0, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "${decoder.processedPartsCount()}", + style: TextStyle( + fontSize: 45, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.primary), + ), + Text( + "/", + style: TextStyle( + fontSize: 45, color: Theme.of(context).colorScheme.onSurfaceVariant), + ), + Text( + "${decoder.expectedPartCount()}", + style: TextStyle(fontSize: 45, color: Theme.of(context).colorScheme.onSurface), + ), + ], + ), + ), + ) + ], + ), + ); + } + + void _handleBarcode(BarcodeCapture barcodes) { + try { + _handleBarcodeInternal(barcodes); + } catch (e, st) { + showPopUp( + context: context, + builder: (context) { + return AlertWithOneAction( + alertTitle: S.of(context).error, + alertContent: S.of(context).error_dialog_content, + buttonText: 'ok', + buttonAction: () { + Navigator.of(context).pop(); + }, + ); + }, + ); + printV("$e\n$st"); + } + } + + void _handleBarcodeInternal(BarcodeCapture barcodes) { + for (final barcode in barcodes.barcodes) { + if (barcode.rawValue?.trim().isEmpty ?? false == false) continue; + if (barcode.rawValue!.startsWith("ur:")) { + if (urCodes.contains(barcode.rawValue)) continue; + decoder.receivePart(barcode.rawValue!); + setState(() { + urCodes.add(barcode.rawValue!); + ur = URQRToURQRData(urCodes); + }); + if (decoder.estimatedPercentComplete() == 1) { + setState(() { + popped = true; + }); + SchedulerBinding.instance.addPostFrameCallback((_) { + Navigator.of(context).pop(ur.inputs.join("\n")); + }); + } + ; + } + } + if (urCodes.isNotEmpty) return; + if (mounted) { + setState(() { + _barcode = barcodes.barcodes.firstOrNull; + }); + if (_barcode != null && popped != true) { + setState(() { + popped = true; + }); + Navigator.of(context).pop(_barcode!.rawValue ?? _barcode!.rawBytes); + } + } + } +} + +class ScanPageButton extends StatelessWidget { + const ScanPageButton( + {super.key, + required this.onTap, + required this.icon, + this.label, + required this.buttonColor, + required this.buttonIconColor}); + + final VoidCallback onTap; + final IconData icon; + final String? label; + final Color buttonColor; + final Color buttonIconColor; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Container( + decoration: BoxDecoration(color: buttonColor, borderRadius: BorderRadius.circular(99999)), + child: Padding( + padding: EdgeInsets.only( + top: 10, bottom: 10, left: label == null ? 10 : 16, right: label == null ? 10 : 20), + child: Row( + spacing: 10, + children: [ + Icon(icon, size: 28, color: buttonIconColor), + if (label != null) + Text(label!, + style: TextStyle( + fontSize: 16, fontWeight: FontWeight.w500, color: buttonIconColor)) + ], + ), + ), + )); + } +} + +class HoleClipper extends CustomClipper { + final double width; + final double height; + final double radius; + + HoleClipper({ + required this.width, + required this.height, + required this.radius, + }); + + @override + Path getClip(Size size) { + final Path fullScreenPath = Path()..addRect(Rect.fromLTWH(0, 0, size.width, size.height)); + + final Path cutoutPath = Path() + ..addRRect( + RRect.fromRectAndRadius( + Rect.fromCenter( + center: Offset(size.width / 2, size.height / 2), + width: width, + height: height, + ), + Radius.circular(radius), + ), + ); + + return Path.combine( + PathOperation.difference, + fullScreenPath, + cutoutPath, + ); + } + + @override + bool shouldReclip(covariant HoleClipper oldClipper) { + return oldClipper.width != width || oldClipper.height != height || oldClipper.radius != radius; + } +} + +class SegmentedCircularProgress extends StatelessWidget { + final URQrProgress progress; + final Color activeColor; + final Color inactiveColor; + final double size; + + const SegmentedCircularProgress({ + Key? key, + required this.progress, + required this.activeColor, + required this.inactiveColor, + required this.size, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return CustomPaint( + size: Size(size, size), + painter: _SegmentedCirclePainter( + progress: progress, + activeColor: activeColor, + inactiveColor: inactiveColor, + ), + ); + } +} + +class _SegmentedCirclePainter extends CustomPainter { + final URQrProgress progress; + static const double strokeWidth = 10; + static const double gapAngle = 0.08; + final Color activeColor; + final Color inactiveColor; + + _SegmentedCirclePainter({ + required this.progress, + required this.activeColor, + required this.inactiveColor, + }); + + @override + void paint(Canvas canvas, Size size) { + final rect = Rect.fromLTWH(0, 0, size.width, size.height); + final totalSegments = progress.expectedPartCount; + + final sweepAngle = (2 * pi - (gapAngle * totalSegments)) / totalSegments; + + final paint = Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = strokeWidth + ..strokeCap = StrokeCap.butt; + + double startAngle = -pi / 2 + (gapAngle / 2); + + for (int i = 0; i < totalSegments; i++) { + bool isHighlighted = progress.receivedPartIndexes.contains(i); + + paint.color = isHighlighted ? activeColor : inactiveColor; + + canvas.drawArc(rect, startAngle, sweepAngle, false, paint); + + startAngle += sweepAngle + gapAngle; + } + } + + @override + bool shouldRepaint(covariant _SegmentedCirclePainter oldDelegate) { + return !oldDelegate.progress.equals(progress) || + oldDelegate.progress.expectedPartCount != progress.expectedPartCount; } } diff --git a/lib/new-ui/widgets/coins_page/action_row/coin_action_row.dart b/lib/new-ui/widgets/coins_page/action_row/coin_action_row.dart index b700f4aab..3eafea4cd 100644 --- a/lib/new-ui/widgets/coins_page/action_row/coin_action_row.dart +++ b/lib/new-ui/widgets/coins_page/action_row/coin_action_row.dart @@ -157,17 +157,7 @@ class CoinActionRow extends StatelessWidget { } Future _onPressedScan(BuildContext context) async { - if (false && FeatureFlag.hasNewUiExtraPages) { - showModalBottomSheet( - context: context, - isScrollControlled: true, - builder: (context) => FractionallySizedBox( - heightFactor: 0.9, - child: ScanPage(), - ), - ); - } else { - final code = await presentQRScanner(context); + final code = await presentQRScanner(context, showHelp: true); if (code == null || code.isEmpty) return; @@ -227,5 +217,4 @@ class CoinActionRow extends StatelessWidget { ), ); } - } } diff --git a/lib/new-ui/widgets/scan_page/network_list.dart b/lib/new-ui/widgets/scan_page/network_list.dart new file mode 100644 index 000000000..aa3d8e448 --- /dev/null +++ b/lib/new-ui/widgets/scan_page/network_list.dart @@ -0,0 +1,105 @@ +import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item_regular_row.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart'; +import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; +import 'package:cake_wallet/src/widgets/new_list_row/new_list_section.dart'; +import 'package:cake_wallet/wallet_types.g.dart'; +import 'package:flutter/material.dart'; +import 'package:hive/hive.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class ScanPageNetworkList extends StatelessWidget { + const ScanPageNetworkList({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.vertical(top: Radius.circular(18)), + color: Theme.of(context).colorScheme.surface), + child: Column( + children: [ + ModalTopBar( + title: S.of(context).compatible_services, + leadingIcon: Icon(Icons.arrow_back_ios_new), + onLeadingPressed: Navigator.of(context).pop, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: Column( + spacing: 32, + children: [ + Column( + spacing: 24, + children: [ + CakeImageWidget( + imageUrl: "assets/new-ui/scan_service.svg", + width: 75, + height: 75, + colorFilter: + ColorFilter.mode(Theme.of(context).colorScheme.primary, BlendMode.srcIn), + ), + Text( + S.of(context).compatible_services_desc, + style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant), + textAlign: TextAlign.center, + ) + ], + ), + NewListSections(sections: { + "base": [ + ListItemRegularRow( + keyValue: "addrs", + label: S.of(context).crypto_addresses, + showArrow: false, + iconPath: "assets/new-ui/navbar/wallets.svg", + iconColor: Theme.of(context).colorScheme.onSurfaceVariant, + subtitle: "${availableWalletTypes.length} ${S.of(context).networks}"), + ListItemRegularRow( + keyValue: "invoices", + label: S.of(context).payment_invoices, + showArrow: false, + iconColor: Theme.of(context).colorScheme.onSurfaceVariant, + iconPath: "assets/new-ui/payment_invoices.svg"), + ListItemRegularRow( + keyValue: "contacts", + label: S.of(context).contacts, + showArrow: false, + iconColor: Theme.of(context).colorScheme.onSurfaceVariant, + iconPath: "assets/new-ui/navbar/contacts.svg"), + ListItemRegularRow( + keyValue: "nodes", + label: S.of(context).nodes, + showArrow: false, + iconColor: Theme.of(context).colorScheme.onSurfaceVariant, + iconPath: "assets/new-ui/node_minimal.svg"), + ], + "extra": [ + ListItemRegularRow( + iconPath: "assets/new-ui/wc_min.svg", + keyValue: "wc", + label: "WalletConnect", + subtitle: S.of(context).wc_desc, + trailingIconPath: "assets/new-ui/external_link.svg", + onTap: () { + launchUrl(Uri.https("walletconnect.com")); + }), + ListItemRegularRow( + iconPath: "assets/new-ui/ocp_min.svg", + keyValue: "ocp", + label: "OpenCryptoPay", + subtitle: S.of(context).ocp_desc, + trailingIconPath: "assets/new-ui/external_link.svg", + onTap: () { + launchUrl(Uri.https("opencryptopay.io")); + }) + ] + }) + ], + ), + ) + ], + ), + ); + } +} diff --git a/lib/src/screens/ur/animated_ur_page.dart b/lib/src/screens/ur/animated_ur_page.dart index df410d4b0..06e34bfde 100644 --- a/lib/src/screens/ur/animated_ur_page.dart +++ b/lib/src/screens/ur/animated_ur_page.dart @@ -87,7 +87,7 @@ class AnimatedURPage extends BasePage { try { switch (urQrType) { case "ur:xmr-txunsigned": // ur:xmr-txsigned - final ur = await presentQRScanner(context); + final ur = await presentQRScanner(context, showManualInput: false); if (ur == null) return; final result = await monero!.commitTransactionUR(animatedURmodel.wallet, ur); @@ -96,7 +96,7 @@ class AnimatedURPage extends BasePage { } break; case "ur:xmr-output": // xmr-keyimage - final ur = await presentQRScanner(context); + final ur = await presentQRScanner(context, showManualInput: false); if (ur == null) return; final result = await monero!.importKeyImagesUR(animatedURmodel.wallet, ur); @@ -105,7 +105,7 @@ class AnimatedURPage extends BasePage { } break; case "ur:psbt": // psbt - final ur = await presentQRScanner(context); + final ur = await presentQRScanner(context, showManualInput: false); if (ur == null) return; await bitcoin! .commitPsbtUR(animatedURmodel.wallet, ur.trim().split("\n")); diff --git a/lib/src/widgets/new_list_row/list_item_regular_row_widget.dart b/lib/src/widgets/new_list_row/list_item_regular_row_widget.dart index d947ef03a..962cb87d1 100644 --- a/lib/src/widgets/new_list_row/list_item_regular_row_widget.dart +++ b/lib/src/widgets/new_list_row/list_item_regular_row_widget.dart @@ -26,6 +26,7 @@ class ListItemRegularRowWidget extends StatelessWidget { this.copyableText, this.leadingIconErrorWidget, this.leadingIconSize, + this.iconColor }); final String keyValue; @@ -46,6 +47,7 @@ class ListItemRegularRowWidget extends StatelessWidget { final String? copyableText; final Widget? leadingIconErrorWidget; final double? leadingIconSize; + final Color? iconColor; @override Widget build(BuildContext context) { @@ -85,6 +87,9 @@ class ListItemRegularRowWidget extends StatelessWidget { width: leadingIconSize ?? 24, height: leadingIconSize ?? 24, errorWidget: leadingIconErrorWidget, + colorFilter: iconColor == null + ? null + : ColorFilter.mode(iconColor!, BlendMode.srcIn), )), Flexible( child: Column( diff --git a/lib/src/widgets/new_list_row/new_list_section.dart b/lib/src/widgets/new_list_row/new_list_section.dart index d6d46aff2..edc0b4eba 100644 --- a/lib/src/widgets/new_list_row/new_list_section.dart +++ b/lib/src/widgets/new_list_row/new_list_section.dart @@ -109,6 +109,7 @@ class NewListSections extends StatelessWidget { bottomWidget: item.bottomWidget, leadingIconErrorWidget: item.leadingIconErrorWidget, leadingIconSize: item.leadingIconSize, + iconColor: item.iconColor, ); } diff --git a/res/pictures/camera_flash.svg b/res/pictures/camera_flash.svg new file mode 100644 index 000000000..103497aef --- /dev/null +++ b/res/pictures/camera_flash.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/pictures/camera_flip.svg b/res/pictures/camera_flip.svg new file mode 100644 index 000000000..b44439a24 --- /dev/null +++ b/res/pictures/camera_flip.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/res/pictures/external_link.svg b/res/pictures/external_link.svg new file mode 100644 index 000000000..fe8cbf68b --- /dev/null +++ b/res/pictures/external_link.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/res/pictures/node_minimal.svg b/res/pictures/node_minimal.svg new file mode 100644 index 000000000..90d124f6a --- /dev/null +++ b/res/pictures/node_minimal.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/pictures/ocp_min.svg b/res/pictures/ocp_min.svg new file mode 100644 index 000000000..bdb22cf3f --- /dev/null +++ b/res/pictures/ocp_min.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/res/pictures/payment_invoices.svg b/res/pictures/payment_invoices.svg new file mode 100644 index 000000000..f2ea8f0d3 --- /dev/null +++ b/res/pictures/payment_invoices.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/pictures/scan_service.svg b/res/pictures/scan_service.svg new file mode 100644 index 000000000..883ffd936 --- /dev/null +++ b/res/pictures/scan_service.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/res/pictures/wc_min.svg b/res/pictures/wc_min.svg new file mode 100644 index 000000000..9661aae64 --- /dev/null +++ b/res/pictures/wc_min.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index c2cf71b4b..d6e4d09dd 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -217,6 +217,8 @@ "color_theme": "Color theme", "coming_soon_tag": "Coming Soon", "commit_transaction_amount_fee": "Commit transaction\nAmount: ${amount}\nFee: ${fee}", + "compatible_services": "Compatible Services", + "compatible_services_desc": "Use the Scan tool to read the following types of QR codes supported on Cake Wallet:", "configure": "Configure", "confirm": "Confirm", "confirm_delete_template": "This action will delete this template. Do you wish to continue?", @@ -280,6 +282,7 @@ "creating_new_wallet": "Creating new wallet", "creating_new_wallet_error": "Error: ${description}", "creation_date": "Creation Date", + "crypto_addresses": "Crypto Addresses", "csv_nothing_to_export": "Nothing to export", "current_swap": "Current Swap", "current_wallet": "Current wallet", @@ -496,6 +499,7 @@ "frozen": "Frozen", "frozen_balance": "Frozen Balance", "full_balance": "Full Balance", + "gallery": "Gallery", "gas_exceeds_allowance": "Gas required by transaction exceeds allowance.", "gas_price": "Gas price", "general": "General", @@ -547,6 +551,7 @@ "infobox_auto_address": "Your receive address will rotate every time you receive a transaction", "infobox_multichain": "Use this addess to receive any token or collectible on", "infobox_rotation": "Your receive address will rotate every time you close and reopen this screen", + "input": "Input", "inputs": "Inputs", "insufficient_funds_for_tx": "Insufficient funds to successfully execute transaction.", "insufficient_lamport_for_tx": "You do not have enough SOL to cover the transaction and its transaction fee. Kindly add more SOL to your wallet or reduce the SOL amount you\\'re sending.", @@ -612,6 +617,7 @@ "manage_pow_nodes": "Manage PoW nodes", "manage_providers": "Manage providers", "manage_yats": "Manage Yats", + "manual_input": "Manual Input", "mark_as_redeemed": "Mark As Redeemed", "market_place": "Marketplace", "mask": "Mask", @@ -657,6 +663,7 @@ "nanogpt_subtitle": "All the newest models (GPT-5.3, Claude 4.6).\\nNo subscription, pay with crypto.", "narrow": "Narrow", "network": "Network", + "networks": "Networks", "new_first_wallet_text": "Keeping your crypto safe is a piece of cake", "new_node_testing": "New node testing", "new_subaddress_create": "Create", @@ -698,6 +705,7 @@ "note_tap_to_change": "Note (tap to change)", "notification_permission_denied": "Notification permission got permamently denied, please manually enable it in settings", "nullURIError": "URI is null", + "ocp_desc": "Pay with crypto in stores", "offer_expires_in": "Offer expires in: ", "offline": "Offline", "ok": "OK", @@ -746,6 +754,7 @@ "payjoin_unavailable_sheet_content": "Receiving a Payjoin transaction requires you to have Bitcoin ready to use.\n\nAs soon as your wallet has funds, Payjoin will be automatically enabled.", "payjoin_unavailable_sheet_title": "Why is Payjoin unavailable?", "payment_id": "Payment ID: ", + "payment_invoices": "Payment Invoices", "payment_made_easy": "Payments made easy", "payment_was_received": "Your payment was received.", "payments": "Payments", @@ -1372,6 +1381,7 @@ "wc_approve_request_title": "Approve request", "wc_connect_request_title": "Connect request", "wc_connected_to": "Connected to ${name}", + "wc_desc": "Easily connect to DApps", "wc_max_network_fee": "Max network fee", "wc_message_to_sign": "Message to sign", "wc_network_fee": "Network fee",