Skip to content

Commit b213b6b

Browse files
committed
feat: Add user registration and key management features
- Implemented ChooseUsernameScreen for user registration. - Integrated KeyProvider for key generation and storage. - Added NodeService for user registration and device enrollment. - Enhanced onboarding and node selection screens with new buttons. - Introduced secure storage services for sensitive data management. - Updated dependencies for secure storage and cryptography.
1 parent 1c9857c commit b213b6b

18 files changed

Lines changed: 849 additions & 47 deletions

lib/data/node/node_connection.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import 'dart:developer';
22

33
import 'package:dio/dio.dart';
44
import 'package:flutter/material.dart';
5+
import 'package:shared_preferences/shared_preferences.dart';
56

67
Future<void> connectToNode(
78
ValueNotifier<int> stepNotifier,
@@ -33,4 +34,8 @@ Future<void> connectToNode(
3334
errorNotifier.value = true;
3435
return;
3536
}
37+
if (errorNotifier.value == false) {
38+
SharedPreferences prefs = await SharedPreferences.getInstance();
39+
await prefs.setString('node_address', nodeAddress);
40+
}
3641
}

lib/main.dart

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
import 'package:flutter/material.dart';
22
import 'package:hushnet_frontend/screens/onboarding.dart';
3+
import 'package:hushnet_frontend/services/key_provider.dart';
34
import 'package:hushnet_frontend/theme.dart';
45

