448 lines
16 KiB
Dart
448 lines
16 KiB
Dart
import 'dart:io';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:kmobile/data/repositories/auth_repository.dart';
|
|
import 'package:kmobile/features/profile/daily_transaction_limit.dart';
|
|
import 'package:kmobile/features/profile/logout_dialog.dart';
|
|
import 'package:kmobile/features/profile/security_settings_screen.dart';
|
|
import 'package:kmobile/security/secure_storage.dart';
|
|
import 'package:local_auth/local_auth.dart';
|
|
import 'package:package_info_plus/package_info_plus.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
|
import '../../di/injection.dart';
|
|
import '../../l10n/app_localizations.dart';
|
|
import 'package:kmobile/features/profile/preferences/preference_screen.dart';
|
|
|
|
class ProfileScreen extends StatefulWidget {
|
|
final String mobileNumber;
|
|
final String customerNo;
|
|
final String customerName;
|
|
const ProfileScreen(
|
|
{super.key,
|
|
required this.mobileNumber,
|
|
required this.customerNo,
|
|
required this.customerName});
|
|
|
|
@override
|
|
State<ProfileScreen> createState() => _ProfileScreenState();
|
|
}
|
|
|
|
class _ProfileScreenState extends State<ProfileScreen> {
|
|
bool _isBiometricEnabled = false;
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_loadBiometricStatus();
|
|
}
|
|
|
|
Future<String> _getAppVersion() async {
|
|
final PackageInfo info = await PackageInfo.fromPlatform();
|
|
return 'Version ${info.version} (${info.buildNumber})';
|
|
}
|
|
|
|
Future<void> _loadBiometricStatus() async {
|
|
final storage = getIt<SecureStorage>();
|
|
final enabled = await storage.read('biometric_enabled');
|
|
setState(() {
|
|
_isBiometricEnabled = enabled ?? false;
|
|
});
|
|
}
|
|
|
|
Future<void> _handleLogout(BuildContext context) async {
|
|
final auth = getIt<AuthRepository>();
|
|
final prefs = await SharedPreferences.getInstance();
|
|
await prefs.clear(); // clear saved session/token
|
|
await auth.clearAuthTokens();
|
|
// Navigate to login and remove all previous routes
|
|
Navigator.pushNamedAndRemoveUntil(context, '/login', (route) => false);
|
|
}
|
|
|
|
Future<void> _handleBiometricToggle(bool enable) async {
|
|
final localAuth = LocalAuthentication();
|
|
final storage = getIt<SecureStorage>();
|
|
final canCheck = await localAuth.canCheckBiometrics;
|
|
|
|
if (!canCheck) {
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content:
|
|
Text(AppLocalizations.of(context).biometricsNotAvailable)),
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (enable) {
|
|
final optIn = await showDialog<bool>(
|
|
context: context,
|
|
barrierDismissible: false,
|
|
builder: (ctx) => AlertDialog(
|
|
title: Text(AppLocalizations.of(context).enableFingerprintLogin),
|
|
content: Text(AppLocalizations.of(context).enableFingerprintMessage),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.of(ctx).pop(false),
|
|
child: Text(AppLocalizations.of(context).no),
|
|
),
|
|
TextButton(
|
|
onPressed: () => Navigator.of(ctx).pop(true),
|
|
child: Text(AppLocalizations.of(context).yes),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
if (optIn == true) {
|
|
try {
|
|
final didAuth = await localAuth.authenticate(
|
|
localizedReason: AppLocalizations.of(context).authenticateToEnable,
|
|
options: const AuthenticationOptions(
|
|
stickyAuth: true,
|
|
biometricOnly: true,
|
|
),
|
|
);
|
|
if (didAuth) {
|
|
await storage.write('biometric_enabled', true);
|
|
if (mounted) {
|
|
setState(() {
|
|
_isBiometricEnabled = true;
|
|
});
|
|
}
|
|
} else {
|
|
// Authentication failed, reload state to refresh UI
|
|
if (mounted) {
|
|
await _loadBiometricStatus();
|
|
}
|
|
}
|
|
} catch (e) {
|
|
// Handle exceptions, reload state to ensure consistency
|
|
if (mounted) {
|
|
await _loadBiometricStatus();
|
|
}
|
|
}
|
|
} else {
|
|
// User cancelled, reload state to refresh UI
|
|
if (mounted) {
|
|
await _loadBiometricStatus();
|
|
}
|
|
}
|
|
} else {
|
|
final optOut = await showDialog<bool>(
|
|
context: context,
|
|
barrierDismissible: false,
|
|
builder: (ctx) => AlertDialog(
|
|
title: Text(AppLocalizations.of(context).disableFingerprintLogin),
|
|
content: Text(AppLocalizations.of(context).disableFingerprintMessage),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.of(ctx).pop(false),
|
|
child: Text(AppLocalizations.of(context).no),
|
|
),
|
|
TextButton(
|
|
onPressed: () => Navigator.of(ctx).pop(true),
|
|
child: Text(AppLocalizations.of(context).yes),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
if (optOut == true) {
|
|
await storage.write('biometric_enabled', false);
|
|
if (mounted) {
|
|
setState(() {
|
|
_isBiometricEnabled = false;
|
|
});
|
|
}
|
|
} else {
|
|
// User cancelled, reload state to refresh UI
|
|
if (mounted) {
|
|
await _loadBiometricStatus();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final loc = AppLocalizations.of(context);
|
|
final theme = Theme.of(context);
|
|
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: Text(loc.profile),
|
|
elevation: 0,
|
|
),
|
|
body: Stack(
|
|
children: [
|
|
ListView(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
|
children: [
|
|
// ===== Profile Header =====
|
|
Card(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Row(
|
|
children: [
|
|
// Avatar
|
|
Container(
|
|
width: 56,
|
|
height: 56,
|
|
child: const CircleAvatar(
|
|
radius: 50,
|
|
child: Icon(
|
|
Symbols.person,
|
|
size: 56,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
// Name + mobile
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
// If you want to show the user's name instead, replace below.
|
|
widget.customerName,
|
|
style: theme.textTheme.titleLarge?.copyWith(
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
widget.customerNo,
|
|
style: theme.textTheme.bodyMedium?.copyWith(
|
|
color: theme.colorScheme.onSurface
|
|
.withOpacity(0.7),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
// ===== Section: Settings =====
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
|
child: Text(
|
|
"Settings",
|
|
style: theme.textTheme.labelLarge?.copyWith(
|
|
fontWeight: FontWeight.w600,
|
|
letterSpacing: 0.2,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
|
|
_SectionTile(
|
|
leadingIcon: Icons.settings,
|
|
title: loc.preferences,
|
|
onTap: () {
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (context) => const PreferenceScreen(),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
_SectionTile(
|
|
leadingIcon: Icons.security,
|
|
title: loc.securitySettings,
|
|
onTap: () {
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (context) => SecuritySettingsScreen(
|
|
mobileNumber: widget.mobileNumber,
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
_SectionTile(
|
|
leadingIcon: Icons.currency_rupee,
|
|
title: loc.dailylimit,
|
|
onTap: () {
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (context) => const DailyLimitScreen(),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
Card(
|
|
child: SwitchListTile(
|
|
title: Text(loc.enableFingerprintLogin),
|
|
value: _isBiometricEnabled,
|
|
onChanged: (bool value) {
|
|
_handleBiometricToggle(value);
|
|
},
|
|
secondary: const Icon(Icons.fingerprint),
|
|
contentPadding:
|
|
const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 16),
|
|
const Divider(height: 24),
|
|
|
|
// ===== Section: Security & App =====
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
|
child: Text(
|
|
loc.appVersion,
|
|
style: theme.textTheme.labelLarge?.copyWith(
|
|
fontWeight: FontWeight.w600,
|
|
letterSpacing: 0.2,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
|
|
// Fingerprint toggle inside a styled container
|
|
Card(
|
|
child: ListTile(
|
|
leading: const Icon(Icons.smartphone),
|
|
title: Text(loc.appVersion),
|
|
trailing: FutureBuilder<String>(
|
|
future: _getAppVersion(),
|
|
builder:
|
|
(BuildContext context, AsyncSnapshot<String> snapshot) {
|
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
|
return const SizedBox(
|
|
width: 18,
|
|
height: 18,
|
|
child: CircularProgressIndicator(strokeWidth: 2),
|
|
);
|
|
} else if (snapshot.hasError) {
|
|
return Text(loc.error);
|
|
} else {
|
|
return Text(
|
|
snapshot.data ?? "N/A",
|
|
);
|
|
}
|
|
},
|
|
),
|
|
contentPadding:
|
|
const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(16),
|
|
),
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 16),
|
|
const Divider(height: 24),
|
|
|
|
// ===== Section: Actions =====
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
|
child: Text(
|
|
"Exit",
|
|
style: theme.textTheme.labelLarge?.copyWith(
|
|
fontWeight: FontWeight.w600,
|
|
letterSpacing: 0.2,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
|
|
_SectionTile(
|
|
leadingIcon: Icons.exit_to_app,
|
|
title: loc.logout,
|
|
trailChevron: false, // action tile, no chevron
|
|
onTap: () async {
|
|
final shouldExit = await showDialog<bool>(
|
|
context: context,
|
|
builder: (context) => AlertDialog(
|
|
title: Text(loc.logout),
|
|
content: Text(loc.logoutCheck),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.of(context).pop(false),
|
|
child: Text(loc.no),
|
|
),
|
|
TextButton(
|
|
onPressed: () => Navigator.of(context).pop(true),
|
|
child: Text(loc.yes),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
if (shouldExit == true) {
|
|
if (Platform.isAndroid) {
|
|
SystemNavigator.pop();
|
|
}
|
|
exit(0);
|
|
}
|
|
},
|
|
),
|
|
_SectionTile(
|
|
leadingIcon: Icons.logout,
|
|
title: loc.deregister,
|
|
trailChevron: false,
|
|
onTap: () async {
|
|
final shouldLogout = await showDialog<bool>(
|
|
context: context,
|
|
builder: (_) => const LogoutDialog(),
|
|
);
|
|
|
|
if (shouldLogout == true) {
|
|
await _handleLogout(context);
|
|
}
|
|
},
|
|
),
|
|
|
|
const SizedBox(height: 24),
|
|
],
|
|
),
|
|
],
|
|
), );
|
|
}
|
|
}
|
|
|
|
class _SectionTile extends StatelessWidget {
|
|
const _SectionTile({
|
|
required this.leadingIcon,
|
|
required this.title,
|
|
this.onTap,
|
|
this.trailChevron = true,
|
|
});
|
|
|
|
final IconData leadingIcon;
|
|
final String title;
|
|
final VoidCallback? onTap;
|
|
final bool trailChevron;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
|
|
return Card(
|
|
margin: const EdgeInsets.only(bottom: 10),
|
|
child: ListTile(
|
|
leading: Icon(leadingIcon, color: theme.colorScheme.onSurface),
|
|
title: Text(title, style: theme.textTheme.bodyLarge),
|
|
trailing: trailChevron
|
|
? Icon(Icons.chevron_right, color: theme.colorScheme.onSurface)
|
|
: null,
|
|
onTap: onTap,
|
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(16),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|