dev
dart 315 lines 10.5 KB
Raw
1 import 'package:cake_wallet/utils/device_info.dart';
2 import 'package:cake_wallet/utils/responsive_layout_util.dart';
3 import 'package:cake_wallet/utils/show_bar.dart';
4 import 'package:another_flushbar/flushbar.dart';
5 import 'package:flutter/material.dart';
6 import 'package:cake_wallet/generated/i18n.dart';
7 import 'package:flutter/services.dart';
8
9 class PinCodeWidget extends StatefulWidget {
10 PinCodeWidget({
11 required Key key,
12 required this.onFullPin,
13 required this.initialPinLength,
14 required this.onChangedPin,
15 required this.hasLengthSwitcher,
16 this.onChangedPinLength,
17 }) : super(key: key);
18
19 final void Function(String pin, PinCodeState state) onFullPin;
20 final void Function(String pin) onChangedPin;
21 final void Function(int length)? onChangedPinLength;
22 final bool hasLengthSwitcher;
23 final int initialPinLength;
24
25 @override
26 State<StatefulWidget> createState() => PinCodeState();
27 }
28
29 class PinCodeState<T extends PinCodeWidget> extends State<T> {
30 PinCodeState()
31 : _aspectRatio = 0,
32 pinLength = 0,
33 pin = '',
34 title = '';
35 static const defaultPinLength = fourPinLength;
36 static const sixPinLength = 6;
37 static const fourPinLength = 4;
38 final _gridViewKey = GlobalKey();
39 final _key = GlobalKey<ScaffoldState>();
40 late final FocusNode _focusNode;
41
42 int pinLength;
43 String pin;
44 String title;
45 double _aspectRatio;
46 Flushbar<void>? _progressBar;
47
48 int currentPinLength() => pin.length;
49
50 @override
51 void initState() {
52 super.initState();
53 pinLength = widget.initialPinLength;
54 pin = '';
55 title = S.current.enter_your_pin;
56 _aspectRatio = 0;
57 _focusNode = FocusNode();
58 WidgetsBinding.instance.addPostFrameCallback((_) {
59 _focusNode.requestFocus();
60 _afterLayout(_);
61 });
62 }
63
64 @override
65 void dispose() {
66 _focusNode.dispose();
67 super.dispose();
68 }
69
70 void setTitle(String title) => setState(() => this.title = title);
71
72 void clear() => setState(() => pin = '');
73
74 void reset() => setState(() {
75 pin = '';
76 pinLength = widget.initialPinLength;
77 title = S.current.enter_your_pin;
78 });
79
80 void changePinLength(int length) {
81 setState(() {
82 pinLength = length;
83 pin = '';
84 });
85
86 widget.onChangedPinLength?.call(length);
87 }
88
89 void setDefaultPinLength() => changePinLength(widget.initialPinLength);
90
91 void calculateAspectRatio() {
92 final renderBox = _gridViewKey.currentContext!.findRenderObject() as RenderBox;
93 final cellWidth = renderBox.size.width / 3;
94 final cellHeight = renderBox.size.height / 4;
95
96 if (cellWidth > 0 && cellHeight > 0) {
97 _aspectRatio = cellWidth / cellHeight;
98 }
99
100 setState(() {});
101 }
102
103 void changeProcessText(String text) {
104 hideProgressText();
105 _progressBar = createBar<void>(text, context, duration: null)..show(_key.currentContext!);
106 }
107
108 void close() {
109 _progressBar?.dismiss();
110 Navigator.of(_key.currentContext!).pop();
111 }
112
113 void hideProgressText() {
114 _progressBar?.dismiss();
115 _progressBar = null;
116 }
117
118 @override
119 Widget build(BuildContext context) =>
120 Scaffold(key: _key, body: body(context), resizeToAvoidBottomInset: false);
121
122 Widget body(BuildContext context) {
123 final deleteIconImage = Image.asset(
124 'assets/images/delete_icon.png',
125 color: Theme.of(context).colorScheme.primary,
126 );
127
128 return KeyboardListener(
129 focusNode: _focusNode,
130 autofocus: false,
131 onKeyEvent: (keyEvent) {
132 if (keyEvent is KeyDownEvent) {
133 if (keyEvent.logicalKey.keyLabel == "Backspace") {
134 _pop();
135 return;
136 }
137 int? number = int.tryParse(keyEvent.character ?? '');
138 if (number != null) {
139 _push(number);
140 }
141 }
142 },
143 child: Container(
144 color: Theme.of(context).colorScheme.surface,
145 padding: EdgeInsets.only(left: 44.0, right: 44.0, bottom: 60.0),
146 child: Column(
147 children: <Widget>[
148 Spacer(flex: 2),
149 Text(
150 title,
151 style: Theme.of(context).textTheme.titleLarge?.copyWith(
152 fontWeight: FontWeight.w500,
153 fontSize: 20,
154 color: Theme.of(context).colorScheme.onSurface,
155 ),
156 ),
157 Spacer(flex: 2),
158 Container(
159 width: 180,
160 child: Row(
161 mainAxisAlignment: MainAxisAlignment.spaceBetween,
162 children: List.generate(pinLength, (index) {
163 const size = 10.0;
164 final isFilled = pin.length > index ? pin[index] != null : false;
165
166 return Container(
167 width: size,
168 height: size,
169 decoration: BoxDecoration(
170 shape: BoxShape.circle,
171 color: isFilled
172 ? Theme.of(context).colorScheme.primary
173 : Theme.of(context).colorScheme.primary.withValues(alpha: 0.25),
174 ));
175 }),
176 ),
177 ),
178 Spacer(flex: 3),
179 if (widget.hasLengthSwitcher) ...[
180 TextButton(
181 onPressed: () {
182 changePinLength(pinLength == PinCodeState.fourPinLength
183 ? PinCodeState.sixPinLength
184 : PinCodeState.fourPinLength);
185 },
186 child: Text(
187 _changePinLengthText(),
188 style: Theme.of(context).textTheme.bodyMedium?.copyWith(
189 color: Theme.of(context).colorScheme.secondary,
190 ),
191 ),
192 )
193 ],
194 Spacer(flex: 1),
195 Flexible(
196 flex: 24,
197 child: Center(
198 child: ConstrainedBox(
199 constraints: BoxConstraints(
200 maxWidth: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint,
201 ),
202 child: Container(
203 key: _gridViewKey,
204 child: _aspectRatio > 0
205 ? ScrollConfiguration(
206 behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
207 child: GridView.count(
208 shrinkWrap: true,
209 crossAxisCount: 3,
210 childAspectRatio: DeviceInfo.instance.isDesktop ? _aspectRatio : 1.1,
211 physics: const NeverScrollableScrollPhysics(),
212 children: List.generate(12, (index) {
213 const double marginRight = 10;
214 const double marginLeft = 10;
215
216 if (index == 9) {
217 // Empty container
218 return Container(
219 margin: EdgeInsets.only(left: marginLeft, right: marginRight),
220 );
221 } else if (index == 10) {
222 index = 0;
223 } else if (index == 11) {
224 return MergeSemantics(
225 child: Container(
226 margin: EdgeInsets.only(left: marginLeft, right: marginRight),
227 child: Semantics(
228 label: S.of(context).delete,
229 button: true,
230 onTap: () => _pop(),
231 child: TextButton(
232 onPressed: () => {_pop(), HapticFeedback.mediumImpact()},
233 style: TextButton.styleFrom(
234 backgroundColor:
235 Theme.of(context).colorScheme.surfaceContainer,
236 shape: CircleBorder(),
237 ),
238 child: deleteIconImage,
239 ),
240 ),
241 ),
242 );
243 } else {
244 index++;
245 }
246
247 return Container(
248 margin: EdgeInsets.only(left: marginLeft, right: marginRight),
249 child: TextButton(
250 key: ValueKey('pin_code_button_${index}_key'),
251 onPressed: () => {_push(index), HapticFeedback.mediumImpact()},
252 style: TextButton.styleFrom(
253 backgroundColor:
254 Theme.of(context).colorScheme.surfaceContainer,
255 shape: CircleBorder(),
256 ),
257 child: Text('$index',
258 style: Theme.of(context).textTheme.headlineMedium?.copyWith(
259 fontWeight: FontWeight.w600,
260 fontSize: 30,
261 color: Theme.of(context).colorScheme.onSurfaceVariant,
262 )),
263 ),
264 );
265 }),
266 ),
267 )
268 : null,
269 ),
270 ),
271 ),
272 )
273 ],
274 ),
275 ),
276 );
277 }
278
279 void _push(int num) {
280 setState(() {
281 if (currentPinLength() >= pinLength) {
282 return;
283 }
284
285 pin += num.toString();
286
287 widget.onChangedPin(pin);
288
289 if (pin.length == pinLength) {
290 widget.onFullPin(pin, this);
291 }
292 });
293 }
294
295 void _pop() {
296 if (currentPinLength() == 0) {
297 return;
298 }
299
300 setState(() => pin = pin.substring(0, pin.length - 1));
301 }
302
303 String _changePinLengthText() {
304 return S.current.use +
305 (pinLength == PinCodeState.fourPinLength
306 ? '${PinCodeState.sixPinLength}'
307 : '${PinCodeState.fourPinLength}') +
308 S.current.digit_pin;
309 }
310
311 void _afterLayout(dynamic _) {
312 setDefaultPinLength();
313 calculateAspectRatio();
314 }
315 }