5-
void main() {
6-
runApp(const MyApp());
6+
void main() async {
7+
WidgetsFlutterBinding.ensureInitialized();
8+
9+
final keyProvider = KeyProvider();
10+
11+
runApp(const HushNetApp());
712
}
813

9-
class MyApp extends StatelessWidget {
10-
const MyApp({super.key});
14+
class HushNetApp extends StatelessWidget {
15+
const HushNetApp({super.key});
1116
@override
1217
Widget build(BuildContext context) {
1318
return MaterialApp(

lib/screens/choose_username.dart

Lines changed: 323 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,323 @@
1+
import 'package:flutter/foundation.dart';
2+
import 'package:flutter/material.dart';
3+
import 'package:hushnet_frontend/services/key_provider.dart';
4+
import 'package:hushnet_frontend/services/node_service.dart';
5+
import 'package:hushnet_frontend/widgets/textfield.dart';
6+
7+
class ChooseUsernameScreen extends StatefulWidget {
8+
const ChooseUsernameScreen({super.key, required this.nodeAddress});
9+
final String nodeAddress;
10+
11+
@override
12+
State<ChooseUsernameScreen> createState() => _ChooseUsernameScreenState();
13+
}
14+
15+
class _ChooseUsernameScreenState extends State<ChooseUsernameScreen> {
16+
bool _isGenerating = false;
17+
bool _hasError = false;
18+
int _errorStepIndex = -1;
19+
20+
final ValueNotifier<int> _stepNotifier = ValueNotifier(0);
21+
final NodeService _nodeService = NodeService();
22+
final KeyProvider _keyProvider = KeyProvider();
23+
final TextEditingController _usernameController = TextEditingController();
24+
25+
final List<String> _steps = [
26+
"Registering user",
27+
"Generating identity key",
28+
"Generating signed prekey",
29+
"Generating one-time prekeys",
30+
"Signing keys",
31+
"Storing local identity",
32+
"Enrolling device with node",
33+
];
34+
35+
@override
36+
void initState() {
37+
_stepNotifier.addListener(() {
38+
if (mounted) setState(() {});
39+
});
40+
super.initState();
41+
}
42+
43+
Future<void> _startKeyGeneration() async {
44+
if (_usernameController.text.isEmpty) {
45+
ScaffoldMessenger.of(
46+
context,
47+
).showSnackBar(const SnackBar(content: Text("Username cannot be empty")));
48+
return;
49+
}
50+
if (_usernameController.text.contains(' ')) {
51+
ScaffoldMessenger.of(context).showSnackBar(
52+
const SnackBar(content: Text("Username cannot contain spaces")),
53+
);
54+
return;
55+
}
56+
if (_usernameController.text.length < 3) {
57+
ScaffoldMessenger.of(context).showSnackBar(
58+
const SnackBar(content: Text("Username must be at least 3 characters")),
59+
);
60+
return;
61+
}
62+
setState(() {
63+
_isGenerating = true;
64+
_hasError = false;
65+
_errorStepIndex = -1;
66+
});
67+
68+
try {
69+
await _nodeService.registerUser(
70+
widget.nodeAddress,
71+
_usernameController.text,
72+
);
73+
_stepNotifier.value = 1;
74+
await _keyProvider.initialize(_stepNotifier);
75+
await _nodeService.enrollDevice(_stepNotifier);
76+
77+
if (mounted) {
78+
ScaffoldMessenger.of(context).showSnackBar(
79+
const SnackBar(content: Text("Keys generated successfully ✅")),
80+
);
81+
}
82+
_stepNotifier.value = _steps.length; // Complete all steps
83+
} catch (e) {
84+
if (kDebugMode) {
85+
print("Error during key generation: $e");
86+
}
87+
setState(() {
88+
_hasError = true;
89+
_errorStepIndex = _stepNotifier.value;
90+
});
91+
}
92+
}
93+
94+
void _retry() {
95+
setState(() {
96+
_hasError = false;
97+
_errorStepIndex = -1;
98+
_stepNotifier.value = 0;
99+
});
100+
_startKeyGeneration();
101+
}
102+
103+
Widget _buildCenteredButton({
104+
required VoidCallback onPressed,
105+
required String label,
106+
required Color color,
107+
required IconData icon,
108+
}) {
109+
return Center(
110+
child: ConstrainedBox(
111+
constraints: const BoxConstraints(maxWidth: 320),
112+
child: ElevatedButton.icon(
113+
onPressed: onPressed,
114+
icon: Icon(icon, color: Colors.white),
115+
label: Text(
116+
label,
117+
style: const TextStyle(
118+
color: Colors.white,
119+
fontWeight: FontWeight.w600,
120+
),
121+
),
122+
style: ElevatedButton.styleFrom(
123+
backgroundColor: color,
124+
minimumSize: const Size.fromHeight(48),
125+
shape: RoundedRectangleBorder(
126+
borderRadius: BorderRadius.circular(12),
127+
),
128+
),
129+
),
130+
),
131+
);
132+
}
133+
134+
@override
135+
Widget build(BuildContext context) {
136+
return Scaffold(
137+
backgroundColor: const Color(0xFF0C0C0C),
138+
body: SafeArea(
139+
child: Center(
140+
child: Padding(
141+
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 40),
142+
child: SingleChildScrollView(
143+
child: Column(
144+
mainAxisAlignment: MainAxisAlignment.center,
145+
children: [
146+
if (!_isGenerating) ...[
147+
const Icon(
148+
Icons.account_circle_rounded,
149+
size: 80,
150+
color: Colors.blueAccent,
151+
),
152+
const SizedBox(height: 32),
153+
const Text(
154+
'Choose your username',
155+
style: TextStyle(
156+
color: Colors.white,
157+
fontSize: 24,
158+
fontWeight: FontWeight.w600,
159+
),
160+
),
161+
const SizedBox(height: 16),
162+
const Text(
163+
'This username will be the only public information associated with your identity. '
164+
'Others can use it to find or contact you.',
165+
style: TextStyle(
166+
color: Colors.white70,
167+
fontSize: 15,
168+
height: 1.4,
169+
),
170+
textAlign: TextAlign.center,
171+
),
172+
const SizedBox(height: 32),
173+
HushTextField(
174+
hint: 'username',
175+
controller: _usernameController,
176+
),
177+
const SizedBox(height: 16),
178+
const Text(
179+
'🔒 We don’t store any other data — no phone number, no email.',
180+
style: TextStyle(color: Colors.white38, fontSize: 13),
181+
textAlign: TextAlign.center,
182+
),
183+
const SizedBox(height: 32),
184+
_buildCenteredButton(
185+
onPressed: _startKeyGeneration,
186+
label: 'Generate Keys & Enroll Devices',
187+
color: const Color(0xFF2563EB),
188+
icon: Icons.vpn_key,
189+
),
190+
const SizedBox(height: 32),
191+
_buildCenteredButton(
192+
onPressed: () {
193+
Navigator.of(context).pop();
194+
},
195+
label: 'Go back',
196+
color: const Color(0xFF2563EB),
197+
icon: Icons.arrow_back,
198+
),
199+
] else ...[
200+
const SizedBox(height: 24),
201+
Text(
202+
_hasError
203+
? 'An error occurred ❌'
204+
: 'Generating your keys...',
205+
style: TextStyle(
206+
color: _hasError ? Colors.redAccent : Colors.white,
207+
fontSize: 20,
208+
fontWeight: FontWeight.w600,
209+
),
210+
),
211+
const SizedBox(height: 32),
212+
Column(
213+
children: _steps.asMap().entries.map((entry) {
214+
final i = entry.key;
215+
final label = entry.value;
216+
final done = i < _stepNotifier.value;
217+
final isActive = i == _stepNotifier.value;
218+
final isError = _hasError && _errorStepIndex == i;
219+
220+
return AnimatedContainer(
221+
duration: const Duration(milliseconds: 300),
222+
margin: const EdgeInsets.symmetric(vertical: 8),
223+
padding: const EdgeInsets.symmetric(
224+
horizontal: 20,
225+
vertical: 12,
226+
),
227+
decoration: BoxDecoration(
228+
color: const Color(0xFF1A1A1A),
229+
borderRadius: BorderRadius.circular(12),
230+
border: Border.all(
231+
color: isError
232+
? Colors.redAccent
233+
: done
234+
? Colors.greenAccent
235+
: isActive
236+
? Colors.blueAccent
237+
: Colors.transparent,
238+
width: 1.2,
239+
),
240+
),
241+
child: Row(
242+
children: [
243+
Icon(
244+
isError
245+
? Icons.error_outline
246+
: done
247+
? Icons.check_circle
248+
: isActive
249+
? Icons.autorenew_rounded
250+
: Icons.radio_button_unchecked,
251+
color: isError
252+
? Colors.redAccent
253+
: done
254+
? Colors.greenAccent
255+
: isActive
256+
? Colors.blueAccent
257+
: Colors.white38,
258+
size: 20,
259+
),
260+
const SizedBox(width: 12),
261+
Expanded(
262+
child: Text(
263+
label,
264+
style: TextStyle(
265+
color: isError
266+
? Colors.redAccent
267+
: done
268+
? Colors.greenAccent
269+
: isActive
270+
? Colors.white
271+
: Colors.white54,
272+
fontSize: 15,
273+
),
274+
),
275+
),
276+
],
277+
),
278+
);
279+
}).toList(),
280+
),
281+
if (_stepNotifier.value >= _steps.length && !_hasError) ...[
282+
const SizedBox(height: 32),
283+
_buildCenteredButton(
284+
onPressed: () {
285+
Navigator.of(context).popUntil((route) => route.isFirst);
286+
},
287+
label: 'All done! Continue',
288+
color: Colors.blueAccent,
289+
icon: Icons.check,
290+
),
291+
],
292+
const SizedBox(height: 32),
293+
if (_hasError) ...[
294+
_buildCenteredButton(
295+
onPressed: () {
296+
setState(() {
297+
_isGenerating = false;
298+
_hasError = false;
299+
_errorStepIndex = -1;
300+
});
301+
},
302+
label: 'Go back',
303+
color: Colors.blueAccent,
304+
icon: Icons.arrow_back,
305+
),
306+
const SizedBox(height: 16),
307+
_buildCenteredButton(
308+
onPressed: _retry,
309+
label: 'Retry',
310+
color: Colors.redAccent,
311+
icon: Icons.refresh,
312+
),
313+
],
314+
],
315+
],
316+
),
317+
),
318+
),
319+
),
320+
),
321+
);
322+
}
323+
}

0 commit comments

Comments
 (0)