add haptic feedback on copy (#3401)

* add haptic feedback on copy * revert gradle

malik1004x committed Jul 14, 2026 at 15:42 UTC 723ee479fcc9b5e2ce7431f0d1fbf9e0a26792f3
4 files changed +36 -51
android/gradle.properties
+2 -1
@@ -1,4 +1,5 @@
1 org.gradle.jvmargs=-Xmx6144M
2 android.useAndroidX=true
3 android.enableJetifier=true
4 -android.ndk.suppressMinSdkVersionError=21
\ No newline at end of file
4 +
5 +android.ndk.suppressMinSdkVersionError=21
lib/new-ui/pages/receive_page.dart
+6 -7
@@ -271,6 +271,12 @@ class _NewReceivePageState extends State<NewReceivePage> {
271 largeQrMode: _largeQrMode,
272 showAccountsButton: widget.addressListViewModel.hasAddressList,
273 showLabelButton: widget.addressListViewModel.hasAddressList && !hasLabel,
274 + copyData: widget.addressListViewModel.hasPayjoin
275 + ? null
276 + : ClipboardData(
277 + text: widget.addressListViewModel.displayAmount.isEmpty
278 + ? widget.addressListViewModel.uri.address
279 + : widget.addressListViewModel.uri.toString()),
280 onCopyButtonPressed: () {
281 if (widget.addressListViewModel.hasPayjoin) {
282 showModalBottomSheet(
@@ -278,13 +284,6 @@ class _NewReceivePageState extends State<NewReceivePage> {
284 context: context,
285 builder: (context) =>
286 PayjoinCopyModal(uri: widget.addressListViewModel.uri));
281 - } else {
282 - Clipboard.setData(
283 - ClipboardData(
284 - text: widget.addressListViewModel.displayAmount.isEmpty
285 - ? widget.addressListViewModel.uri.address
286 - : widget.addressListViewModel.uri.toString()),
287 - );
287 }
288 },
289 onAmountButtonPressed: () {
lib/new-ui/widgets/copy_wrapper.dart
+1
@@ -30,6 +30,7 @@ class _CopyWrapperState extends State<CopyWrapper> {
30 void handleCopy() async {
31 if (widget.data == null) return;
32 ClipboardUtil.setSensitiveDataToClipboard(widget.data!, isSensitive: widget.isSensitive);
33 + HapticFeedback.mediumImpact();
34 if (await shouldShowCopied()) {
35 setState(() => copied = true);
36 Future.delayed(widget.duration, () {
lib/new-ui/widgets/receive_page/receive_bottom_buttons.dart
+27 -43
@@ -4,9 +4,12 @@ import 'package:cake_wallet/generated/i18n.dart';
4 import 'package:cake_wallet/new-ui/widgets/modern_button.dart';
5 import 'package:device_info_plus/device_info_plus.dart';
6 import 'package:flutter/material.dart';
7 +import 'package:flutter/services.dart';
8 +import 'package:cake_wallet/new-ui/widgets/copy_wrapper.dart';
9
10 class ReceiveBottomButtons extends StatefulWidget {
11 final bool largeQrMode;
12 + final ClipboardData? copyData;
13 final VoidCallback onCopyButtonPressed;
14 final VoidCallback onAmountButtonPressed;
15 final VoidCallback onLabelButtonPressed;
@@ -15,7 +18,6 @@ class ReceiveBottomButtons extends StatefulWidget {
18 final bool showAccountsButton;
19
20 const ReceiveBottomButtons({
18 - super.key,
21 required this.largeQrMode,
22 required this.onCopyButtonPressed,
23 required this.onAccountsButtonPressed,
@@ -23,6 +25,8 @@ class ReceiveBottomButtons extends StatefulWidget {
25 required this.onLabelButtonPressed,
26 required this.showLabelButton,
27 required this.showAccountsButton,
28 + required this.copyData,
29 + super.key,
30 });
31
32 @override
@@ -30,17 +34,6 @@ class ReceiveBottomButtons extends StatefulWidget {
34 }
35
36 class _ReceiveBottomButtonsState extends State<ReceiveBottomButtons> {
33 - bool copied = false;
34 -
35 - void handleCopy() async {
36 - widget.onCopyButtonPressed();
37 - if (await shouldShowCopied()) {
38 - setState(() => copied = true);
39 - Future.delayed(const Duration(milliseconds: 1200), () {
40 - if (mounted) setState(() => copied = false);
41 - });
42 - }
43 - }
37
38 @override
39 Widget build(BuildContext context) {
@@ -63,19 +56,28 @@ class _ReceiveBottomButtonsState extends State<ReceiveBottomButtons> {
56 mainAxisAlignment: MainAxisAlignment.center,
57 spacing: 16,
58 children: [
66 - AnimatedSwitcher(
67 - duration: Duration(milliseconds: 200),
68 - child: ModernButton.svg(
69 - key: ValueKey(copied),
70 - size: 60,
71 - iconSize: 32,
72 - svgPath: "assets/new-ui/copy.svg",
73 - onPressed: handleCopy,
74 - label: copied ? S.of(context).copied : S.of(context).copy,
75 - iconColor: copied ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.surfaceContainer,
76 - backgroundColor: copied ? Theme.of(context).colorScheme.surfaceContainerHighest : Theme.of(context).colorScheme.primary,
77 - ),
78 - ),
59 + GestureDetector(
60 + onTap: widget.copyData == null ? widget.onCopyButtonPressed : null,
61 + child: IgnorePointer(
62 + ignoring: widget.copyData == null,
63 + child: CopyWrapper(
64 + data: widget.copyData,
65 + builder: (context, copied) => AnimatedSwitcher(
66 + duration: Duration(milliseconds: 200),
67 + child: IgnorePointer(
68 + child: ModernButton.svg(
69 + key: ValueKey(copied),
70 + size: 60,
71 + iconSize: 32,
72 + svgPath: "assets/new-ui/copy.svg",
73 + onPressed: () {},
74 + label: copied ? S.of(context).copied : S.of(context).copy,
75 + iconColor: copied ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.surfaceContainer,
76 + backgroundColor: copied ? Theme.of(context).colorScheme.surfaceContainerHighest : Theme.of(context).colorScheme.primary,
77 + ),
78 + ),
79 + ),
80 + ),),),
81 ModernButton.svg(
82 size: 60,
83 iconSize: 32,
@@ -103,22 +105,4 @@ class _ReceiveBottomButtonsState extends State<ReceiveBottomButtons> {
105 ),
106 );
107 }
106 -
107 -
108 -
109 - // android 13 (sdk 33) added a built-in "text was copied to clipboard" ui element
110 - // older android and iphone still needs an indicator though
111 - Future<bool> shouldShowCopied() async {
112 - if (!Platform.isAndroid) return true;
113 -
114 - try {
115 - final deviceInfo = DeviceInfoPlugin();
116 - final androidInfo = await deviceInfo.androidInfo;
117 - final sdk = androidInfo.version.sdkInt;
118 -
119 - return sdk < 33;
120 - } catch (_) {
121 - return true;
122 - }
123 - }
108 }
\ No newline at end of file