Skip to content

Commit ae229c6

Browse files
authored
Swap button feature flag (#397)
1 parent d0163e5 commit ae229c6

9 files changed

Lines changed: 409 additions & 321 deletions

File tree

mobile-app/lib/utils/feature_flags.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ class FeatureFlags {
44
static const bool enableKeystoneHardwareWallet = false; // turn keystone hw wallet on and off
55
static const bool enableHighSecurity = true; // turn keystone hw wallet on and off
66
static const bool enableRemoteNotifications = false; // turn remote notifications on and off
7+
static const bool enableSwap = false;
78
}
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:image_picker/image_picker.dart';
3+
import 'package:mobile_scanner/mobile_scanner.dart';
4+
import 'package:resonance_network_wallet/v2/components/glass_container.dart';
5+
import 'package:resonance_network_wallet/v2/theme/app_colors.dart';
6+
import 'package:resonance_network_wallet/v2/theme/app_text_styles.dart';
7+
8+
class QrScannerPage extends StatefulWidget {
9+
const QrScannerPage({super.key});
10+
11+
@override
12+
State<QrScannerPage> createState() => _QrScannerPageState();
13+
}
14+
15+
class _QrScannerPageState extends State<QrScannerPage> {
16+
final _controller = MobileScannerController();
17+
bool _scanned = false;
18+
19+
@override
20+
void dispose() {
21+
_controller.dispose();
22+
super.dispose();
23+
}
24+
25+
void _onDetect(BarcodeCapture capture) {
26+
if (_scanned) return;
27+
final code = capture.barcodes.firstOrNull?.rawValue;
28+
if (code != null && code.isNotEmpty) {
29+
_scanned = true;
30+
Navigator.pop(context, code);
31+
}
32+
}
33+
34+
Future<void> _pickImage() async {
35+
final image = await ImagePicker().pickImage(source: ImageSource.gallery);
36+
if (image == null || !mounted) return;
37+
final capture = await _controller.analyzeImage(image.path);
38+
if (!mounted) return;
39+
if (capture != null) {
40+
_onDetect(capture);
41+
} else {
42+
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('No QR code found in image')));
43+
}
44+
}
45+
46+
@override
47+
Widget build(BuildContext context) {
48+
final colors = context.colors;
49+
final text = context.themeText;
50+
final screen = MediaQuery.of(context).size;
51+
final frameSize = (screen.width - 112).clamp(220.0, 280.0);
52+
53+
return Scaffold(
54+
backgroundColor: Colors.black,
55+
body: Stack(
56+
children: [
57+
MobileScanner(controller: _controller, onDetect: _onDetect),
58+
CustomPaint(
59+
size: Size(screen.width, screen.height),
60+
painter: _OverlayPainter(frameSize: frameSize, screenSize: screen),
61+
),
62+
Center(child: _ScanFrame(size: frameSize)),
63+
Positioned(
64+
left: 0,
65+
right: 0,
66+
top: screen.height / 2 + frameSize / 2 + 24,
67+
child: Row(
68+
mainAxisAlignment: MainAxisAlignment.center,
69+
children: [
70+
_actionButton(icon: Icons.image_outlined, onTap: _pickImage, colors: colors),
71+
const SizedBox(width: 8),
72+
ValueListenableBuilder<MobileScannerState>(
73+
valueListenable: _controller,
74+
builder: (_, state, _) {
75+
final isOn = state.torchState == TorchState.on;
76+
return _actionButton(
77+
icon: isOn ? Icons.flash_on : Icons.flash_off,
78+
onTap: _controller.toggleTorch,
79+
colors: colors,
80+
);
81+
},
82+
),
83+
],
84+
),
85+
),
86+
Positioned(
87+
bottom: 58,
88+
left: 24,
89+
right: 24,
90+
child: GestureDetector(
91+
onTap: () => Navigator.pop(context),
92+
child: GlassContainer(
93+
asset: GlassContainer.wideAsset,
94+
child: Text(
95+
'Cancel',
96+
textAlign: TextAlign.center,
97+
style: text.paragraph?.copyWith(color: colors.textPrimary, fontWeight: FontWeight.w500),
98+
),
99+
),
100+
),
101+
),
102+
],
103+
),
104+
);
105+
}
106+
107+
Widget _actionButton({required IconData icon, required VoidCallback onTap, required AppColorsV2 colors}) {
108+
return GestureDetector(
109+
onTap: onTap,
110+
child: Container(
111+
width: 40,
112+
height: 40,
113+
decoration: BoxDecoration(color: Colors.white.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8)),
114+
child: Icon(icon, size: 20, color: colors.textPrimary),
115+
),
116+
);
117+
}
118+
}
119+
120+
class _OverlayPainter extends CustomPainter {
121+
final double frameSize;
122+
final Size screenSize;
123+
124+
_OverlayPainter({required this.frameSize, required this.screenSize});
125+
126+
@override
127+
void paint(Canvas canvas, Size size) {
128+
final rect = Offset.zero & size;
129+
final frameRect = Rect.fromCenter(
130+
center: Offset(size.width / 2, size.height / 2),
131+
width: frameSize,
132+
height: frameSize,
133+
);
134+
final path = Path()
135+
..addRect(rect)
136+
..addRRect(RRect.fromRectAndRadius(frameRect, const Radius.circular(16)));
137+
path.fillType = PathFillType.evenOdd;
138+
canvas.drawPath(path, Paint()..color = Colors.black.withValues(alpha: 0.6));
139+
}
140+
141+
@override
142+
bool shouldRepaint(_OverlayPainter old) => frameSize != old.frameSize;
143+
}
144+
145+
class _ScanFrame extends StatelessWidget {
146+
final double size;
147+
const _ScanFrame({required this.size});
148+
149+
@override
150+
Widget build(BuildContext context) {
151+
final color = Colors.white.withValues(alpha: 0.92);
152+
return SizedBox(
153+
width: size,
154+
height: size,
155+
child: Stack(
156+
children: [
157+
_corner(top: true, left: true, color: color),
158+
_corner(top: true, left: false, color: color),
159+
_corner(top: false, left: true, color: color),
160+
_corner(top: false, left: false, color: color),
161+
],
162+
),
163+
);
164+
}
165+
166+
Widget _corner({required bool top, required bool left, required Color color}) {
167+
return Positioned(
168+
top: top ? 0 : null,
169+
bottom: top ? null : 0,
170+
left: left ? 0 : null,
171+
right: left ? null : 0,
172+
child: SizedBox(
173+
width: 41,
174+
height: 41,
175+
child: DecoratedBox(
176+
decoration: BoxDecoration(
177+
borderRadius: BorderRadius.only(
178+
topLeft: top && left ? const Radius.circular(16) : Radius.zero,
179+
topRight: top && !left ? const Radius.circular(16) : Radius.zero,
180+
bottomLeft: !top && left ? const Radius.circular(16) : Radius.zero,
181+
bottomRight: !top && !left ? const Radius.circular(16) : Radius.zero,
182+
),
183+
border: Border(
184+
top: top ? BorderSide(color: color, width: 1.6) : BorderSide.none,
185+
bottom: !top ? BorderSide(color: color, width: 1.6) : BorderSide.none,
186+
left: left ? BorderSide(color: color, width: 1.6) : BorderSide.none,
187+
right: !left ? BorderSide(color: color, width: 1.6) : BorderSide.none,
188+
),
189+
),
190+
),
191+
),
192+
);
193+
}
194+
}

mobile-app/lib/v2/components/toaster_helper.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,14 @@ Future<void> showToaster(
99
required String message,
1010
required IconData iconData,
1111
Color? iconColor,
12-
Duration duration = const Duration(seconds: 3),
12+
Duration duration = const Duration(seconds: 2),
1313
FlashBehavior behavior = FlashBehavior.floating,
1414
}) async {
1515
if (!context.mounted) return;
1616

1717
await context.showFlash<void>(
1818
duration: duration,
19-
persistent: false,
19+
persistent: true,
2020
builder: (context, controller) {
2121
return FlashBar(
2222
controller: controller,

0 commit comments

Comments
 (0)