Skip to content

Commit a2987df

Browse files
committed
feat: Add FindUserScreen for user login and identity verification
- Implemented FindUserScreen to handle user login process with step-by-step feedback. - Added generateSignedMessage method in KeyProvider for signing messages. - Updated NodeService to include loginUser method for sending login requests to the node. - Modified connection bottom sheet to navigate to FindUserScreen instead of ChooseUsernameScreen. - Included verify.json asset for Lottie animation in the login process.
1 parent b213b6b commit a2987df

6 files changed

Lines changed: 209 additions & 2 deletions

File tree

assets/verify.json

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.

lib/screens/find_user.dart

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:hushnet_frontend/screens/choose_username.dart';
3+
import 'package:hushnet_frontend/services/node_service.dart';
4+
import 'package:hushnet_frontend/widgets/button.dart';
5+
import 'package:lottie/lottie.dart';
6+
7+
class FindUserScreen extends StatefulWidget {
8+
const FindUserScreen({super.key, required this.nodeAddress});
9+
final String nodeAddress;
10+
11+
@override
12+
State<FindUserScreen> createState() => _FindUserScreenState();
13+
}
14+
15+
class _FindUserScreenState extends State<FindUserScreen> {
16+
final NodeService _nodeService = NodeService();
17+
ValueNotifier<int> stepNotifier = ValueNotifier(0);
18+
String? username;
19+
20+
final List<String> _steps = [
21+
"Generating proof of identity",
22+
"Sending signed request to node",
23+
"Verifying identity signature",
24+
"Fetching user information",
25+
];
26+
27+
bool _isDone = false;
28+
29+
@override
30+
void initState() {
31+
stepNotifier.addListener(() {
32+
if (mounted) setState(() {});
33+
});
34+
_nodeService.loginUser(widget.nodeAddress, stepNotifier).then((value) {
35+
setState(() {
36+
username = value;
37+
_isDone = true;
38+
stepNotifier.value = 4; // Mark all steps as done
39+
});
40+
}).catchError((error) {
41+
setState(() {
42+
_isDone = true;
43+
stepNotifier.value = 4; // Mark all steps as done
44+
});
45+
});
46+
super.initState();
47+
}
48+
49+
@override
50+
Widget build(BuildContext context) {
51+
return Scaffold(
52+
backgroundColor: Colors.black,
53+
body: Center(
54+
child: Padding(
55+
padding: const EdgeInsets.all(24),
56+
child: Column(
57+
mainAxisAlignment: MainAxisAlignment.center,
58+
children: [
59+
Lottie.asset('assets/verify.json', width: 180, height: 180),
60+
const SizedBox(height: 16),
61+
if (!_isDone || username == null)
62+
Text(
63+
'Trying to find user…',
64+
style: Theme.of(
65+
context,
66+
).textTheme.headlineSmall?.copyWith(color: Colors.white),
67+
),
68+
if (_isDone && username != null)
69+
Text(
70+
'Welcome back, $username!',
71+
style: Theme.of(
72+
context,
73+
).textTheme.headlineSmall?.copyWith(color: Colors.white),
74+
),
75+
const SizedBox(height: 24),
76+
Container(
77+
decoration: BoxDecoration(
78+
color: const Color(0xFF1A1A1A),
79+
borderRadius: BorderRadius.circular(16),
80+
),
81+
padding: const EdgeInsets.all(20),
82+
child: Column(
83+
crossAxisAlignment: CrossAxisAlignment.start,
84+
children: List.generate(_steps.length, (index) {
85+
final done = index <= stepNotifier.value;
86+
return Padding(
87+
padding: const EdgeInsets.symmetric(vertical: 6),
88+
child: Row(
89+
children: [
90+
Icon(
91+
done
92+
? Icons.check_circle
93+
: Icons.radio_button_unchecked,
94+
color: done ? Colors.greenAccent : Colors.grey,
95+
size: 20,
96+
),
97+
const SizedBox(width: 12),
98+
Flexible(
99+
child: Text(
100+
_steps[index],
101+
style: TextStyle(
102+
color: done ? Colors.white : Colors.grey,
103+
),
104+
),
105+
),
106+
],
107+
),
108+
);
109+
}),
110+
),
111+
),
112+
const SizedBox(height: 32),
113+
if (_isDone && username != null)
114+
Text(
115+
'You have been automatically logged in using your in-device identity.',
116+
style: Theme.of(
117+
context,
118+
).textTheme.bodyLarge?.copyWith(color: Colors.white),
119+
),
120+
if (_isDone && username == null)
121+
Text(
122+
'No existing user found. We can create a new one.',
123+
style: Theme.of(
124+
context,
125+
).textTheme.bodyLarge?.copyWith(color: Colors.white),
126+
),
127+
if (_isDone) const SizedBox(height: 24),
128+
if (_isDone)
129+
HushButton(
130+
label: 'Continue',
131+
icon: Icons.arrow_forward,
132+
onPressed: () {
133+
Navigator.pushReplacement(
134+
context,
135+
MaterialPageRoute(
136+
builder: (context) => ChooseUsernameScreen(
137+
nodeAddress: widget.nodeAddress,
138+
),
139+
),
140+
);
141+
},
142+
),
143+
],
144+
),
145+
),
146+
),
147+
);
148+
}
149+
}

