Skip to content

Commit b5a67b8

Browse files
committed
Add unit tests for utility functions and implement changelog, donation, privacy, and terms pages
- Created unit tests for AppLogger, FileSizeFormatter, and NameGenerator utilities. - Implemented a changelog page to display version history and release notes. - Added a donation page with options for supporting PeerChat via Ko-fi and UPI. - Developed a privacy policy page outlining data handling practices. - Created a terms of service page detailing user agreements and acceptable use. - Introduced a reusable SubPageLayout component for consistent page structure.
1 parent 635125a commit b5a67b8

43 files changed

Lines changed: 2903 additions & 409 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/mobile-ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ jobs:
4646
echo "storeFile=keystore.jks" >> android/key.properties
4747
4848
- name: Build release APK
49-
run: flutter build apk --release
49+
run: flutter build apk --release --target-platform android-arm,android-arm64
5050

5151
- name: Publish to GitHub Releases
5252
env:

lib/main.dart

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ class BootstrapApp extends StatefulWidget {
4343

4444
class _BootstrapAppState extends State<BootstrapApp> {
4545
final FirstSignInService _firstSignInService = FirstSignInService();
46-
final MenuSettingsController _menuSettingsController =
46+
MenuSettingsController _menuSettingsController =
4747
MenuSettingsController();
4848
AppState? _appState;
4949
Object? _fatalError;
@@ -97,7 +97,22 @@ class _BootstrapAppState extends State<BootstrapApp> {
9797
appState.dispose();
9898
return;
9999
}
100+
101+
// Re-create controller with the live updateLocalName callback so any
102+
// username change immediately re-broadcasts to mesh peers.
103+
final newController = MenuSettingsController(
104+
onDisplayNameChanged: appState.updateLocalName,
105+
);
106+
await newController.init();
107+
108+
// Apply stored username into mesh advertising on startup.
109+
final storedUsername = newController.username;
110+
if (storedUsername != null && storedUsername.isNotEmpty) {
111+
appState.updateLocalName(storedUsername);
112+
}
113+
100114
setState(() {
115+
_menuSettingsController = newController;
101116
_appState = appState;
102117
_showFirstSignIn = false;
103118
_isLoading = false;

lib/src/app_state.dart

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -144,27 +144,55 @@ class AppState extends ChangeNotifier with WidgetsBindingObserver {
144144

145145
String? get publicKey => meshRouter.localPeerId;
146146

147-
// Get human-readable name from public key (Signing Key)
147+
String? _customUsername;
148+
149+
// Get human-readable name — uses custom username if set, else key-derived.
148150
String get displayName {
151+
if (_customUsername != null && _customUsername!.isNotEmpty) {
152+
return _customUsername!;
153+
}
149154
final key = publicKey;
150155
if (key == null) return 'Generating...';
151156
return NameGenerator.generateName(key);
152157
}
153158

154-
// Get short name (without number)
159+
// Get short name (without number) — uses first word of custom username if set.
155160
String get shortName {
161+
if (_customUsername != null && _customUsername!.isNotEmpty) {
162+
return _customUsername!.split(' ').first;
163+
}
156164
final key = publicKey;
157165
if (key == null) return 'Unknown';
158166
return NameGenerator.generateShortName(key);
159167
}
160168

161-
// Get initials
169+
// Get initials — from custom username if set.
162170
String get initials {
171+
if (_customUsername != null && _customUsername!.isNotEmpty) {
172+
final parts = _customUsername!.trim().split(RegExp(r'\s+'));
173+
if (parts.length >= 2) {
174+
return '${parts[0][0]}${parts[1][0]}'.toUpperCase();
175+
}
176+
return _customUsername!.substring(0, _customUsername!.length >= 2 ? 2 : 1).toUpperCase();
177+
}
163178
final key = publicKey;
164179
if (key == null) return 'U';
165180
return NameGenerator.generateInitials(key);
166181
}
167182

183+
/// Set (or clear) a custom username. Updates mesh broadcasting immediately.
184+
void setCustomUsername(String? username) {
185+
_customUsername = (username?.trim().isEmpty ?? true) ? null : username?.trim();
186+
updateLocalName(displayName);
187+
notifyListeners();
188+
}
189+
190+
/// Updates the local peer's advertised name in WiFi Direct and Bluetooth
191+
/// handshakes. Callable from outside (main.dart, MenuSettingsController).
192+
void updateLocalName(String name) {
193+
meshRouter.updateLocalName(name);
194+
}
195+
168196
bool get hasActiveTransfer => fileTransferService.activeSessions.isNotEmpty;
169197

170198
Future<void> init() async {
Lines changed: 56 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -1,121 +1,90 @@
11
import 'package:flutter/material.dart';
22
import 'package:peerchat_secure/src/utils/google_fonts.dart';
3+
import 'package:url_launcher/url_launcher.dart';
34
import '../../theme.dart';
45

56
class AboutScreen extends StatelessWidget {
67
const AboutScreen({super.key});
78

89
static const String _appVersion = '1.0.0';
10+
static const String _baseUrl = 'https://peerchat.mathi.live';
11+
static const String _changelogUrl = '$_baseUrl/changelog';
12+
static const String _tosUrl = '$_baseUrl/tos';
13+
static const String _policiesUrl = '$_baseUrl/policies';
14+
static const String _githubUrl = 'https://github.com/Mathi4Raja/P2P-app';
15+
16+
Future<void> _launch(BuildContext context, String url) async {
17+
final uri = Uri.parse(url);
18+
if (await canLaunchUrl(uri)) {
19+
await launchUrl(uri, mode: LaunchMode.externalApplication);
20+
} else if (context.mounted) {
21+
ScaffoldMessenger.of(context).showSnackBar(
22+
const SnackBar(content: Text('Could not open link')),
23+
);
24+
}
25+
}
926

1027
@override
1128
Widget build(BuildContext context) {
1229
return Scaffold(
1330
appBar: AppBar(
14-
title: Text(
15-
'About',
16-
style: GoogleFonts.inter(fontWeight: FontWeight.w700),
17-
),
31+
title: Text('About', style: GoogleFonts.inter(fontWeight: FontWeight.w700)),
1832
),
1933
body: ListView(
20-
padding: const EdgeInsets.all(14),
34+
padding: const EdgeInsets.fromLTRB(14, 14, 14, 24),
2135
children: [
2236
Container(
23-
padding: const EdgeInsets.all(14),
37+
padding: const EdgeInsets.all(16),
2438
decoration: BoxDecoration(
2539
color: AppTheme.bgSurface,
2640
borderRadius: BorderRadius.circular(12),
2741
border: Border.all(color: Colors.white.withValues(alpha: 0.06)),
2842
),
29-
child: Column(
30-
crossAxisAlignment: CrossAxisAlignment.start,
43+
child: Row(
3144
children: [
32-
Row(
33-
children: [
34-
ClipRRect(
35-
borderRadius: BorderRadius.circular(12),
36-
child: Image.asset(
37-
'assets/image.png',
38-
height: 48,
39-
width: 48,
40-
fit: BoxFit.contain,
41-
errorBuilder: (context, error, stackTrace) => Container(
42-
width: 48,
43-
height: 48,
44-
decoration: BoxDecoration(
45-
gradient: AppTheme.primaryGradient,
46-
borderRadius: BorderRadius.circular(10),
47-
),
48-
child: const Icon(Icons.shield, size: 24, color: AppTheme.bgDeep),
49-
),
50-
),
51-
),
52-
const SizedBox(width: 16),
53-
Expanded(
54-
child: Column(
55-
crossAxisAlignment: CrossAxisAlignment.start,
56-
children: [
57-
Text(
58-
'PeerChat',
59-
style: GoogleFonts.inter(
60-
fontSize: 18,
61-
fontWeight: FontWeight.w800,
62-
color: AppTheme.textPrimary,
63-
),
64-
),
65-
const SizedBox(height: 6),
66-
Text(
67-
'App version: $_appVersion',
68-
style: GoogleFonts.inter(
69-
fontSize: 13,
70-
color: AppTheme.textSecondary,
71-
),
72-
),
73-
],
74-
),
75-
),
76-
],
45+
Container(
46+
width: 52, height: 52,
47+
decoration: BoxDecoration(
48+
gradient: AppTheme.primaryGradient,
49+
borderRadius: BorderRadius.circular(12),
50+
),
51+
child: const Icon(Icons.shield, size: 28, color: AppTheme.bgDeep),
52+
),
53+
const SizedBox(width: 16),
54+
Expanded(
55+
child: Column(
56+
crossAxisAlignment: CrossAxisAlignment.start,
57+
children: [
58+
Text('PeerChat', style: GoogleFonts.inter(fontSize: 20, fontWeight: FontWeight.w800, color: AppTheme.textPrimary)),
59+
const SizedBox(height: 4),
60+
Text('Version $_appVersion — Secure Mesh', style: GoogleFonts.inter(fontSize: 12, color: AppTheme.textSecondary)),
61+
const SizedBox(height: 2),
62+
Text('Serverless · E2E Encrypted · Open Source', style: GoogleFonts.inter(fontSize: 11, color: AppTheme.accent.withValues(alpha: 0.8), fontWeight: FontWeight.w500)),
63+
],
64+
),
7765
),
7866
],
7967
),
8068
),
81-
const SizedBox(height: 10),
82-
_LinkCard(
83-
title: 'Changelog',
84-
subtitle: 'See what is new in recent versions',
85-
onTap: () => _showPlaceholder(context, 'Changelog link'),
86-
),
87-
_LinkCard(
88-
title: 'Terms of Service',
89-
subtitle: 'Read usage terms and conditions',
90-
onTap: () => _showPlaceholder(context, 'Terms of Service link'),
91-
),
92-
_LinkCard(
93-
title: 'Policies',
94-
subtitle: 'Privacy and security policy details',
95-
onTap: () => _showPlaceholder(context, 'Policies link'),
96-
),
69+
const SizedBox(height: 12),
70+
_LinkCard(icon: Icons.history_rounded, iconColor: AppTheme.primary, title: 'Changelog', subtitle: "What's new in recent versions", onTap: () => _launch(context, _changelogUrl)),
71+
_LinkCard(icon: Icons.article_outlined, iconColor: AppTheme.accent, title: 'Terms of Service', subtitle: 'Usage terms and conditions', onTap: () => _launch(context, _tosUrl)),
72+
_LinkCard(icon: Icons.shield_outlined, iconColor: AppTheme.accentPurple, title: 'Privacy Policy', subtitle: 'How we handle your data', onTap: () => _launch(context, _policiesUrl)),
73+
_LinkCard(icon: Icons.code_rounded, iconColor: AppTheme.textSecondary, title: 'Source Code', subtitle: 'Audit the protocol on GitHub', onTap: () => _launch(context, _githubUrl)),
9774
],
9875
),
9976
);
10077
}
101-
102-
void _showPlaceholder(BuildContext context, String label) {
103-
ScaffoldMessenger.of(context).showSnackBar(
104-
SnackBar(content: Text('$label (UI only for now)')),
105-
);
106-
}
10778
}
10879

10980
class _LinkCard extends StatelessWidget {
81+
final IconData icon;
82+
final Color iconColor;
11083
final String title;
11184
final String subtitle;
11285
final VoidCallback onTap;
11386

114-
const _LinkCard({
115-
required this.title,
116-
required this.subtitle,
117-
required this.onTap,
118-
});
87+
const _LinkCard({required this.icon, required this.iconColor, required this.title, required this.subtitle, required this.onTap});
11988

12089
@override
12190
Widget build(BuildContext context) {
@@ -127,43 +96,27 @@ class _LinkCard extends StatelessWidget {
12796
onTap: onTap,
12897
borderRadius: BorderRadius.circular(12),
12998
child: Container(
130-
padding: const EdgeInsets.all(12),
99+
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14),
131100
decoration: BoxDecoration(
132101
color: AppTheme.bgSurface,
133102
borderRadius: BorderRadius.circular(12),
134103
border: Border.all(color: Colors.white.withValues(alpha: 0.06)),
135104
),
136105
child: Row(
137106
children: [
107+
Icon(icon, size: 18, color: iconColor),
108+
const SizedBox(width: 14),
138109
Expanded(
139110
child: Column(
140111
crossAxisAlignment: CrossAxisAlignment.start,
141112
children: [
142-
Text(
143-
title,
144-
style: GoogleFonts.inter(
145-
fontWeight: FontWeight.w700,
146-
color: AppTheme.accent,
147-
decoration: TextDecoration.underline,
148-
),
149-
),
150-
const SizedBox(height: 4),
151-
Text(
152-
subtitle,
153-
style: GoogleFonts.inter(
154-
fontSize: 12,
155-
color: AppTheme.textSecondary,
156-
),
157-
),
113+
Text(title, style: GoogleFonts.inter(fontWeight: FontWeight.w700, color: AppTheme.textPrimary)),
114+
const SizedBox(height: 2),
115+
Text(subtitle, style: GoogleFonts.inter(fontSize: 12, color: AppTheme.textSecondary)),
158116
],
159117
),
160118
),
161-
const SizedBox(width: 8),
162-
Icon(
163-
Icons.open_in_new_rounded,
164-
color: AppTheme.textSecondary.withValues(alpha: 0.7),
165-
size: 18,
166-
),
119+
Icon(Icons.open_in_new_rounded, color: AppTheme.textSecondary.withValues(alpha: 0.5), size: 16),
167120
],
168121
),
169122
),
@@ -172,4 +125,3 @@ class _LinkCard extends StatelessWidget {
172125
);
173126
}
174127
}
175-

0 commit comments

Comments
 (0)