dev
dart 295 lines 10.5 KB
Raw
1 import 'package:cake_wallet/entities/contact_base.dart';
2 import 'package:cake_wallet/entities/qr_scanner.dart';
3 import 'package:cake_wallet/generated/i18n.dart';
4 import 'package:cake_wallet/new-ui/widgets/send_page/floating_icon_button.dart';
5 import 'package:cake_wallet/routes.dart';
6 import 'package:cake_wallet/utils/permission_handler.dart';
7 import 'package:cw_core/currency.dart';
8 import 'package:flutter/material.dart';
9 import 'package:flutter/services.dart';
10 import "package:permission_handler_platform_interface/permission_handler_platform_interface.dart";
11
12 class NewSendAddressInput extends StatefulWidget {
13 const NewSendAddressInput({
14 super.key,
15 required this.addressController,
16 this.onURIScanned,
17 this.onPushPasteButton,
18 required this.selectedCurrency,
19 this.onSelectedContact,
20 this.onPushAddressBookButton,
21 required this.onEditingComplete,
22 this.bottomPadding = false,
23 this.validator,
24 this.focusNode,
25 this.displayName,
26 this.hintText,
27 });
28
29 final TextEditingController addressController;
30 final Function(Uri)? onURIScanned;
31 final Function(BuildContext)? onPushPasteButton;
32 final Function(BuildContext)? onPushAddressBookButton;
33 final Function(ContactBase)? onSelectedContact;
34 final String? displayName;
35 final Currency selectedCurrency;
36 final VoidCallback onEditingComplete;
37 final bool bottomPadding;
38 final FormFieldValidator<String>? validator;
39 final FocusNode? focusNode;
40 final String? hintText;
41
42 @override
43 State<NewSendAddressInput> createState() => _NewSendAddressInputState();
44 }
45
46 class _NewSendAddressInputState extends State<NewSendAddressInput> {
47 FocusNode? node;
48 GlobalKey<FormFieldState<String>> formFieldKey = GlobalKey<FormFieldState<String>>();
49
50 @override
51 void initState() {
52 super.initState();
53 node = widget.focusNode ?? FocusNode();
54 node!.addListener(_onFocusChange);
55 widget.addressController
56 .addListener(() => formFieldKey.currentState?.didChange(widget.addressController.text));
57 }
58
59 void _onFocusChange() {
60 if (mounted) {
61 setState(() {});
62 }
63 }
64
65 @override
66 Widget build(BuildContext context) {
67 return Padding(
68 padding: widget.bottomPadding
69 ? EdgeInsets.only(
70 bottom: MediaQuery.of(context).viewInsets.bottom,
71 )
72 : EdgeInsets.zero,
73 child: FormField<String>(
74 key: formFieldKey,
75 initialValue: widget.addressController.text,
76 validator: widget.validator,
77 builder: (state) => Column(
78 crossAxisAlignment: CrossAxisAlignment.start,
79 children: [
80 Container(
81 decoration: BoxDecoration(
82 color: Theme.of(context).colorScheme.surfaceContainer,
83 borderRadius: BorderRadius.circular(18)),
84 child: Row(
85 children: [
86 Expanded(
87 child: Stack(
88 children: [
89 MergeSemantics(
90 child: Semantics(
91 label: _fieldSemanticsLabel(context),
92 child: TextField(
93 focusNode: widget.focusNode,
94 autocorrect: false,
95 enableSuggestions: false,
96 onSubmitted: (val) => FocusScope.of(context).unfocus(),
97 onChanged: state.didChange,
98 onEditingComplete: () {
99 widget.onEditingComplete();
100 },
101 onTapOutside: (_) {
102 widget.onEditingComplete();
103 },
104 controller: widget.addressController,
105 decoration: InputDecoration(
106 hintText: widget.hintText ?? S.of(context).search_or_enter,
107 errorMaxLines: 3,
108 ),
109 ),
110 ),
111 ),
112 Positioned.fill(
113 child: IgnorePointer(
114 child: AnimatedOpacity(
115 duration: Duration(milliseconds: 150),
116 opacity: (widget.focusNode == null ||
117 widget.focusNode!.hasFocus ||
118 widget.addressController.text.isEmpty)
119 ? 0
120 : 1,
121 // Purely visual copy of the field content; announcing it again
122 // would read the address twice.
123 child: ExcludeSemantics(
124 child: SendAddressOverlay(
125 address: widget.addressController.text,
126 displayName: widget.displayName,
127 ),
128 )),
129 ),
130 ),
131 ],
132 ),
133 ),
134 Row(
135 spacing: 12,
136 children: [
137 SizedBox.shrink(),
138 FloatingIconButton(
139 iconPath: "assets/new-ui/paste.svg",
140 semanticLabel: S.of(context).paste,
141 onPressed: () async {
142 _pasteAddress(context);
143 }),
144 FloatingIconButton(
145 iconPath: "assets/new-ui/scan.svg",
146 semanticLabel: S.of(context).scan_qr_code,
147 onPressed: () {
148 _presentQRScanner(context);
149 }),
150 FloatingIconButton(
151 iconPath: "assets/new-ui/contacts_outlined.svg",
152 semanticLabel: S.of(context).address_book,
153 onPressed: () {
154 _presetAddressBookPicker(context);
155 }),
156 SizedBox.shrink()
157 ],
158 )
159 ],
160 ),
161 ),
162 if (state.hasError)
163 Padding(
164 padding: EdgeInsets.only(top: 6, left: 8),
165 child: Semantics(
166 container: true,
167 liveRegion: true,
168 label: "${S.of(context).address_or_alias}, ${state.errorText!}",
169 excludeSemantics: true,
170 child: Text(
171 state.errorText!,
172 style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.error),
173 ),
174 ),
175 )
176 ],
177 ),
178 ),
179 );
180 }
181
182 String _fieldSemanticsLabel(BuildContext context) {
183 final label = S.of(context).address_or_alias;
184 final displayName = widget.displayName;
185
186 if (displayName == null ||
187 displayName.isEmpty ||
188 displayName == widget.addressController.text) {
189 return label;
190 }
191
192 return "$label, $displayName";
193 }
194
195 Future<void> _presentQRScanner(BuildContext context) async {
196 bool isCameraPermissionGranted =
197 await PermissionHandler.checkPermission(Permission.camera, context);
198 if (!isCameraPermissionGranted) return;
199 final code = await presentQRScanner(context);
200 if (code == null) return;
201 if (code.isEmpty) return;
202
203 try {
204 final uri = Uri.parse(code);
205 // probably should remove this and let the `onURIScanned` handle it, but for now,
206 // will fix that it takes the token contract address
207 if (!uri.path.contains("/transfer")) {
208 widget.addressController.text = uri.path;
209 }
210 widget.onURIScanned?.call(uri);
211 } catch (_) {
212 widget.addressController.text = code;
213 }
214 }
215
216 Future<void> _pasteAddress(BuildContext context) async {
217 final clipboard = await Clipboard.getData('text/plain');
218 final address = clipboard?.text ?? '';
219
220 if (address.isNotEmpty) {
221 // if it has query parameters then it's a valid uri
222 // added because Uri.parse(address) can parse a normal address string and would still be valid
223 if (address.contains("=")) {
224 try {
225 final uri = Uri.parse(address);
226 widget.addressController.text = uri.path;
227 widget.onURIScanned?.call(uri);
228 return;
229 } catch (_) {
230 widget.addressController.text = address;
231 }
232 } else {
233 widget.addressController.text = address;
234 }
235 }
236
237 widget.onPushPasteButton?.call(context);
238 }
239
240 Future<void> _presetAddressBookPicker(BuildContext context) async {
241 final contact = await Navigator.of(context)
242 .pushNamed(Routes.pickerAddressBook, arguments: [widget.selectedCurrency, false]);
243
244 if (contact is ContactBase) {
245 widget.addressController.text = contact.address;
246 widget.onPushAddressBookButton?.call(context);
247 widget.onSelectedContact?.call(contact);
248 }
249 }
250 }
251
252 class SendAddressOverlay extends StatelessWidget {
253 const SendAddressOverlay({super.key, required this.address, this.displayName});
254
255 final String address;
256 final String? displayName;
257
258 @override
259 Widget build(BuildContext context) {
260 final primaryTextStyle = TextStyle(fontSize: 16.5);
261 final secondaryTextStyle =
262 TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant);
263
264 final showDisplayName =
265 displayName != null && displayName!.isNotEmpty && displayName != address;
266
267 return Container(
268 decoration: BoxDecoration(
269 color: Theme.of(context).colorScheme.surfaceContainer,
270 borderRadius: BorderRadius.circular(16)),
271 child: Padding(
272 padding: const EdgeInsets.symmetric(horizontal: 12.0),
273 child: Column(
274 mainAxisAlignment: MainAxisAlignment.center,
275 crossAxisAlignment: CrossAxisAlignment.start,
276 children: [
277 if (showDisplayName)
278 Text(
279 displayName!,
280 maxLines: 1,
281 overflow: TextOverflow.ellipsis,
282 style: primaryTextStyle,
283 ),
284 Text(
285 address,
286 maxLines: 1,
287 overflow: TextOverflow.ellipsis,
288 style: showDisplayName ? secondaryTextStyle : primaryTextStyle,
289 )
290 ],
291 ),
292 ),
293 );
294 }
295 }