lib/services/key_provider.dart

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,4 +151,27 @@ class KeyProvider {
151151
'one_time_prekeys': oneTimePreKeys.map((k) => {"key": base64Encode(k['public']!)}).toList(),
152152
};
153153
}
154+
155+
Future<Map<String, String>> generateSignedMessage(String message) async {
156+
final identityKey = await getIdentityKeyPair();
157+
if (identityKey == null) {
158+
throw Exception('Identity key not found');
159+
}
160+
161+
final privateKey =
162+
await identityAlgorithm.newKeyPairFromSeed(identityKey['private']!);
163+
164+
final messageBytes = utf8.encode(message);
165+
final signature = await identityAlgorithm.sign(
166+
messageBytes,
167+
keyPair: privateKey,
168+
);
169+
170+
return {
171+
'identity_pubkey': base64Encode(identityKey['public']!),
172+
'message': base64Encode(messageBytes),
173+
'signature': base64Encode(signature.bytes),
174+
};
175+
}
176+
154177
}

lib/services/node_service.dart

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import 'package:flutter/foundation.dart';
44
import 'package:hushnet_frontend/services/key_provider.dart';
55
import 'package:hushnet_frontend/services/secure_storage_service.dart';
66
import 'package:dio/dio.dart';
7-
import 'package:hushnet_frontend/utils/json_converter.dart';
87
import 'package:logging/logging.dart';
98

109
class NodeService {
@@ -53,6 +52,39 @@ class NodeService {
5352
}
5453
}
5554

55+
Future<String?> loginUser(String nodeUrl, ValueNotifier<int>? stepNotifier) async {
56+
Map<String, dynamic> signedMessage = await _keyProvider.generateSignedMessage('Login request');
57+
stepNotifier?.value += 1;
58+
try {
59+
log.info('Logging in user');
60+
print(signedMessage);
61+
final response = await dio.post(
62+
'$nodeUrl/users/login',
63+
options: Options(headers: {'Content-Type': 'application/json'}),
64+
data: jsonEncode(signedMessage),
65+
);
66+
stepNotifier?.value += 2;
67+
68+
if (response.statusCode == 200) {
69+
final data = response.data as Map<String, dynamic>;
70+
final userId = data['id'] as String;
71+
final username = data['username'] as String;
72+
await _storage.write('username', username);
73+
await _storage.write('user_id', userId);
74+
await _storage.write('node_url', nodeUrl);
75+
return username;
76+
} else {
77+
log.severe('Login failed: ${response.statusCode} ${response.data}');
78+
stepNotifier?.value = 4;
79+
return null;
80+
}
81+
} catch (e) {
82+
stepNotifier?.value = 4;
83+
log.severe('Error logging in user: $e');
84+
return null;
85+
}
86+
}
87+
5688
Future<bool> enrollDevice(ValueNotifier<int>? stepNotifier) async {
5789
try {
5890
final nodeUrl = await _storage.read('node_url');

lib/widgets/connection/bottom_sheet.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'package:flutter/material.dart';
22
import 'package:hushnet_frontend/data/node/node_connection.dart';
33
import 'package:hushnet_frontend/screens/choose_username.dart';
4+
import 'package:hushnet_frontend/screens/find_user.dart';
45
import 'package:hushnet_frontend/services/node_service.dart';
56

67
void showConnectionSheet(BuildContext context, String nodeAddress) {
@@ -137,7 +138,7 @@ class _ConnectionStepsSheetState extends State<ConnectionStepsSheet> {
137138
onPressed: () async {
138139
Navigator.of(context).push(
139140
MaterialPageRoute(
140-
builder: (context) => ChooseUsernameScreen(nodeAddress: widget.nodeAddress),
141+
builder: (context) => FindUserScreen(nodeAddress: widget.nodeAddress),
141142
),
142143
);
143144
},

pubspec.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ flutter:
7777
assets:
7878
- assets/node.json
7979
- assets/images/hushnet_icon.png
80+
- assets/verify.json
8081

8182
# An image asset can refer to one or more resolution-specific "variants", see
8283
# https://flutter.dev/to/resolution-aware-images

0 commit comments

Comments
 (0)