TNC Route Fixed
This commit is contained in:
@@ -146,7 +146,7 @@ class AuthService {
|
|||||||
try {
|
try {
|
||||||
final response = await _dio.post(
|
final response = await _dio.post(
|
||||||
'/api/auth/tnc',
|
'/api/auth/tnc',
|
||||||
data: {"flag": true},
|
data: {"flag": 'Y'},
|
||||||
);
|
);
|
||||||
if (response.statusCode != 200) {
|
if (response.statusCode != 200) {
|
||||||
throw AuthException('Failed to proceed with T&C');
|
throw AuthException('Failed to proceed with T&C');
|
||||||
|
|||||||
@@ -42,49 +42,57 @@ class AuthCubit extends Cubit<AuthState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> login(String customerNo, String password) async {
|
Future<void> login(String customerNo, String password) async {
|
||||||
emit(AuthLoading());
|
emit(AuthLoading());
|
||||||
try {
|
try {
|
||||||
final (users, authToken) = await _authRepository.login(customerNo, password);
|
final (users, authToken) = await _authRepository.login(customerNo, password);
|
||||||
|
|
||||||
if (authToken.tnc == false) {
|
if (authToken.tnc == false) {
|
||||||
// TNC not accepted, tell UI to show the dialog
|
emit(ShowTncDialog(authToken, users));
|
||||||
emit(ShowTncDialog(authToken, users));
|
} else {
|
||||||
} else {
|
await _checkMpinAndNavigate(users);
|
||||||
// TNC already accepted, emit Authenticated and then proceed to MPIN check
|
}
|
||||||
emit(Authenticated(users));
|
} catch (e) {
|
||||||
await _checkMpinAndNavigate();
|
emit(AuthError(e is AuthException ? e.message : e.toString()));
|
||||||
}
|
}
|
||||||
} catch (e) {
|
}
|
||||||
emit(AuthError(e is AuthException ? e.message : e.toString()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> onTncDialogResult(
|
|
||||||
bool agreed, AuthToken authToken, List<User> users) async {
|
|
||||||
if (agreed) {
|
|
||||||
try {
|
|
||||||
await _authRepository.acceptTnc();
|
|
||||||
// User agreed, emit Authenticated and then proceed to MPIN check
|
|
||||||
emit(Authenticated(users));
|
|
||||||
await _checkMpinAndNavigate();
|
|
||||||
} catch (e) {
|
|
||||||
emit(AuthError('Failed to accept TNC: $e'));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// User disagreed, tell UI to navigate to the required screen
|
|
||||||
emit(NavigateToTncRequiredScreen());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _checkMpinAndNavigate() async {
|
Future<void> onTncDialogResult(
|
||||||
final mpin = await _secureStorage.read('mpin');
|
bool agreed, AuthToken authToken, List<User> users) async {
|
||||||
if (mpin == null) {
|
if (agreed) {
|
||||||
// No MPIN, tell UI to navigate to MPIN setup
|
try {
|
||||||
emit(NavigateToMpinSetupScreen());
|
await _authRepository.acceptTnc();
|
||||||
} else {
|
// The user is NOT fully authenticated yet. Just check for MPIN.
|
||||||
// MPIN exists, tell UI to navigate to the dashboard
|
await _checkMpinAndNavigate(users);
|
||||||
emit(NavigateToDashboardScreen());
|
} catch (e) {
|
||||||
}
|
emit(AuthError('Failed to accept TNC: $e'));
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
emit(NavigateToTncRequiredScreen());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void mpinSetupCompleted() {
|
||||||
|
if (state is NavigateToMpinSetupScreen) {
|
||||||
|
final users = (state as NavigateToMpinSetupScreen).users;
|
||||||
|
emit(Authenticated(users));
|
||||||
|
} else {
|
||||||
|
// Handle unexpected state if necessary
|
||||||
|
emit(AuthError("Invalid state during MPIN setup completion."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Future<void> _checkMpinAndNavigate(List<User> users) async {
|
||||||
|
final mpin = await _secureStorage.read('mpin');
|
||||||
|
if (mpin == null) {
|
||||||
|
// No MPIN, tell UI to navigate to MPIN setup, carrying user data
|
||||||
|
emit(NavigateToMpinSetupScreen(users));
|
||||||
|
} else {
|
||||||
|
// MPIN exists, user is authenticated
|
||||||
|
emit(Authenticated(users));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,13 @@ class ShowTncDialog extends AuthState {
|
|||||||
// States to trigger specific navigations from the UI
|
// States to trigger specific navigations from the UI
|
||||||
class NavigateToTncRequiredScreen extends AuthState {}
|
class NavigateToTncRequiredScreen extends AuthState {}
|
||||||
|
|
||||||
class NavigateToMpinSetupScreen extends AuthState {}
|
class NavigateToMpinSetupScreen extends AuthState {
|
||||||
|
final List<User> users;
|
||||||
|
|
||||||
|
const NavigateToMpinSetupScreen(this.users);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => [users];
|
||||||
|
}
|
||||||
|
|
||||||
class NavigateToDashboardScreen extends AuthState {}
|
class NavigateToDashboardScreen extends AuthState {}
|
||||||
@@ -61,8 +61,21 @@ class AuthToken extends Equatable {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assuming 'tnc' is directly a boolean in the JWT payload
|
final tncValue = payloadMap['tnc'];
|
||||||
return payloadMap['tnc'] as bool;
|
|
||||||
|
// Handle different representations of 'true'
|
||||||
|
if (tncValue is bool) {
|
||||||
|
return tncValue;
|
||||||
|
}
|
||||||
|
if (tncValue is String) {
|
||||||
|
return tncValue.toLowerCase() == 'true';
|
||||||
|
}
|
||||||
|
if (tncValue is int) {
|
||||||
|
return tncValue == 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to false for any other case
|
||||||
|
return false;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log('Error decoding tnc from token: $e');
|
log('Error decoding tnc from token: $e');
|
||||||
// Default to false if decoding fails or 'tnc' is not found/invalid
|
// Default to false if decoding fails or 'tnc' is not found/invalid
|
||||||
|
|||||||
@@ -41,51 +41,236 @@ class LoginScreenState extends State<LoginScreen>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
// return Scaffold(
|
||||||
|
// body: BlocConsumer<AuthCubit, AuthState>(
|
||||||
|
// listener: (context, state) async {
|
||||||
|
// if (state is ShowTncDialog) {
|
||||||
|
// // The dialog now returns a boolean for the 'disagree' case,
|
||||||
|
// // or it completes when the 'proceed' action is finished.
|
||||||
|
// final agreed = await showDialog<bool>(
|
||||||
|
// context: context,
|
||||||
|
// barrierDismissible: false,
|
||||||
|
// builder: (dialogContext) => TncDialog(
|
||||||
|
// onProceed: () async {
|
||||||
|
// // This function is passed to the dialog.
|
||||||
|
// // It calls the cubit and completes when the cubit's work is done.
|
||||||
|
// await context
|
||||||
|
// .read<AuthCubit>()
|
||||||
|
// .onTncDialogResult(true, state.authToken, state.users);
|
||||||
|
// },
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
|
||||||
|
// // If 'agreed' is false, it means the user clicked 'Disagree'.
|
||||||
|
// if (agreed == false) {
|
||||||
|
// if (!context.mounted) return;
|
||||||
|
// context
|
||||||
|
// .read<AuthCubit>()
|
||||||
|
// .onTncDialogResult(false, state.authToken, state.users);
|
||||||
|
// }
|
||||||
|
// } else if (state is NavigateToTncRequiredScreen) {
|
||||||
|
// Navigator.of(context).pushNamed(TncRequiredScreen.routeName);
|
||||||
|
// } else if (state is NavigateToMpinSetupScreen) {
|
||||||
|
// Navigator.of(context).push( // Use push, NOT pushReplacement
|
||||||
|
// MaterialPageRoute(
|
||||||
|
// builder: (_) => MPinScreen(
|
||||||
|
// mode: MPinMode.set,
|
||||||
|
// onCompleted: (_) {
|
||||||
|
// // This clears the entire stack and pushes the dashboard
|
||||||
|
// Navigator.of(context, rootNavigator: true).pushAndRemoveUntil(
|
||||||
|
// MaterialPageRoute(builder: (_) => const NavigationScaffold()),
|
||||||
|
// (route) => false,
|
||||||
|
// );
|
||||||
|
// },
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
// } else if (state is NavigateToDashboardScreen) {
|
||||||
|
// Navigator.of(context).pushReplacement(
|
||||||
|
// MaterialPageRoute(builder: (_) => const NavigationScaffold()),
|
||||||
|
// );
|
||||||
|
// } else if (state is AuthError) {
|
||||||
|
// if (state.message == 'MIGRATED_USER_HAS_NO_PASSWORD') {
|
||||||
|
// Navigator.of(context).push(MaterialPageRoute(
|
||||||
|
// builder: (_) => SetPasswordScreen(
|
||||||
|
// customerNo: _customerNumberController.text.trim(),
|
||||||
|
// )));
|
||||||
|
// } else {
|
||||||
|
// ScaffoldMessenger.of(context)
|
||||||
|
// .showSnackBar(SnackBar(content: Text(state.message)));
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// builder: (context, state) {
|
||||||
|
// // The commented out section is removed for clarity, the logic is now above.
|
||||||
|
// return Padding(
|
||||||
|
// padding: const EdgeInsets.all(24.0),
|
||||||
|
// child: Form(
|
||||||
|
// key: _formKey,
|
||||||
|
// child: Column(
|
||||||
|
// mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
// children: [
|
||||||
|
// Image.asset(
|
||||||
|
// 'assets/images/logo.png',
|
||||||
|
// width: 150,
|
||||||
|
// height: 150,
|
||||||
|
// errorBuilder: (context, error, stackTrace) {
|
||||||
|
// return Icon(
|
||||||
|
// Icons.account_balance,
|
||||||
|
// size: 100,
|
||||||
|
// color: Theme.of(context).primaryColor,
|
||||||
|
// );
|
||||||
|
// },
|
||||||
|
// ),
|
||||||
|
// const SizedBox(height: 16),
|
||||||
|
// Text(
|
||||||
|
// AppLocalizations.of(context).kccb,
|
||||||
|
// style: TextStyle(
|
||||||
|
// fontSize: 32,
|
||||||
|
// fontWeight: FontWeight.bold,
|
||||||
|
// color: Theme.of(context).primaryColor,
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// const SizedBox(height: 48),
|
||||||
|
// TextFormField(
|
||||||
|
// controller: _customerNumberController,
|
||||||
|
// decoration: InputDecoration(
|
||||||
|
// labelText: AppLocalizations.of(context).customerNumber,
|
||||||
|
// border: const OutlineInputBorder(),
|
||||||
|
// isDense: true,
|
||||||
|
// filled: true,
|
||||||
|
// fillColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
|
// enabledBorder: OutlineInputBorder(
|
||||||
|
// borderSide: BorderSide(
|
||||||
|
// color: Theme.of(context).colorScheme.outline),
|
||||||
|
// ),
|
||||||
|
// focusedBorder: OutlineInputBorder(
|
||||||
|
// borderSide: BorderSide(
|
||||||
|
// color: Theme.of(context).colorScheme.primary,
|
||||||
|
// width: 2),
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// keyboardType: TextInputType.number,
|
||||||
|
// textInputAction: TextInputAction.next,
|
||||||
|
// validator: (value) {
|
||||||
|
// if (value == null || value.isEmpty) {
|
||||||
|
// return AppLocalizations.of(context).pleaseEnterUsername;
|
||||||
|
// }
|
||||||
|
// return null;
|
||||||
|
// },
|
||||||
|
// ),
|
||||||
|
// const SizedBox(height: 24),
|
||||||
|
// TextFormField(
|
||||||
|
// controller: _passwordController,
|
||||||
|
// obscureText: _obscurePassword,
|
||||||
|
// textInputAction: TextInputAction.done,
|
||||||
|
// onFieldSubmitted: (_) => _submitForm(),
|
||||||
|
// decoration: InputDecoration(
|
||||||
|
// labelText: AppLocalizations.of(context).password,
|
||||||
|
// border: const OutlineInputBorder(),
|
||||||
|
// isDense: true,
|
||||||
|
// filled: true,
|
||||||
|
// fillColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
|
// enabledBorder: OutlineInputBorder(
|
||||||
|
// borderSide: BorderSide(
|
||||||
|
// color: Theme.of(context).colorScheme.outline),
|
||||||
|
// ),
|
||||||
|
// focusedBorder: OutlineInputBorder(
|
||||||
|
// borderSide: BorderSide(
|
||||||
|
// color: Theme.of(context).colorScheme.primary,
|
||||||
|
// width: 2),
|
||||||
|
// ),
|
||||||
|
// suffixIcon: IconButton(
|
||||||
|
// icon: Icon(
|
||||||
|
// _obscurePassword
|
||||||
|
// ? Icons.visibility
|
||||||
|
// : Icons.visibility_off,
|
||||||
|
// ),
|
||||||
|
// onPressed: () {
|
||||||
|
// setState(() {
|
||||||
|
// _obscurePassword = !_obscurePassword;
|
||||||
|
// });
|
||||||
|
// },
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// validator: (value) {
|
||||||
|
// if (value == null || value.isEmpty) {
|
||||||
|
// return AppLocalizations.of(context).pleaseEnterPassword;
|
||||||
|
// }
|
||||||
|
// return null;
|
||||||
|
// },
|
||||||
|
// ),
|
||||||
|
// const SizedBox(height: 24),
|
||||||
|
// SizedBox(
|
||||||
|
// width: 250,
|
||||||
|
// child: ElevatedButton(
|
||||||
|
// onPressed: state is AuthLoading ? null : _submitForm,
|
||||||
|
// style: ElevatedButton.styleFrom(
|
||||||
|
// shape: const StadiumBorder(),
|
||||||
|
// padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
// backgroundColor:
|
||||||
|
// Theme.of(context).scaffoldBackgroundColor,
|
||||||
|
// foregroundColor: Theme.of(context).primaryColorDark,
|
||||||
|
// side: BorderSide(
|
||||||
|
// color: Theme.of(context).colorScheme.outline,
|
||||||
|
// width: 1),
|
||||||
|
// elevation: 0,
|
||||||
|
// ),
|
||||||
|
// child: state is AuthLoading
|
||||||
|
// ? const CircularProgressIndicator()
|
||||||
|
// : Text(
|
||||||
|
// AppLocalizations.of(context).login,
|
||||||
|
// style: TextStyle(
|
||||||
|
// color: Theme.of(context)
|
||||||
|
// .colorScheme
|
||||||
|
// .onPrimaryContainer),
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// const SizedBox(height: 25),
|
||||||
|
// ],
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
// },
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
return Scaffold(
|
||||||
body: BlocConsumer<AuthCubit, AuthState>(
|
body: BlocConsumer<AuthCubit, AuthState>(
|
||||||
listener: (context, state) async {
|
listener: (context, state) {
|
||||||
if (state is ShowTncDialog) {
|
if (state is ShowTncDialog) {
|
||||||
// The dialog now returns a boolean for the 'disagree' case,
|
showDialog<bool>(
|
||||||
// or it completes when the 'proceed' action is finished.
|
|
||||||
final agreed = await showDialog<bool>(
|
|
||||||
context: context,
|
context: context,
|
||||||
barrierDismissible: false,
|
barrierDismissible: false,
|
||||||
builder: (dialogContext) => TncDialog(
|
builder: (dialogContext) => TncDialog(
|
||||||
onProceed: () async {
|
onProceed: () async {
|
||||||
// This function is passed to the dialog.
|
// Pop the dialog before the cubit action
|
||||||
// It calls the cubit and completes when the cubit's work is done.
|
Navigator.of(dialogContext).pop();
|
||||||
await context
|
await context
|
||||||
.read<AuthCubit>()
|
.read<AuthCubit>()
|
||||||
.onTncDialogResult(true, state.authToken, state.users);
|
.onTncDialogResult(true, state.authToken, state.users);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// If 'agreed' is false, it means the user clicked 'Disagree'.
|
|
||||||
if (agreed == false) {
|
|
||||||
if (!context.mounted) return;
|
|
||||||
context
|
|
||||||
.read<AuthCubit>()
|
|
||||||
.onTncDialogResult(false, state.authToken, state.users);
|
|
||||||
}
|
|
||||||
} else if (state is NavigateToTncRequiredScreen) {
|
} else if (state is NavigateToTncRequiredScreen) {
|
||||||
Navigator.of(context).pushNamed(TncRequiredScreen.routeName);
|
Navigator.of(context).pushNamed(TncRequiredScreen.routeName);
|
||||||
} else if (state is NavigateToMpinSetupScreen) {
|
} else if (state is NavigateToMpinSetupScreen) {
|
||||||
Navigator.of(context).pushReplacement(
|
Navigator.of(context).push( // Use push, NOT pushReplacement
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (_) => MPinScreen(
|
builder: (_) => MPinScreen(
|
||||||
mode: MPinMode.set,
|
mode: MPinMode.set,
|
||||||
onCompleted: (_) {
|
onCompleted: (_) {
|
||||||
Navigator.of(context, rootNavigator: true).pushReplacement(
|
// Call the cubit to signal MPIN setup is complete
|
||||||
MaterialPageRoute(builder: (_) => const NavigationScaffold()),
|
context.read<AuthCubit>().mpinSetupCompleted();
|
||||||
);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else if (state is NavigateToDashboardScreen) {
|
} else if (state is Authenticated) {
|
||||||
Navigator.of(context).pushReplacement(
|
// This is the single source of truth for navigating to the dashboard
|
||||||
|
Navigator.of(context, rootNavigator: true).pushAndRemoveUntil(
|
||||||
MaterialPageRoute(builder: (_) => const NavigationScaffold()),
|
MaterialPageRoute(builder: (_) => const NavigationScaffold()),
|
||||||
|
(route) => false,
|
||||||
);
|
);
|
||||||
} else if (state is AuthError) {
|
} else if (state is AuthError) {
|
||||||
if (state.message == 'MIGRATED_USER_HAS_NO_PASSWORD') {
|
if (state.message == 'MIGRATED_USER_HAS_NO_PASSWORD') {
|
||||||
@@ -100,7 +285,7 @@ class LoginScreenState extends State<LoginScreen>
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
// The commented out section is removed for clarity, the logic is now above.
|
// The builder part remains largely the same, focusing on UI display
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.all(24.0),
|
padding: const EdgeInsets.all(24.0),
|
||||||
child: Form(
|
child: Form(
|
||||||
|
|||||||
@@ -185,19 +185,16 @@ class _MPinScreenState extends State<MPinScreen> with TickerProviderStateMixin {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case MPinMode.confirm:
|
case MPinMode.confirm:
|
||||||
if (widget.initialPin == pin) {
|
if (widget.initialPin == pin) {
|
||||||
// 1) persist the pin
|
// 1) persist the pin
|
||||||
await storage.write('mpin', pin);
|
await storage.write('mpin', pin);
|
||||||
|
|
||||||
// 3) now clear the entire navigation stack and go to your main scaffold
|
// 2) Call the onCompleted callback to let the parent handle navigation
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
Navigator.of(context, rootNavigator: true).pushAndRemoveUntil(
|
widget.onCompleted?.call(pin);
|
||||||
MaterialPageRoute(builder: (_) => const NavigationScaffold()),
|
}
|
||||||
(route) => false,
|
} else {
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_isError = true;
|
_isError = true;
|
||||||
errorText = AppLocalizations.of(context).pinsDoNotMatch;
|
errorText = AppLocalizations.of(context).pinsDoNotMatch;
|
||||||
|
|||||||
@@ -75,10 +75,17 @@ class _TncDialogState extends State<TncDialog> {
|
|||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
// Disable button while loading
|
// Disable button while loading
|
||||||
onPressed: _isLoading ? null : () {
|
onPressed: _isLoading
|
||||||
// Pop with false to indicate disagreement
|
? null
|
||||||
Navigator.of(context).pop(false);
|
: () {
|
||||||
},
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text(
|
||||||
|
'You must agree to the terms and conditions to proceed.'),
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
child: const Text('Disagree'),
|
child: const Text('Disagree'),
|
||||||
),
|
),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
|
|||||||
Reference in New Issue
Block a user