feat(auth): update biometric login to use refresh token and add sign out confirmation dialog
This commit is contained in:
parent
0f3cb701c4
commit
eb3008caf1
|
@ -90,10 +90,11 @@ class BiometricService extends GetxService {
|
|||
);
|
||||
}
|
||||
|
||||
// Also store the auth token, which is more secure than storing credentials
|
||||
// Store the current session's refresh token instead of access token
|
||||
// The refresh token has longer validity and can be used to generate new tokens
|
||||
final session = SupabaseService.instance.client.auth.currentSession;
|
||||
if (session != null) {
|
||||
await _secureStorage.write(key: _sessionKey, value: session.accessToken);
|
||||
await _secureStorage.write(key: _sessionKey, value: session.refreshToken);
|
||||
}
|
||||
|
||||
// Parse user metadata with our type-safe model
|
||||
|
@ -131,7 +132,16 @@ class BiometricService extends GetxService {
|
|||
final success = await authenticate(reason: 'Log in to your account');
|
||||
if (!success) return null;
|
||||
|
||||
return await _secureStorage.read(key: _sessionKey);
|
||||
// Return the stored refresh token
|
||||
final token = await _secureStorage.read(key: _sessionKey);
|
||||
|
||||
if (token == null || token.isEmpty) {
|
||||
// If no token is found, disable biometric login as it's in an inconsistent state
|
||||
await disableBiometricLogin();
|
||||
return null;
|
||||
}
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
// Get stored user identifier (NIK or NRP)
|
||||
|
|
|
@ -52,9 +52,37 @@ class AuthenticationRepository extends GetxController {
|
|||
return false;
|
||||
}
|
||||
|
||||
// Use the session token to restore the session
|
||||
final response = await restoreSession(sessionToken);
|
||||
return response;
|
||||
try {
|
||||
// Use the session token to restore the session
|
||||
await _supabase.auth.setSession(sessionToken);
|
||||
|
||||
// Verify if session was successfully restored
|
||||
final session = _supabase.auth.currentSession;
|
||||
if (session != null) {
|
||||
_logger.i('Biometric login successful');
|
||||
Get.offAllNamed(AppRoutes.navigationMenu);
|
||||
return true;
|
||||
} else {
|
||||
_logger.w('Session is null after biometric login');
|
||||
return false;
|
||||
}
|
||||
} catch (sessionError) {
|
||||
_logger.e('Session error: $sessionError');
|
||||
|
||||
// Try alternate method with recoverSession if setSession fails
|
||||
try {
|
||||
final response = await _supabase.auth.recoverSession(sessionToken);
|
||||
if (response.session != null) {
|
||||
_logger.i('Biometric login successful using recoverSession');
|
||||
Get.offAllNamed(AppRoutes.navigationMenu);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (recoverError) {
|
||||
_logger.e('Recover session error: $recoverError');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// If token is invalid or expired, disable biometric login
|
||||
await _biometricService.disableBiometricLogin();
|
||||
|
@ -63,6 +91,7 @@ class AuthenticationRepository extends GetxController {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
static bool _isRedirecting = false;
|
||||
|
||||
/// Updated screenRedirect method to handle onboarding preferences
|
||||
|
@ -825,23 +854,4 @@ class AuthenticationRepository extends GetxController {
|
|||
throw TExceptions('Something went wrong. Please try again later.');
|
||||
}
|
||||
}
|
||||
|
||||
// Restore session using a stored token (for biometric authentication)
|
||||
Future<bool> restoreSession(String sessionToken) async {
|
||||
try {
|
||||
// Use the token to restore a session
|
||||
await _supabase.auth.recoverSession(sessionToken);
|
||||
|
||||
// Check if session was successfully restored
|
||||
final session = _supabase.auth.currentSession;
|
||||
if (session != null) {
|
||||
return true;
|
||||
} else {
|
||||
throw 'Failed to restore session';
|
||||
}
|
||||
} catch (e) {
|
||||
_logger.e('Error restoring session: $e');
|
||||
throw 'Session restoration failed: $e';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import 'package:sigap/src/features/auth/presentasion/widgets/auth_header.dart';
|
|||
import 'package:sigap/src/features/auth/presentasion/widgets/password_field.dart';
|
||||
import 'package:sigap/src/features/auth/presentasion/widgets/social_button.dart';
|
||||
import 'package:sigap/src/shared/widgets/text/custom_text_field.dart';
|
||||
import 'package:sigap/src/utils/constants/colors.dart';
|
||||
import 'package:sigap/src/utils/constants/image_strings.dart'; // Added for logo image
|
||||
import 'package:sigap/src/utils/helpers/helper_functions.dart';
|
||||
import 'package:sigap/src/utils/validators/validation.dart';
|
||||
|
@ -66,6 +67,7 @@ class SignInScreen extends StatelessWidget {
|
|||
errorText: controller.emailError.value,
|
||||
textInputAction: TextInputAction.next,
|
||||
prefixIcon: const Icon(TablerIcons.mail, size: 20),
|
||||
hintText: 'Enter your email address',
|
||||
),
|
||||
),
|
||||
|
||||
|
@ -79,6 +81,7 @@ class SignInScreen extends StatelessWidget {
|
|||
errorText: controller.passwordError.value,
|
||||
onToggleVisibility: controller.togglePasswordVisibility,
|
||||
prefixIcon: const Icon(TablerIcons.lock, size: 20),
|
||||
hintText: 'Enter your password',
|
||||
),
|
||||
),
|
||||
|
||||
|
@ -117,17 +120,12 @@ class SignInScreen extends StatelessWidget {
|
|||
onPressed: controller.signInWithBiometrics,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.surface,
|
||||
isDarkMode ? TColors.primary : TColors.accent,
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.primary,
|
||||
Theme.of(context).primaryColor,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
side: BorderSide(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.outline.withOpacity(0.3),
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
minimumSize: const Size(double.infinity, 0),
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:sigap/src/features/auth/data/repositories/authentication_repository.dart';
|
||||
import 'package:sigap/src/features/personalization/data/models/models/profile_model.dart';
|
||||
import 'package:sigap/src/features/personalization/data/models/models/users_model.dart';
|
||||
import 'package:sigap/src/features/personalization/presentasion/controllers/settings/base_settings_controller.dart';
|
||||
|
@ -9,12 +12,22 @@ import 'package:sigap/src/features/personalization/presentasion/controllers/sett
|
|||
import 'package:sigap/src/features/personalization/presentasion/controllers/settings/privacy_controller.dart';
|
||||
import 'package:sigap/src/features/personalization/presentasion/controllers/settings/security_controller.dart';
|
||||
import 'package:sigap/src/features/personalization/presentasion/controllers/settings/text_size_controller.dart';
|
||||
import 'package:sigap/src/shared/widgets/loaders/custom_circular_loader.dart';
|
||||
import 'package:sigap/src/utils/popups/dialogs.dart';
|
||||
import 'package:sigap/src/utils/popups/loaders.dart';
|
||||
|
||||
class SettingsController extends BaseSettingsController {
|
||||
// Singleton instance
|
||||
static SettingsController get instance => Get.find<SettingsController>();
|
||||
|
||||
// User info
|
||||
final Rx<UserModel?> currentUser = Rx<UserModel?>(null);
|
||||
final Rx<ProfileModel?> userProfile = Rx<ProfileModel?>(null);
|
||||
|
||||
// Get auth repository for sign out functionality
|
||||
final AuthenticationRepository _authRepo =
|
||||
Get.find<AuthenticationRepository>();
|
||||
|
||||
// Current tab index
|
||||
final RxInt selectedTabIndex = 0.obs;
|
||||
|
||||
|
@ -27,6 +40,9 @@ class SettingsController extends BaseSettingsController {
|
|||
late final EmailController emailController;
|
||||
late final TextSizeController textSizeController;
|
||||
|
||||
// Track sign out loading state
|
||||
final RxBool isSigningOut = false.obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
|
@ -113,4 +129,31 @@ class SettingsController extends BaseSettingsController {
|
|||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle sign out with confirmation
|
||||
void handleSignOut(BuildContext context) {
|
||||
TDialogs.confirmDialog(
|
||||
context: context,
|
||||
title: 'Sign Out',
|
||||
message: 'Are you sure you want to sign out?',
|
||||
onConfirm: () async {
|
||||
try {
|
||||
isSigningOut.value = true;
|
||||
|
||||
TLoader.fullScreenLoader(text: 'Signing out, please wait...');
|
||||
|
||||
await _authRepo.signOut();
|
||||
|
||||
isSigningOut.value = false;
|
||||
} catch (e) {
|
||||
isSigningOut.value = false;
|
||||
// Show error snackbar if sign out fails
|
||||
TLoaders.errorSnackBar(
|
||||
title: 'Sign Out Failed',
|
||||
message: 'Failed to sign out. please try again later.',
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import 'package:sigap/src/features/personalization/presentasion/pages/settings/w
|
|||
import 'package:sigap/src/features/personalization/presentasion/pages/settings/widgets/security_setting.dart';
|
||||
import 'package:sigap/src/features/personalization/presentasion/pages/settings/widgets/terms_services_screen.dart';
|
||||
import 'package:sigap/src/features/personalization/presentasion/pages/settings/widgets/text_size_setting.dart';
|
||||
import 'package:sigap/src/utils/constants/colors.dart';
|
||||
import 'package:sigap/src/utils/constants/sizes.dart';
|
||||
|
||||
class SettingsScreen extends StatefulWidget {
|
||||
|
@ -24,9 +25,9 @@ class SettingsScreen extends StatefulWidget {
|
|||
class _SettingsScreenState extends State<SettingsScreen>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late TabController _tabController;
|
||||
// Use find instead of implicit creation with Get.put
|
||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||
|
||||
// Get the profile controller to access user data
|
||||
final SettingsController _settingController = Get.find<SettingsController>();
|
||||
final ProfileController _profileController = Get.find<ProfileController>();
|
||||
|
||||
@override
|
||||
|
@ -80,7 +81,7 @@ class _SettingsScreenState extends State<SettingsScreen>
|
|||
child: Text(
|
||||
'Failed to load profile',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: Colors.red,
|
||||
color: TColors.error,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -298,6 +299,15 @@ class _SettingsScreenState extends State<SettingsScreen>
|
|||
);
|
||||
},
|
||||
),
|
||||
_buildSettingsItem(
|
||||
icon: Icons.logout,
|
||||
title: 'Sign Out',
|
||||
isDestructive: true,
|
||||
onTap: () => _settingController.handleSignOut(context),
|
||||
),
|
||||
|
||||
// Add some padding at the bottom for better scroll experience
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
@ -335,6 +345,7 @@ class _SettingsScreenState extends State<SettingsScreen>
|
|||
required IconData icon,
|
||||
required String title,
|
||||
required VoidCallback onTap,
|
||||
bool isDestructive = false,
|
||||
}) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
|
@ -342,16 +353,30 @@ class _SettingsScreenState extends State<SettingsScreen>
|
|||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
|
||||
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, size: 24, color: theme.iconTheme.color),
|
||||
Icon(
|
||||
icon,
|
||||
size: 24,
|
||||
color: isDestructive ? TColors.error : theme.iconTheme.color,
|
||||
),
|
||||
const SizedBox(width: 15),
|
||||
Expanded(child: Text(title, style: theme.textTheme.bodyLarge)),
|
||||
Icon(Icons.chevron_right, color: theme.hintColor),
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
color: isDestructive ? TColors.error : null,
|
||||
),
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
Icons.chevron_right,
|
||||
color: isDestructive ? TColors.error : theme.hintColor,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -78,4 +78,7 @@ class TLoader {
|
|||
static Widget centeredLoader({double? size, Color? color}) {
|
||||
return Center(child: DCircularLoader(size: size, color: color));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Class for handling dialog popups in the application
|
||||
class TDialogs {
|
||||
/// Display a confirmation dialog with optional custom button text
|
||||
static Future<bool?> confirmDialog({
|
||||
required BuildContext context,
|
||||
required String title,
|
||||
required String message,
|
||||
String confirmText = 'Yes',
|
||||
String cancelText = 'Cancel',
|
||||
VoidCallback? onConfirm,
|
||||
VoidCallback? onCancel,
|
||||
}) async {
|
||||
final result = await showDialog<bool>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder:
|
||||
(context) => AlertDialog(
|
||||
title: Text(title),
|
||||
content: Text(message),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(false);
|
||||
if (onCancel != null) onCancel();
|
||||
},
|
||||
child: Text(cancelText),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(true);
|
||||
if (onConfirm != null) onConfirm();
|
||||
},
|
||||
child: Text(confirmText),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Show a simple alert dialog with a single OK button
|
||||
static Future<void> alertDialog({
|
||||
required BuildContext context,
|
||||
required String title,
|
||||
required String message,
|
||||
String buttonText = 'OK',
|
||||
VoidCallback? onPressed,
|
||||
}) async {
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder:
|
||||
(context) => AlertDialog(
|
||||
title: Text(title),
|
||||
content: Text(message),
|
||||
actions: [
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
if (onPressed != null) onPressed();
|
||||
},
|
||||
child: Text(buttonText),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue