dev
dart 196 lines 8.88 KB
Raw
1 import "package:cake_wallet/generated/i18n.dart";
2 import "package:cake_wallet/new-ui/widgets/coins_page/token_image_widget.dart";
3 import "package:cake_wallet/new-ui/widgets/send_page/floating_icon_button.dart";
4 import "package:cake_wallet/src/widgets/cake_image_widget.dart";
5 import "package:cake_wallet/utils/decimal_input_formatter.dart";
6 import "package:flutter/material.dart";
7 import "package:flutter/services.dart";
8 import "package:flutter_mobx/flutter_mobx.dart";
9
10 class NewSendAmountInput extends StatefulWidget {
11 const NewSendAmountInput({
12 required this.currency,
13 required this.maxDecimals,
14 required this.hasPicker,
15 required this.onPickerClicked,
16 required this.currencyIconPath,
17 required this.amountController,
18 super.key,
19 this.validator,
20 });
21
22 final String currency;
23 final String currencyIconPath;
24 final bool hasPicker;
25 final int maxDecimals;
26 final VoidCallback onPickerClicked;
27 final TextEditingController amountController;
28 final FormFieldValidator<String>? validator;
29
30 @override
31 State<NewSendAmountInput> createState() => _NewSendAmountInputState();
32 }
33
34 class _NewSendAmountInputState extends State<NewSendAmountInput> {
35 final formFieldKey = GlobalKey<FormFieldState<String>>();
36
37 @override
38 void initState() {
39 widget.amountController
40 .addListener(() => formFieldKey.currentState?.didChange(widget.amountController.text));
41 super.initState();
42 }
43
44 @override
45 Widget build(BuildContext context) => FormField<String>(
46 key: formFieldKey,
47 initialValue: widget.amountController.text,
48 validator: widget.validator,
49 builder: (state) => Column(
50 crossAxisAlignment: CrossAxisAlignment.start,
51 children: [
52 Container(
53 decoration: BoxDecoration(
54 color: widget.hasPicker
55 ? Theme.of(context).colorScheme.surfaceContainerHigh
56 : Theme.of(context).colorScheme.surfaceContainer,
57 borderRadius: BorderRadius.circular(18),
58 ),
59 child: Row(
60 children: [
61 Expanded(
62 child: Container(
63 decoration: BoxDecoration(
64 color: Theme.of(context).colorScheme.surfaceContainer,
65 borderRadius: const BorderRadius.horizontal(left: Radius.circular(18)),
66 ),
67 child: Row(
68 mainAxisSize: MainAxisSize.max,
69 spacing: 8,
70 children: [
71 // Deliberately unwrapped. Naming the field with
72 // MergeSemantics > Semantics(label:) made Android announce
73 // the amount twice: FormField's own wrapper node reflects
74 // its descendants' text, so the authored label came back a
75 // second time on the container. The visible "Amount:"
76 // caption on the send page carries the name instead, and it
77 // must stay in the semantics tree for this field to be
78 // named at all.
79 Expanded(
80 child: TextField(
81 keyboardType: TextInputType.numberWithOptions(
82 signed: false,
83 decimal: widget.maxDecimals > 0,
84 ),
85 autocorrect: false,
86 enableSuggestions: false,
87 inputFormatters: <TextInputFormatter>[
88 DecimalInputFormatter(maxDecimals: widget.maxDecimals),
89 ],
90 controller: widget.amountController,
91 decoration: InputDecoration(
92 hintText: widget.maxDecimals == 0 ? "0" : "0.00",
93 errorMaxLines: 3,
94 ),
95 onChanged: state.didChange,
96 ),
97 ),
98 FloatingIconButton(
99 iconPath: "assets/new-ui/paste.svg",
100 semanticLabel: S.of(context).paste,
101 onPressed: () async {
102 final data = await Clipboard.getData(Clipboard.kTextPlain);
103 if (data != null && data.text != null) {
104 final text = data.text!;
105 widget.amountController.value = TextEditingValue(
106 text: text,
107 selection: TextSelection.collapsed(offset: text.length),
108 );
109 }
110 },
111 ),
112 ],
113 ),
114 ),
115 ),
116 IntrinsicWidth(
117 child: Observer(
118 // `container: true` is load bearing. FormField wraps this
119 // builder in its own non-container Semantics annotation, so
120 // without a container of its own this configuration is
121 // absorbed into that wrapper node — which then parents the
122 // amount TextField and hides it from screen readers.
123 builder: (_) => Semantics(
124 container: true,
125 button: widget.hasPicker ? true : null,
126 enabled: widget.hasPicker ? true : null,
127 label: widget.hasPicker ? S.of(context).select_asset : widget.currency,
128 value: widget.hasPicker ? widget.currency : null,
129 onTap: widget.hasPicker ? widget.onPickerClicked : null,
130 excludeSemantics: true,
131 child: GestureDetector(
132 onTap: widget.onPickerClicked,
133 child: Container(
134 alignment: Alignment.center,
135 width: double.infinity,
136 decoration: BoxDecoration(
137 borderRadius: const BorderRadius.only(
138 topRight: Radius.circular(18),
139 bottomRight: Radius.circular(18),
140 ),
141 color: widget.hasPicker
142 ? Theme.of(context).colorScheme.surfaceContainerHigh
143 : Theme.of(context).colorScheme.surfaceContainer,
144 ),
145 child: Padding(
146 padding: const EdgeInsets.symmetric(horizontal: 12),
147 child: Row(
148 mainAxisAlignment: MainAxisAlignment.center,
149 mainAxisSize: MainAxisSize.max,
150 spacing: 8,
151 children: [
152 if (widget.hasPicker && widget.currencyIconPath.isNotEmpty)
153 TokenImageWidget(
154 imageUrl: widget.currencyIconPath,
155 size: 24,
156 ),
157 Text(widget.currency),
158 if (widget.hasPicker)
159 CakeImageWidget(
160 imageUrl: "assets/new-ui/chooser.svg",
161 width: 12,
162 height: 12,
163 colorFilter: ColorFilter.mode(
164 Theme.of(context).colorScheme.primary,
165 BlendMode.srcIn,
166 ),
167 ),
168 ],
169 ),
170 ),
171 ),
172 ),
173 ),
174 ),
175 ),
176 ],
177 ),
178 ),
179 if (state.hasError)
180 Padding(
181 padding: const EdgeInsets.only(top: 6, left: 8),
182 child: Semantics(
183 container: true,
184 liveRegion: true,
185 label: "${S.of(context).amount}${state.errorText!}",
186 excludeSemantics: true,
187 child: Text(
188 state.errorText!,
189 style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.error),
190 ),
191 ),
192 ),
193 ],
194 ),
195 );
196 }