saml flow
This commit is contained in:
parent
750a3becad
commit
1288a67256
@ -19,12 +19,12 @@ if (project.hasProperty('google-services.json')) {
|
||||
|
||||
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
|
||||
if (flutterVersionCode == null) {
|
||||
flutterVersionCode = '58'
|
||||
flutterVersionCode = '61'
|
||||
}
|
||||
|
||||
def flutterVersionName = localProperties.getProperty('flutter.versionName')
|
||||
if (flutterVersionName == null) {
|
||||
flutterVersionName = '2.0.20'
|
||||
flutterVersionName = '2.0.23'
|
||||
}
|
||||
|
||||
def keystoreProperties = new Properties()
|
||||
|
||||
BIN
assets/images/login/microsoft.png
Normal file
BIN
assets/images/login/microsoft.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 200 B |
@ -43,16 +43,19 @@ class Environment {
|
||||
}
|
||||
}
|
||||
|
||||
// static String get chatbotUrl {
|
||||
// switch (flavor) {
|
||||
// case Flavor.dev:
|
||||
// return "https://venbait.in/nhance/dev/chatbot";
|
||||
// case Flavor.uat:
|
||||
// return "https://uat.nhanceindia.in/zenith/chatbot";
|
||||
// case Flavor.prod:
|
||||
// return "https://app.nhanceindia.in/zenith/chatbot";
|
||||
// }
|
||||
// }
|
||||
/// SAML IdP entry (`…/saml/login?email=…`). Web: same-tab redirect. Mobile: WebView.
|
||||
static String get SsoLogin {
|
||||
switch (flavor) {
|
||||
case Flavor.dev:
|
||||
return "https://venbait.in/saml/login";
|
||||
case Flavor.uat:
|
||||
return "https://appstage.nhanceindia.in/apex/saml/login";
|
||||
case Flavor.prod:
|
||||
return "https://app.nhanceindia.in/saml/login";
|
||||
case Flavor.prod1:
|
||||
return "https://app.nhanceindia.in/saml/login";
|
||||
}
|
||||
}
|
||||
|
||||
static String get baseHref {
|
||||
switch (flavor) {
|
||||
|
||||
@ -1,16 +1,16 @@
|
||||
import 'dart:developer' as developer;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/foundation.dart' show debugPrint, kDebugMode;
|
||||
import 'package:nhance_app_pwa/config/environment.dart';
|
||||
|
||||
String _timestamp() => DateTime.now().toIso8601String();
|
||||
|
||||
enum LogLevel { debug, info, warning, error }
|
||||
|
||||
class LoggerConfig {
|
||||
// Logging is always disabled in non-debug builds.
|
||||
static bool enabled = true;
|
||||
|
||||
// Keep debug logs off by default to avoid local slowdown on noisy screens.
|
||||
/// Logs below this level are dropped ([logDebug] when this is [LogLevel.info]).
|
||||
static LogLevel minLevel = LogLevel.info;
|
||||
|
||||
// Stack parsing is expensive; keep it disabled unless specifically needed.
|
||||
@ -42,8 +42,20 @@ void _log(
|
||||
Object? error,
|
||||
StackTrace? stackTrace,
|
||||
}) {
|
||||
if (!kDebugMode || !LoggerConfig.enabled) return;
|
||||
if (level.index < LoggerConfig.minLevel.index) return;
|
||||
if (!LoggerConfig.enabled) return;
|
||||
|
||||
// Verbose minimum: UAT (all modes), or local Dev when using a debug Flutter build.
|
||||
final LogLevel effectiveMinLevel = switch (Environment.flavor) {
|
||||
Flavor.uat => LogLevel.debug,
|
||||
Flavor.dev when kDebugMode => LogLevel.debug,
|
||||
_ => LoggerConfig.minLevel,
|
||||
};
|
||||
if (level.index < effectiveMinLevel.index) return;
|
||||
|
||||
// Debug lines: debug Flutter build (typical `flutter run`), or any UAT build.
|
||||
if (level == LogLevel.debug && !kDebugMode && Environment.flavor != Flavor.uat) {
|
||||
return;
|
||||
}
|
||||
|
||||
final inferredTag = tag ??
|
||||
(LoggerConfig.includeCallerFromStack
|
||||
@ -58,8 +70,21 @@ void _log(
|
||||
error: error,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
|
||||
// `developer.log` is easy to miss in release/profile (IDE filters, logcat tags).
|
||||
// UAT: always mirror to stdout-style logging so `adb logcat`, Xcode, and web consoles show lines.
|
||||
// Debug builds: mirror too for parity with `print` during local runs.
|
||||
if (Environment.flavor == Flavor.uat || kDebugMode) {
|
||||
debugPrint(fullMessage);
|
||||
if (Environment.flavor == Flavor.uat && (error != null || stackTrace != null)) {
|
||||
debugPrint('$error\n$stackTrace');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// **Dev + localhost:** shown when you use `flutter run` (debug mode); not in `flutter run --release`.
|
||||
/// **UAT:** also emitted in profile/release (verbose).
|
||||
/// **Prod / Prod1:** debug Flutter build only; in release/profile use [logInfo] or `print`.
|
||||
void logDebug(Object? message, {String? tag}) {
|
||||
_log(LogLevel.debug, message, tag: tag);
|
||||
}
|
||||
|
||||
126
lib/main.dart
126
lib/main.dart
@ -12,6 +12,7 @@ import 'package:nhance_app_pwa/pages/enrollment/addons.dart';
|
||||
import 'package:nhance_app_pwa/pages/enrollment/empDetails.dart';
|
||||
import 'package:nhance_app_pwa/pages/enrollment/empReview.dart';
|
||||
import 'package:nhance_app_pwa/pages/login.dart';
|
||||
import 'package:nhance_app_pwa/pages/sso_login_return_page.dart';
|
||||
import 'package:nhance_app_pwa/pages/postEnrollment/AddPolicyScreen.dart';
|
||||
import 'package:nhance_app_pwa/pages/postEnrollment/claimprocess.dart';
|
||||
import 'package:nhance_app_pwa/pages/postEnrollment/claims.dart';
|
||||
@ -98,6 +99,7 @@ Future<String?> tokenRedirectLogic(
|
||||
logDebug('ABCDEFGH');
|
||||
const guestRoutes = [
|
||||
'/login',
|
||||
'/sso-login',
|
||||
'/verify',
|
||||
'/mailVerify',
|
||||
'/pinPage',
|
||||
@ -107,21 +109,32 @@ Future<String?> tokenRedirectLogic(
|
||||
final hasToken = await TokenService.hasValidToken();
|
||||
logDebug('hasToken : $hasToken');
|
||||
final location = state.matchedLocation;
|
||||
final session = SessionManager();
|
||||
|
||||
logDebug('Location: $location');
|
||||
logDebug('hasToken: $hasToken');
|
||||
|
||||
// Already logged in → prevent guest pages
|
||||
if (hasToken) {
|
||||
await session.restoreSession();
|
||||
}
|
||||
|
||||
if (!kIsWeb && hasToken) {
|
||||
final reauth = await _mobileReauthRedirectIfNeeded(location);
|
||||
if (reauth != null) return reauth;
|
||||
}
|
||||
|
||||
// Logged in on a guest route → home (only when mobile session is unlocked).
|
||||
if (hasToken && guestRoutes.contains(location)) {
|
||||
if (!kIsWeb && !session.isMobileAppSessionUnlocked) {
|
||||
return null;
|
||||
}
|
||||
return '/home';
|
||||
}
|
||||
|
||||
// Not logged in → allow guest pages directly
|
||||
if (!hasToken && guestRoutes.contains(location)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Protected route without token
|
||||
if (!hasToken && !guestRoutes.contains(location)) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
ToastHelper.showErrorToast(
|
||||
@ -130,15 +143,56 @@ Future<String?> tokenRedirectLogic(
|
||||
return '/login';
|
||||
}
|
||||
|
||||
// ✅ Restore session (decode JWT etc.)
|
||||
await SessionManager().restoreSession();
|
||||
return null;
|
||||
}
|
||||
|
||||
return null; // no redirect
|
||||
/// After a full app restart (new process), require MPIN or password login before
|
||||
/// protected routes. Tokens remain in secure storage. Minimizing does not lock.
|
||||
Future<String?> _mobileReauthRedirectIfNeeded(String location) async {
|
||||
const allowedWhileLocked = <String>{
|
||||
'/login',
|
||||
'/pinPage',
|
||||
'/verify',
|
||||
'/mailVerify',
|
||||
'/sso-login',
|
||||
'/pinSettingPage',
|
||||
'/changePin',
|
||||
};
|
||||
final session = SessionManager();
|
||||
if (session.isMobileAppSessionUnlocked) return null;
|
||||
if (allowedWhileLocked.contains(location)) return null;
|
||||
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final skip = prefs.getString('is_mpin_skipped');
|
||||
return skip == '0' ? '/pinPage' : '/login';
|
||||
}
|
||||
|
||||
|
||||
/// When not using `-t lib/config/main_uat.dart`, pass e.g. `--dart-define=APP_FLAVOR=uat`
|
||||
/// so [Environment.flavor] (and logger UAT rules) match the build.
|
||||
void _applyAppFlavorFromDartDefine() {
|
||||
const fromDefine = String.fromEnvironment('APP_FLAVOR', defaultValue: '');
|
||||
switch (fromDefine.toLowerCase()) {
|
||||
case 'uat':
|
||||
Environment.flavor = Flavor.uat;
|
||||
break;
|
||||
case 'prod':
|
||||
Environment.flavor = Flavor.prod;
|
||||
break;
|
||||
case 'prod1':
|
||||
Environment.flavor = Flavor.prod1;
|
||||
break;
|
||||
case 'dev':
|
||||
Environment.flavor = Flavor.dev;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> startApp() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
_applyAppFlavorFromDartDefine();
|
||||
|
||||
await TokenService.clearLegacyWebAuthFromSharedPreferences();
|
||||
|
||||
@ -156,8 +210,11 @@ Future<void> startApp() async {
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/splash', builder: (context, state) => const SplashScreen()),
|
||||
GoRoute(path: '/login', builder: (context, state) => login()),
|
||||
|
||||
GoRoute(path: '/login', builder: (context, state) => const login()),
|
||||
GoRoute(
|
||||
path: '/sso-login',
|
||||
builder: (context, state) => const SsoLoginReturnPage(),
|
||||
),
|
||||
|
||||
GoRoute(
|
||||
path: '/mailVerify',
|
||||
@ -179,57 +236,74 @@ Future<void> startApp() async {
|
||||
path: '/changePin',
|
||||
builder: (context, state) => changePin(),
|
||||
),
|
||||
GoRoute(path: '/home', builder: (context, state) => Home()),
|
||||
GoRoute(path: '/profile', builder: (context, state) => profile()),
|
||||
GoRoute(path: '/help', builder: (context, state) => help()),
|
||||
GoRoute(
|
||||
path: '/claimprocess', builder: (context, state) => claimprocess()),
|
||||
GoRoute(path: '/wellness', builder: (context, state) => wellness()),
|
||||
path: '/home',
|
||||
builder: (context, state) => ChatbotHost(
|
||||
child: Home(),
|
||||
triggerIntroOnHome: true,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/profile',
|
||||
builder: (context, state) => ChatbotHost(child: profile()),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/help',
|
||||
builder: (context, state) => ChatbotHost(child: help()),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/claimprocess', builder: (context, state) => ChatbotHost(child: claimprocess()),
|
||||
),
|
||||
GoRoute(path: '/wellness', builder: (context, state) => ChatbotHost(child:wellness()),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/privacypolicy', builder: (context, state) => privacypolicy()),
|
||||
GoRoute(path: '/termsofuse', builder: (context, state) => termsofuse()),
|
||||
GoRoute(
|
||||
path: '/generalExclusionsDeductibles',
|
||||
builder: (context, state) => generalExclusionsDeductibles()),
|
||||
path: '/generalExclusionsDeductibles',
|
||||
builder: (context, state) => ChatbotHost(child:generalExclusionsDeductibles()),
|
||||
),
|
||||
|
||||
GoRoute(path: '/tickets', builder: (context, state) => tickets()),
|
||||
GoRoute(path: '/tickets', builder: (context, state) => ChatbotHost(child:tickets()),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/policies',
|
||||
builder: (context, state) {
|
||||
final arguments =
|
||||
state.extra as Map<String, dynamic>?; // 👈 receive here
|
||||
return policies(arguments: arguments);
|
||||
return ChatbotHost(child:policies(arguments: arguments));
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: '/claims',
|
||||
builder: (context, state) {
|
||||
final int tabIndex = state.extra as int? ?? 0; // default to 0
|
||||
return claims(initialTab: tabIndex);
|
||||
return ChatbotHost(child: claims(initialTab: tabIndex));
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: '/planclaimsform',
|
||||
builder: (context, state) {
|
||||
final details = state.extra as Map<String, dynamic>?;
|
||||
return planclaimsform(details: details);
|
||||
return ChatbotHost(child: planclaimsform(details: details));
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: '/retailClaimForm',
|
||||
builder: (context, state) {
|
||||
final details = state.extra as Map<String, dynamic>?;
|
||||
return retailClaimForm(details: details);
|
||||
return ChatbotHost(child:retailClaimForm(details: details));
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: '/raisedTicketHistory',
|
||||
builder: (context, state) => raisedTicketHistory()),
|
||||
path: '/raisedTicketHistory',
|
||||
builder: (context, state) => ChatbotHost(child:raisedTicketHistory()),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/tickettracklist/:ticketID',
|
||||
builder: (context, state) {
|
||||
final ticketID = state.pathParameters['ticketID']!;
|
||||
return tickettracklist(ticketID: ticketID);
|
||||
return ChatbotHost(child:tickettracklist(ticketID: ticketID));
|
||||
},
|
||||
),
|
||||
// GoRoute(
|
||||
@ -245,7 +319,7 @@ Future<void> startApp() async {
|
||||
),
|
||||
GoRoute(
|
||||
path: '/faqs',
|
||||
builder: (context, state) => faqs(),
|
||||
builder: (context, state) => ChatbotHost(child: faqs()),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/addOnsDetails', builder: (context, state) => addOnsDetails()),
|
||||
@ -294,7 +368,7 @@ Future<void> startApp() async {
|
||||
|
||||
// ✅ Middleware hook
|
||||
redirect: (context, state) async =>
|
||||
await tokenRedirectLogic(context, state),
|
||||
await tokenRedirectLogic(context, state),
|
||||
);
|
||||
|
||||
runApp(
|
||||
@ -332,4 +406,4 @@ class _MyAppState extends State<MyApp> {
|
||||
debugShowCheckedModeBanner: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -173,7 +173,7 @@ class _changesPasswordState extends State<changesPassword> {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
// ToastHelper.showErrorToast(context, 'Something went wrong');
|
||||
// ToastHelper.showErrorToast(context, 'Unable to process. Please try again later');
|
||||
logDebug('Error: $e');
|
||||
}
|
||||
|
||||
|
||||
@ -307,29 +307,42 @@ class _MyEmailVerifyState extends State<MyEmailVerify> {
|
||||
if (!context.mounted) return;
|
||||
context.go('/login');
|
||||
|
||||
} else if (response.statusCode == 403) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
await SessionManager().clear();
|
||||
ToastHelper.showErrorToast(context, 'Session Out');
|
||||
if (!context.mounted) return;
|
||||
context.go('/login');
|
||||
|
||||
} else if (response.statusCode == 451) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
final body = jsonDecode(response.body);
|
||||
final message = body['message'];
|
||||
ToastHelper.showWarningToast(context, message);
|
||||
} else if (response.statusCode == 429) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
final body = jsonDecode(response.body);
|
||||
final message = body['message'];
|
||||
ToastHelper.showWarningToast(context, message);
|
||||
Map<String, dynamic> data = json.decode(response.body);
|
||||
if (data.containsKey('error')) {
|
||||
final message = data['error']['message'];
|
||||
ToastHelper.showErrorToast(context, message);
|
||||
} else {
|
||||
final message = data['message'];
|
||||
ToastHelper.showErrorToast(context, message);
|
||||
}
|
||||
} else if (response.statusCode == 403) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
Map<String, dynamic> data = json.decode(response.body);
|
||||
if (data.containsKey('error')) {
|
||||
final message = data['error']['message'];
|
||||
ToastHelper.showErrorToast(context, message);
|
||||
} else {
|
||||
final message = data['message'];
|
||||
ToastHelper.showErrorToast(context, message);
|
||||
}
|
||||
} else if (response.statusCode == 451) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
Map<String, dynamic> data = json.decode(response.body);
|
||||
if (data.containsKey('error')) {
|
||||
final message = data['error']['message'];
|
||||
ToastHelper.showErrorToast(context, message);
|
||||
} else {
|
||||
final message = data['message'];
|
||||
ToastHelper.showErrorToast(context, message);
|
||||
}
|
||||
} else {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
@ -340,7 +353,7 @@ class _MyEmailVerifyState extends State<MyEmailVerify> {
|
||||
final SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
prefs.clear();
|
||||
logDebug('Error: $e');
|
||||
ToastHelper.showWarningToast(context, 'Something went wrong');
|
||||
ToastHelper.showWarningToast(context, 'Unable to process. Please try again later');
|
||||
// Show a Snackbar if there's an error while verifying OTP
|
||||
logDebug('Failed to verify OTP. Please try again.');
|
||||
} finally {
|
||||
@ -630,12 +643,48 @@ class _MyEmailVerifyState extends State<MyEmailVerify> {
|
||||
ToastHelper.showErrorToast(context, message);
|
||||
logDebug('Invalid mobile number');
|
||||
}
|
||||
} else if (response.statusCode == 429) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
Map<String, dynamic> data = json.decode(response.body);
|
||||
if (data.containsKey('error')) {
|
||||
final message = data['error']['message'];
|
||||
ToastHelper.showErrorToast(context, message);
|
||||
} else {
|
||||
final message = data['message'];
|
||||
ToastHelper.showErrorToast(context, message);
|
||||
}
|
||||
} else if (response.statusCode == 403) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
Map<String, dynamic> data = json.decode(response.body);
|
||||
if (data.containsKey('error')) {
|
||||
final message = data['error']['message'];
|
||||
ToastHelper.showErrorToast(context, message);
|
||||
} else {
|
||||
final message = data['message'];
|
||||
ToastHelper.showErrorToast(context, message);
|
||||
}
|
||||
} else if (response.statusCode == 451) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
Map<String, dynamic> data = json.decode(response.body);
|
||||
if (data.containsKey('error')) {
|
||||
final message = data['error']['message'];
|
||||
ToastHelper.showErrorToast(context, message);
|
||||
} else {
|
||||
final message = data['message'];
|
||||
ToastHelper.showErrorToast(context, message);
|
||||
}
|
||||
} else {
|
||||
ToastHelper.showErrorToast(context, 'Something went wrong');
|
||||
ToastHelper.showErrorToast(context, 'Unable to process. Please try again later');
|
||||
throw Exception('Failed to verify mobile number');
|
||||
}
|
||||
} catch (e) {
|
||||
ToastHelper.showErrorToast(context, 'Something went wrong');
|
||||
ToastHelper.showErrorToast(context, 'Unable to process. Please try again later');
|
||||
logDebug('Error: $e');
|
||||
}
|
||||
}
|
||||
@ -689,10 +738,12 @@ class _MyEmailVerifyState extends State<MyEmailVerify> {
|
||||
|
||||
if (_postToken != null && _postToken.isNotEmpty) {
|
||||
ToastHelper.showSuccessToast(context, 'Successfully Logged In');
|
||||
SessionManager().unlockMobileAppSession();
|
||||
context.replace('/home');
|
||||
// context.go('/home');
|
||||
// Navigator.pushReplacementNamed(context, 'home');
|
||||
} else {
|
||||
SessionManager().unlockMobileAppSession();
|
||||
context.replace('/empDetails');
|
||||
// context.go('/empDetails');
|
||||
// Navigator.pushReplacementNamed(context, 'empDetails');
|
||||
@ -716,12 +767,48 @@ class _MyEmailVerifyState extends State<MyEmailVerify> {
|
||||
context.go('/pinSettingPage');
|
||||
// Navigator.pushReplacementNamed(context, 'pinSettingPage');
|
||||
}
|
||||
} else if (response.statusCode == 429) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
Map<String, dynamic> data = json.decode(response.body);
|
||||
if (data.containsKey('error')) {
|
||||
final message = data['error']['message'];
|
||||
ToastHelper.showErrorToast(context, message);
|
||||
} else {
|
||||
final message = data['message'];
|
||||
ToastHelper.showErrorToast(context, message);
|
||||
}
|
||||
} else if (response.statusCode == 403) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
Map<String, dynamic> data = json.decode(response.body);
|
||||
if (data.containsKey('error')) {
|
||||
final message = data['error']['message'];
|
||||
ToastHelper.showErrorToast(context, message);
|
||||
} else {
|
||||
final message = data['message'];
|
||||
ToastHelper.showErrorToast(context, message);
|
||||
}
|
||||
} else if (response.statusCode == 451) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
Map<String, dynamic> data = json.decode(response.body);
|
||||
if (data.containsKey('error')) {
|
||||
final message = data['error']['message'];
|
||||
ToastHelper.showErrorToast(context, message);
|
||||
} else {
|
||||
final message = data['message'];
|
||||
ToastHelper.showErrorToast(context, message);
|
||||
}
|
||||
} else {
|
||||
ToastHelper.showErrorToast(context, 'Something went wrong');
|
||||
ToastHelper.showErrorToast(context, 'Unable to process. Please try again later');
|
||||
throw Exception('Failed to verify pin number');
|
||||
}
|
||||
} catch (e) {
|
||||
ToastHelper.showErrorToast(context, 'Something went wrong');
|
||||
ToastHelper.showErrorToast(context, 'Unable to process. Please try again later');
|
||||
logDebug('Error: $e');
|
||||
}
|
||||
}
|
||||
@ -760,11 +847,11 @@ class _MyEmailVerifyState extends State<MyEmailVerify> {
|
||||
);
|
||||
}
|
||||
} else {
|
||||
ToastHelper.showErrorToast(context, 'Something went wrong');
|
||||
ToastHelper.showErrorToast(context, 'Unable to process. Please try again later');
|
||||
throw Exception('Failed to verify pin number');
|
||||
}
|
||||
} catch (e) {
|
||||
ToastHelper.showErrorToast(context, 'Something went wrong');
|
||||
ToastHelper.showErrorToast(context, 'Unable to process. Please try again later');
|
||||
logDebug('Error: $e');
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -347,7 +347,7 @@ class _empDetailsState extends State<empDetails> {
|
||||
gpaDataIsEmpty = 0;
|
||||
});
|
||||
// Handle other status codes
|
||||
ToastHelper.showWarningToast(context, 'Something went wrong');
|
||||
ToastHelper.showWarningToast(context, 'Unable to process. Please try again later');
|
||||
logDebug('Request failed with status: ${response['code']}');
|
||||
}
|
||||
} catch (e) {
|
||||
@ -1161,6 +1161,181 @@ class _empDetailsState extends State<empDetails> {
|
||||
|
||||
logDebug("Normalized relationship list: $relationshipObjects");
|
||||
|
||||
Future<bool> showEditConfirmationDialog(
|
||||
BuildContext dialogContext,
|
||||
Map<String, dynamic> oldData,
|
||||
Map<String, dynamic> newData,
|
||||
bool relationshipEnabled,
|
||||
) async {
|
||||
final oldRelationship = (oldData["relationship"] ?? "").toString().trim();
|
||||
final oldName = (oldData["name"] ?? "").toString().trim();
|
||||
final oldDob = (oldData["dob"] ?? "").toString().trim();
|
||||
|
||||
final newRelationship = (newData["relationship"] ?? "").toString().trim();
|
||||
final newName = (newData["memberName"] ?? "").toString().trim();
|
||||
final newDob = (newData["dateOfBirth"] ?? "").toString().trim();
|
||||
|
||||
final List<Map<String, String>> changedFields = [];
|
||||
if (oldRelationship != newRelationship) {
|
||||
changedFields.add({
|
||||
"label": "Relationship",
|
||||
"old": oldRelationship,
|
||||
"new": newRelationship,
|
||||
});
|
||||
}
|
||||
if (oldName != newName) {
|
||||
changedFields.add({
|
||||
"label": "Member Name",
|
||||
"old": oldName,
|
||||
"new": newName,
|
||||
});
|
||||
}
|
||||
if (oldDob != newDob) {
|
||||
changedFields.add({
|
||||
"label": "Date of Birth",
|
||||
"old": oldDob,
|
||||
"new": newDob,
|
||||
});
|
||||
}
|
||||
|
||||
if (changedFields.isEmpty) {
|
||||
ToastHelper.showWarningToast(
|
||||
dialogContext, "No changes detected to save");
|
||||
return false;
|
||||
}
|
||||
|
||||
final List<Map<String, String>> reviewFields = [];
|
||||
if (relationshipEnabled || oldRelationship != newRelationship) {
|
||||
reviewFields.add({
|
||||
"label": "Relationship",
|
||||
"old": oldRelationship,
|
||||
"new": newRelationship,
|
||||
});
|
||||
}
|
||||
reviewFields.add({
|
||||
"label": "Member Name",
|
||||
"old": oldName,
|
||||
"new": newName,
|
||||
});
|
||||
reviewFields.add({
|
||||
"label": "Date of Birth",
|
||||
"old": oldDob,
|
||||
"new": newDob,
|
||||
});
|
||||
|
||||
final bool? confirmed = await showDialog<bool>(
|
||||
context: dialogContext,
|
||||
barrierDismissible: false,
|
||||
builder: (confirmContext) {
|
||||
return AlertDialog(
|
||||
title: Text(
|
||||
"Confirm Changes",
|
||||
style: GoogleFonts.poppins(fontWeight: FontWeight.w600),
|
||||
),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Please review old and new values before saving:",
|
||||
style: GoogleFonts.poppins(fontSize: 13),
|
||||
),
|
||||
SizedBox(height: 12),
|
||||
...reviewFields.map((field) {
|
||||
final oldValue = (field["old"] ?? '').trim().isEmpty
|
||||
? '-'
|
||||
: (field["old"] ?? '');
|
||||
final newValue = (field["new"] ?? '').trim().isEmpty
|
||||
? '-'
|
||||
: (field["new"] ?? '');
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF9F9F9),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: const Color(0xFFE0E0E0)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
field["label"] ?? '',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: const Color(0xFFE26728),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13,
|
||||
color: const Color(0xFF232526),
|
||||
),
|
||||
children: [
|
||||
const TextSpan(
|
||||
text: "OLD: ",
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Color(0xFFD32F2F),
|
||||
),
|
||||
),
|
||||
TextSpan(text: oldValue),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13,
|
||||
color: const Color(0xFF232526),
|
||||
),
|
||||
children: [
|
||||
const TextSpan(
|
||||
text: "NEW: ",
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Color(0xFF2E7D32),
|
||||
),
|
||||
),
|
||||
TextSpan(text: newValue),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(confirmContext, false),
|
||||
child: Text("Cancel"),
|
||||
),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Color(0xFFE26728),
|
||||
),
|
||||
onPressed: () => Navigator.pop(confirmContext, true),
|
||||
child: Text("Confirm", style: TextStyle(color: Colors.white)),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
return confirmed == true;
|
||||
}
|
||||
|
||||
// Reset UI Fields
|
||||
_relationShipController.clear();
|
||||
_memberNameController.clear();
|
||||
@ -1323,7 +1498,7 @@ class _empDetailsState extends State<empDetails> {
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Color(0xFFE26728),
|
||||
),
|
||||
onPressed: () {
|
||||
onPressed: () async {
|
||||
if (_memberNameController.text.isEmpty ||
|
||||
_dobController.text.isEmpty ||
|
||||
dropdownValue == null) {
|
||||
@ -1342,6 +1517,18 @@ class _empDetailsState extends State<empDetails> {
|
||||
logDebug(floaterData);
|
||||
logDebug(selectedFloaterData);
|
||||
|
||||
if (action == "Edit" && floaterData != null) {
|
||||
final shouldProceed = await showEditConfirmationDialog(
|
||||
context,
|
||||
floaterData,
|
||||
formData,
|
||||
action != "Edit",
|
||||
);
|
||||
if (!shouldProceed) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
saveFamilyMemberDetails(
|
||||
formData,
|
||||
selectedFloaterData ?? floaterData ?? {},
|
||||
|
||||
@ -32,6 +32,7 @@ class _empReviewDetailsState extends State<empReviewDetails> {
|
||||
List<String> typeOrder = [
|
||||
'GPA',
|
||||
'GMC',
|
||||
'GMC - OPD',
|
||||
'GMC - Parents',
|
||||
'GMC - Topup',
|
||||
'GMC - Topup(Parents)'
|
||||
@ -111,10 +112,13 @@ class _empReviewDetailsState extends State<empReviewDetails> {
|
||||
dynamic topUpSiSelectedSI;
|
||||
dynamic topUpSumInsured;
|
||||
dynamic topUpFloaterTextHeading;
|
||||
dynamic opdFloaterTextHeading;
|
||||
dynamic topUpECardDownload;
|
||||
bool topUpIsPremiumSummary = false;
|
||||
dynamic topUpDisclaimer;
|
||||
|
||||
int showHideTopUpCard = 0;
|
||||
int showHideOpdCard = 0;
|
||||
int showHideAddOnsCard = 0;
|
||||
int showHideTopUpParentCard = 0;
|
||||
dynamic iAgreeForAddOn = [];
|
||||
@ -133,7 +137,25 @@ class _empReviewDetailsState extends State<empReviewDetails> {
|
||||
dynamic topUpParentFloterTextHeading;
|
||||
dynamic topUpParentDisclaimer;
|
||||
// int topUpParentOpenForEnrollment = 0;
|
||||
dynamic opdPolicies = [];
|
||||
dynamic opdClientPolicyId;
|
||||
dynamic opdSlabRates;
|
||||
dynamic opdFamilyFloater;
|
||||
dynamic opdPolicyName;
|
||||
dynamic opdPolicyType;
|
||||
dynamic opdMappedFamilyFloatersSi;
|
||||
dynamic opdECardDownload;
|
||||
int opdOpenForEnrollment = 0;
|
||||
dynamic opdTypeName;
|
||||
dynamic opdSiValue;
|
||||
dynamic opdSiSelectedSI;
|
||||
dynamic opdSiPremiumValue;
|
||||
dynamic opdSiPremiumGst;
|
||||
dynamic opdSiTotalAmt;
|
||||
bool opdIsPremiumSummary = false;
|
||||
dynamic opdDisclaimer;
|
||||
int activeSiData = 0;
|
||||
int activeOPDSiData = 0;
|
||||
int activeSiParentData = 0;
|
||||
int activeDependentData = 0;
|
||||
int activePremiumSummary = 0;
|
||||
@ -215,6 +237,7 @@ class _empReviewDetailsState extends State<empReviewDetails> {
|
||||
getClientLogoAndDetails();
|
||||
}
|
||||
getGmcSiTopUp();
|
||||
getGmcOPD();
|
||||
getGmcSiParentTopUp();
|
||||
getGmcDependentAddOns();
|
||||
getGpaEmpPolicyDetails(enrollmentEmpPrimaryId);
|
||||
@ -268,6 +291,11 @@ class _empReviewDetailsState extends State<empReviewDetails> {
|
||||
return intValue != 0;
|
||||
}
|
||||
|
||||
bool _isOpenForEnrollment(dynamic value) {
|
||||
if (value is int) return value == 1;
|
||||
return value?.toString() == '1';
|
||||
}
|
||||
|
||||
void primarySummary() async {
|
||||
final SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
if (prefs.containsKey('siData')) {
|
||||
@ -286,6 +314,21 @@ class _empReviewDetailsState extends State<empReviewDetails> {
|
||||
topUpSiTotalAmt = firstSiData['topUpSiTotalAmt'];
|
||||
}
|
||||
}
|
||||
if (prefs.containsKey('opdData')) {
|
||||
activeOPDSiData = 1;
|
||||
String? opdDataJson = prefs.getString('opdData');
|
||||
logDebug('siDataJson');
|
||||
logDebug(opdDataJson);
|
||||
List<dynamic> siDataList = jsonDecode(opdDataJson!);
|
||||
if (siDataList.isNotEmpty) {
|
||||
// Access the first map in the list
|
||||
Map<String, dynamic> firstSiData = siDataList.first;
|
||||
// Get the value of topUpSiPremiumValue
|
||||
opdSiPremiumValue = firstSiData['opdSiPremiumValue'];
|
||||
opdSiPremiumGst = firstSiData['opdSiPremiumGst'];
|
||||
opdSiTotalAmt = firstSiData['opdSiTotalAmt'];
|
||||
}
|
||||
}
|
||||
if (prefs.containsKey('siParentData')) {
|
||||
activeSiParentData = 1;
|
||||
String? siParentDataJson = prefs.getString('siParentData');
|
||||
@ -327,7 +370,8 @@ class _empReviewDetailsState extends State<empReviewDetails> {
|
||||
logDebug(addOnsDependentTotalAmt);
|
||||
}
|
||||
}
|
||||
if (prefs.containsKey('siData') &&
|
||||
|
||||
if (prefs.containsKey('siData') && prefs.containsKey('opdData') &&
|
||||
prefs.containsKey('siParentData') &&
|
||||
prefs.containsKey('dependentData')) {
|
||||
activePremiumSummary = 0;
|
||||
@ -343,9 +387,9 @@ class _empReviewDetailsState extends State<empReviewDetails> {
|
||||
logDebug('gmcTotalAmt: $gmcTotalAmt');
|
||||
|
||||
if (topUpSiTotalAmt != null) totalPayableAmt += topUpSiTotalAmt;
|
||||
if (opdSiTotalAmt != null) totalPayableAmt += opdSiTotalAmt;
|
||||
if (topUpParentSiTotalAmt != null) totalPayableAmt += topUpParentSiTotalAmt;
|
||||
if (addOnsDependentTotalAmt != null)
|
||||
totalPayableAmt += addOnsDependentTotalAmt;
|
||||
if (addOnsDependentTotalAmt != null) totalPayableAmt += addOnsDependentTotalAmt;
|
||||
|
||||
logDebug('Final Total Payable Amount: $totalPayableAmt');
|
||||
}
|
||||
@ -423,7 +467,7 @@ class _empReviewDetailsState extends State<empReviewDetails> {
|
||||
|
||||
for (var item in gpaPolicies) {
|
||||
// Extract disclaimer value from each object
|
||||
if (item['OpenForEnrollment'] == '1') {
|
||||
if (_isOpenForEnrollment(item['OpenForEnrollment'])) {
|
||||
dynamic disclaimer = item['disclaimer'];
|
||||
if (disclaimer != null && disclaimer.isNotEmpty) {
|
||||
// Temporary list to hold new entries
|
||||
@ -472,7 +516,7 @@ class _empReviewDetailsState extends State<empReviewDetails> {
|
||||
gpaDataIsEmpty = 0;
|
||||
});
|
||||
// Handle other status codes
|
||||
ToastHelper.showWarningToast(context, 'Something went wrong');
|
||||
ToastHelper.showWarningToast(context, 'Unable to process. Please try again later');
|
||||
logDebug('Request failed with status: ${response['code']}');
|
||||
}
|
||||
} catch (e) {
|
||||
@ -508,7 +552,7 @@ class _empReviewDetailsState extends State<empReviewDetails> {
|
||||
}
|
||||
|
||||
for (var item in gmcPolicies) {
|
||||
if (item['OpenForEnrollment'] == '1') {
|
||||
if (_isOpenForEnrollment(item['OpenForEnrollment'])) {
|
||||
dynamic disclaimer = item['disclaimer'];
|
||||
if (disclaimer != null && disclaimer.isNotEmpty) {
|
||||
// Temporary list to hold new entries
|
||||
@ -632,7 +676,7 @@ class _empReviewDetailsState extends State<empReviewDetails> {
|
||||
topUpDisclaimer =
|
||||
await topUpSiPolicies['gmc_si_topup']['disclaimer'];
|
||||
|
||||
if (topUpOpenForEnrollment == '1' && activeSiData == 1) {
|
||||
if (_isOpenForEnrollment(topUpOpenForEnrollment) && activeSiData == 1) {
|
||||
if (topUpDisclaimer != null && topUpDisclaimer.isNotEmpty) {
|
||||
// Temporary list to hold new entries
|
||||
List<Map<String, dynamic>> newEntries = [];
|
||||
@ -693,6 +737,128 @@ class _empReviewDetailsState extends State<empReviewDetails> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> getGmcOPD() async {
|
||||
try {
|
||||
if (enrollmentClient_id == null ||
|
||||
enrollmentEmpCodeString == null ||
|
||||
enrollmentEmpClientBranchId == null) {
|
||||
return;
|
||||
}
|
||||
final response = await apiService.getGmcSiTopUpToApi(
|
||||
enrollmentClient_id!,
|
||||
enrollmentEmpCodeString!,
|
||||
'GMC-OPD',
|
||||
enrollmentEmpClientBranchId!);
|
||||
if (response['status'] == 'success') {
|
||||
if (response.containsKey('data')) {
|
||||
setState(() {
|
||||
opdPolicies = response['data'];
|
||||
});
|
||||
if (opdPolicies.isNotEmpty) {
|
||||
int _toInt(dynamic value) {
|
||||
if (value is int) return value;
|
||||
if (value is double) return value.toInt();
|
||||
return int.tryParse(value?.toString() ?? '0') ?? 0;
|
||||
}
|
||||
|
||||
final opdNode = opdPolicies['gmc_opd'];
|
||||
opdClientPolicyId = opdNode['client_policy_id'];
|
||||
opdSlabRates = opdNode['SlabRates'];
|
||||
opdFloaterTextHeading = opdNode['floter_text_heading'];
|
||||
opdPolicyName = opdNode['policy_name'];
|
||||
opdPolicyType = opdNode['type'];
|
||||
opdFamilyFloater = opdNode['policy_terms']['family_floater'];
|
||||
opdMappedFamilyFloatersSi =
|
||||
opdNode['family_floaters_of_only_si_array'];
|
||||
opdECardDownload = opdNode['eCardDownload'];
|
||||
opdOpenForEnrollment = _toInt(opdNode['OpenForEnrollment']);
|
||||
opdTypeName = opdNode['type'];
|
||||
opdSiValue = opdNode['family_floaters_of_only_si_value'];
|
||||
logDebug(opdSiValue);
|
||||
|
||||
if (opdSiValue == 0) {
|
||||
showHideOpdCard = 0;
|
||||
} else {
|
||||
showHideOpdCard = 1;
|
||||
}
|
||||
opdSiSelectedSI = opdSiValue != 0 ? opdSiValue.toString() : null;
|
||||
|
||||
// // OPD can have SI value 0 initially; still show card when policy exists.
|
||||
// showHideOpdCard = (opdPolicyName != null ||
|
||||
// opdPolicyType != null ||
|
||||
// (opdMappedFamilyFloatersSi is List &&
|
||||
// opdMappedFamilyFloatersSi.isNotEmpty))
|
||||
// ? 1
|
||||
// : 0;
|
||||
|
||||
setState(() {
|
||||
opdSiPremiumValue =
|
||||
_toInt(opdNode['family_floaters_of_only_si_premium_value']);
|
||||
opdSiPremiumGst =
|
||||
_toInt(opdNode['family_floaters_of_only_si_gst_value']);
|
||||
opdSiTotalAmt = opdSiPremiumValue + opdSiPremiumGst;
|
||||
opdIsPremiumSummary =
|
||||
isPremiumEnabled(opdNode?['is_premium_summery']);
|
||||
});
|
||||
|
||||
opdDisclaimer = opdNode['disclaimer'];
|
||||
|
||||
if (_isOpenForEnrollment(opdOpenForEnrollment) &&
|
||||
activeOPDSiData == 1) {
|
||||
if (opdDisclaimer != null && opdDisclaimer.isNotEmpty) {
|
||||
// Temporary list to hold new entries
|
||||
List<Map<String, dynamic>> newEntries = [];
|
||||
|
||||
if (opdDisclaimer is String) {
|
||||
// Wrap string in a map with id and checked
|
||||
newEntries.add({
|
||||
'id': allDisclaimer.length + 1, // Unique ID based on length
|
||||
'text': opdDisclaimer,
|
||||
'checked': false,
|
||||
'type': opdTypeName
|
||||
});
|
||||
} else if (opdDisclaimer is List) {
|
||||
// Convert each string in the list to a map with id and checked
|
||||
newEntries.addAll(
|
||||
List<Map<String, dynamic>>.from(
|
||||
opdDisclaimer.map((text) => {
|
||||
'id': allDisclaimer.length + newEntries.length + 1,
|
||||
'text': text,
|
||||
'checked': false,
|
||||
'type': opdTypeName
|
||||
}),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
logDebug('Unexpected type for disclaimer');
|
||||
}
|
||||
|
||||
// Add new entries to the main list, ensuring no duplicate texts
|
||||
newEntries.forEach((entry) {
|
||||
if (!allDisclaimer
|
||||
.any((item) => item['text'] == entry['text'])) {
|
||||
allDisclaimer.add(entry);
|
||||
}
|
||||
});
|
||||
|
||||
// Sort disclaimers after adding
|
||||
_sortDisclaimerByTypeOrder();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logDebug('No data found in the response');
|
||||
}
|
||||
} else {
|
||||
logDebug('API request failed with status: ${response['status']}');
|
||||
}
|
||||
} else {
|
||||
logDebug('Request failed with status: ${response['code']}');
|
||||
}
|
||||
} catch (e) {
|
||||
logDebug('Exception occurred: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> getGmcSiParentTopUp() async {
|
||||
try {
|
||||
if (enrollmentClient_id == null ||
|
||||
@ -786,7 +952,7 @@ class _empReviewDetailsState extends State<empReviewDetails> {
|
||||
await topUpSiParentPolicies['gmc_si_parent_topup']
|
||||
['disclaimer'];
|
||||
|
||||
if (topUpParentOpenForEnrollment == '1' &&
|
||||
if (_isOpenForEnrollment(topUpParentOpenForEnrollment) &&
|
||||
activeSiParentData == 1) {
|
||||
if (topUpParentDisclaimer != null &&
|
||||
topUpParentDisclaimer.isNotEmpty) {
|
||||
@ -936,7 +1102,7 @@ setState(() {
|
||||
logDebug(
|
||||
'addOnsDependentOpenForEnrollment $addOnsDependentOpenForEnrollment');
|
||||
logDebug('activeDependentData $activeDependentData');
|
||||
if (addOnsDependentOpenForEnrollment == '1' &&
|
||||
if (_isOpenForEnrollment(addOnsDependentOpenForEnrollment) &&
|
||||
activeDependentData == 1) {
|
||||
logDebug('addOnsDependentOpenForEnrollment');
|
||||
if (addOnsDependentDisclaimer != null &&
|
||||
@ -1025,6 +1191,18 @@ setState(() {
|
||||
}
|
||||
}
|
||||
|
||||
if (opdMappedFamilyFloatersSi != null) {
|
||||
dynamic getSiTrueObjects = opdMappedFamilyFloatersSi
|
||||
.where((element) => element['is_value_exist'] == true)
|
||||
.toList();
|
||||
|
||||
logDebug('getSiTrueObjects : $getSiTrueObjects');
|
||||
|
||||
if (getSiTrueObjects.isNotEmpty) {
|
||||
iAgreeForAddOn.add(int.parse(opdClientPolicyId));
|
||||
}
|
||||
}
|
||||
|
||||
if (topUpParentMappedFamilyFloatersSi != null) {
|
||||
dynamic getSiTrueObjects = topUpParentMappedFamilyFloatersSi
|
||||
.where((element) => element['is_value_exist'] == true)
|
||||
@ -1172,7 +1350,7 @@ setState(() {
|
||||
color: Colors.red,
|
||||
),
|
||||
),
|
||||
content: Text('Something went wrong. Enrollemnt not completed'),
|
||||
content: Text('Unable to process. Please try again later. Enrollemnt not completed'),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
child: Text(
|
||||
@ -1251,6 +1429,7 @@ setState(() {
|
||||
double total = 0.0; // Initialize to avoid unwanted accumulation
|
||||
|
||||
if (topUpSiTotalAmt != null) total += topUpSiTotalAmt!;
|
||||
if (opdSiTotalAmt != null) total += opdSiTotalAmt!;
|
||||
if (topUpParentSiTotalAmt != null) total += topUpParentSiTotalAmt!;
|
||||
if (addOnsDependentTotalAmt != null) total += addOnsDependentTotalAmt!;
|
||||
if (gpaTotalAmt != null) total += gpaTotalAmt!;
|
||||
@ -1633,7 +1812,7 @@ setState(() {
|
||||
gmchasPremiumSummary ||
|
||||
topUpIsPremiumSummary ||
|
||||
topUpParentIsPremiumSummary ||
|
||||
addOnsDependentIsPremiumSummary)...[
|
||||
addOnsDependentIsPremiumSummary || opdIsPremiumSummary)...[
|
||||
Card(
|
||||
elevation: 0,
|
||||
color: Colors.white,
|
||||
@ -1716,7 +1895,7 @@ setState(() {
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'₹${allTotalSum()}/Year',
|
||||
'₹${formatAmount(allTotalSum())}/Year',
|
||||
textAlign: TextAlign.left,
|
||||
style:
|
||||
GoogleFonts.poppins(
|
||||
@ -1936,7 +2115,7 @@ setState(() {
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'₹ ${addOnsDependentSumInsured != null ? addOnsDependentSumInsured : 'NA'}',
|
||||
'₹ ${formatNullableAmount(addOnsDependentSumInsured)}',
|
||||
textAlign:
|
||||
TextAlign.right,
|
||||
style: GoogleFonts
|
||||
@ -2193,7 +2372,7 @@ setState(() {
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'₹ ${topUpSumInsured != null ? topUpSumInsured : 'NA'}',
|
||||
'₹ ${formatNullableAmount(topUpSumInsured)}',
|
||||
textAlign:
|
||||
TextAlign.right,
|
||||
style: GoogleFonts
|
||||
@ -2271,6 +2450,187 @@ setState(() {
|
||||
),
|
||||
)),
|
||||
SizedBox(height: showHideTopUpCard == 1 ? 16 : 0),
|
||||
if (showHideOpdCard == 1)
|
||||
Card(
|
||||
elevation: 0,
|
||||
color: Colors.white,
|
||||
child: Padding(
|
||||
padding: Responsive.isDesktop(context)
|
||||
? EdgeInsets.all(30)
|
||||
: EdgeInsets.all(10),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xFFFFF1DD),
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
),
|
||||
padding: Responsive.isDesktop(context)
|
||||
? EdgeInsets.only(
|
||||
top: 15,
|
||||
bottom: 15,
|
||||
left: 25,
|
||||
right: 25)
|
||||
: EdgeInsets.only(
|
||||
top: 10,
|
||||
bottom: 10,
|
||||
left: 10,
|
||||
right: 10),
|
||||
child: Row(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: Responsive.isDesktop(context)
|
||||
? 9
|
||||
: 7,
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
Responsive.isDesktop(
|
||||
context)
|
||||
? opdPolicyName
|
||||
: opdPolicyType,
|
||||
textAlign: TextAlign.left,
|
||||
style:
|
||||
GoogleFonts.poppins(
|
||||
fontSize: Responsive
|
||||
.isDesktop(
|
||||
context)
|
||||
? 20
|
||||
: 14,
|
||||
fontWeight:
|
||||
FontWeight.w600,
|
||||
),
|
||||
),
|
||||
if (opdECardDownload != null)
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
_launchURL(
|
||||
opdECardDownload);
|
||||
},
|
||||
child: MouseRegion(
|
||||
cursor:
|
||||
SystemMouseCursors
|
||||
.click,
|
||||
child: Icon(
|
||||
Icons.file_download,
|
||||
color: Color(
|
||||
0xFFE26728),
|
||||
size: 25,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: Responsive.isDesktop(
|
||||
context)
|
||||
? 3
|
||||
: 5,
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
opdFloaterTextHeading ?? '',
|
||||
textAlign: TextAlign.right,
|
||||
style:
|
||||
GoogleFonts.poppins(
|
||||
fontSize: Responsive
|
||||
.isDesktop(
|
||||
context)
|
||||
? 18
|
||||
: 13,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'₹ ${formatNullableAmount(opdSiValue)}',
|
||||
textAlign: TextAlign.right,
|
||||
style:
|
||||
GoogleFonts.poppins(
|
||||
fontSize: Responsive
|
||||
.isDesktop(
|
||||
context)
|
||||
? 20
|
||||
: 14,
|
||||
fontWeight:
|
||||
FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 15),
|
||||
Container(
|
||||
padding: Responsive.isDesktop(context)
|
||||
? EdgeInsets.only(
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 15,
|
||||
right: 15)
|
||||
: EdgeInsets.only(
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 5,
|
||||
right: 5),
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children:
|
||||
(opdMappedFamilyFloatersSi ??
|
||||
[])
|
||||
.where((item) =>
|
||||
item['is_value_exist'] ==
|
||||
true)
|
||||
.map<Widget>((item) {
|
||||
Map<String, dynamic> data =
|
||||
item['data'];
|
||||
String formattedDate =
|
||||
data['dob'] ?? '';
|
||||
return Row(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(Icons.arrow_right,
|
||||
color:
|
||||
Color(0xFFE26728)),
|
||||
SizedBox(
|
||||
width: Responsive.isDesktop(
|
||||
context)
|
||||
? 10
|
||||
: 5),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'${data['name']} ~ ${data['relationship']} ~ DOB : $formattedDate',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: Responsive
|
||||
.isDesktop(
|
||||
context)
|
||||
? 18
|
||||
: 14,
|
||||
color: Color(0xFF232526),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)),
|
||||
SizedBox(height: showHideOpdCard == 1 ? 16 : 0),
|
||||
if (showHideTopUpParentCard == 1)
|
||||
Card(
|
||||
elevation: 0,
|
||||
@ -2460,7 +2820,7 @@ setState(() {
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'₹ ${topUpParentSiValue != null ? topUpParentSiValue : 'NA'}',
|
||||
'₹ ${formatNullableAmount(topUpParentSiValue)}',
|
||||
textAlign:
|
||||
TextAlign.right,
|
||||
style: GoogleFonts
|
||||
@ -2552,7 +2912,7 @@ setState(() {
|
||||
gmchasPremiumSummary ||
|
||||
topUpIsPremiumSummary ||
|
||||
topUpParentIsPremiumSummary ||
|
||||
addOnsDependentIsPremiumSummary)...[
|
||||
addOnsDependentIsPremiumSummary || opdIsPremiumSummary)...[
|
||||
Container(
|
||||
child: Row(
|
||||
crossAxisAlignment:
|
||||
@ -2804,6 +3164,102 @@ setState(() {
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
if (activeOPDSiData == 1 &&
|
||||
opdIsPremiumSummary)
|
||||
TableRow(
|
||||
children: [
|
||||
TableCell(
|
||||
child: Padding(
|
||||
padding:
|
||||
EdgeInsets
|
||||
.all(8.0),
|
||||
child: Text(
|
||||
(Responsive.isDesktop(
|
||||
context)
|
||||
? (opdPolicyName ??
|
||||
'Default OPD Name')
|
||||
: 'GMC - OPD'),
|
||||
textAlign:
|
||||
TextAlign
|
||||
.start,
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize:
|
||||
Responsive.isDesktop(context)
|
||||
? 14
|
||||
: 12),
|
||||
),
|
||||
),
|
||||
),
|
||||
TableCell(
|
||||
child: Padding(
|
||||
padding:
|
||||
EdgeInsets
|
||||
.all(8.0),
|
||||
child: Text(
|
||||
'₹$opdSiPremiumValue/Year',
|
||||
textAlign: Responsive
|
||||
.isDesktop(
|
||||
context)
|
||||
? TextAlign
|
||||
.start
|
||||
: TextAlign
|
||||
.start,
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize:
|
||||
Responsive.isDesktop(context)
|
||||
? 14
|
||||
: 12),
|
||||
),
|
||||
),
|
||||
),
|
||||
TableCell(
|
||||
child: Padding(
|
||||
padding:
|
||||
EdgeInsets
|
||||
.all(8.0),
|
||||
child: Text(
|
||||
'₹$opdSiPremiumGst/-',
|
||||
textAlign: Responsive
|
||||
.isDesktop(
|
||||
context)
|
||||
? TextAlign
|
||||
.start
|
||||
: TextAlign
|
||||
.start,
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize:
|
||||
Responsive.isDesktop(context)
|
||||
? 14
|
||||
: 12),
|
||||
),
|
||||
),
|
||||
),
|
||||
TableCell(
|
||||
child: Padding(
|
||||
padding:
|
||||
EdgeInsets
|
||||
.all(8.0),
|
||||
child: Text(
|
||||
'₹$opdSiTotalAmt/Year',
|
||||
textAlign: Responsive
|
||||
.isDesktop(
|
||||
context)
|
||||
? TextAlign
|
||||
.start
|
||||
: TextAlign
|
||||
.start,
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize:
|
||||
Responsive.isDesktop(context)
|
||||
? 14
|
||||
: 12),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
if (activeSiParentData ==
|
||||
1 &&
|
||||
topUpParentIsPremiumSummary)
|
||||
@ -3051,7 +3507,7 @@ setState(() {
|
||||
.end,
|
||||
children: [
|
||||
Text(
|
||||
'₹${allTotalSum()}/Year',
|
||||
'₹${formatAmount(allTotalSum())}/Year',
|
||||
textAlign:
|
||||
TextAlign.right,
|
||||
style: GoogleFonts
|
||||
@ -3198,17 +3654,12 @@ setState(() {
|
||||
child: ElevatedButton(
|
||||
onPressed: () async {
|
||||
// Remove siData, siParentData, and dependentData from local storage
|
||||
final SharedPreferences
|
||||
prefs =
|
||||
await SharedPreferences
|
||||
.getInstance();
|
||||
final SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
prefs.remove('siData');
|
||||
prefs
|
||||
.remove('siParentData');
|
||||
prefs.remove(
|
||||
'dependentData');
|
||||
context
|
||||
.go('/addOnsDetails');
|
||||
prefs.remove('siParentData');
|
||||
prefs.remove('dependentData');
|
||||
prefs.remove('opdData');
|
||||
context.go('/addOnsDetails');
|
||||
// Navigator.pushNamed(
|
||||
// context, 'addOnsDetails');
|
||||
},
|
||||
@ -3569,7 +4020,7 @@ setState(() {
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'₹ ${gpaSumInsured != null ? gpaSumInsured : 'NA'}',
|
||||
'₹ ${formatNullableAmount(gpaSumInsured)}',
|
||||
textAlign: TextAlign.right,
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize:
|
||||
@ -3766,7 +4217,7 @@ setState(() {
|
||||
gmchasPremiumSummary ||
|
||||
topUpIsPremiumSummary ||
|
||||
topUpParentIsPremiumSummary ||
|
||||
addOnsDependentIsPremiumSummary)
|
||||
addOnsDependentIsPremiumSummary || opdIsPremiumSummary)
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@ -3829,6 +4280,15 @@ setState(() {
|
||||
: value.toStringAsFixed(2);
|
||||
}
|
||||
|
||||
String formatNullableAmount(dynamic value, {String fallback = 'NA'}) {
|
||||
if (value == null) return fallback;
|
||||
final numericValue = value is num
|
||||
? value.toDouble()
|
||||
: double.tryParse(value.toString());
|
||||
if (numericValue == null) return value.toString();
|
||||
return formatAmount(numericValue);
|
||||
}
|
||||
|
||||
List<Widget> generateGmcCards(List<dynamic> data) {
|
||||
List<Widget> cards = [];
|
||||
// gmcEnrollmentStatus = isEnrollmentOpen(data);
|
||||
@ -3996,7 +4456,7 @@ setState(() {
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'₹ ${gmcSumInsured != null ? gmcSumInsured : 'NA'}',
|
||||
'₹ ${formatNullableAmount(gmcSumInsured)}',
|
||||
textAlign: TextAlign.right,
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize:
|
||||
@ -4368,7 +4828,7 @@ setState(() {
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
'₹${(policy["si_premium_value"] ?? 0) + (policy["si_gst_value"] ?? 0)}/-',
|
||||
'₹${(policy["si_premium_value"] ?? 0) + (policy["si_gst_value"] ?? 0)}/Year',
|
||||
textAlign: TextAlign.start,
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: Responsive.isDesktop(context) ? 14 : 12),
|
||||
@ -4444,7 +4904,7 @@ setState(() {
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
'₹${(policy["family_floaters_of_dependent_and_si_premium_value"] ?? 0) + (policy["family_floaters_of_dependent_and_gst_value"] ?? 0)}/-',
|
||||
'₹${(policy["family_floaters_of_dependent_and_si_premium_value"] ?? 0) + (policy["family_floaters_of_dependent_and_gst_value"] ?? 0)}/Year',
|
||||
textAlign: TextAlign.start,
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: Responsive.isDesktop(context) ? 14 : 12),
|
||||
|
||||
7
lib/pages/helpers/browser_href.dart
Normal file
7
lib/pages/helpers/browser_href.dart
Normal file
@ -0,0 +1,7 @@
|
||||
import 'browser_href_stub.dart'
|
||||
if (dart.library.html) 'browser_href_web.dart' as impl;
|
||||
|
||||
String currentBrowserHref() => impl.currentBrowserHref();
|
||||
|
||||
void stripPayloadParamFromBrowserUrl() =>
|
||||
impl.stripPayloadParamFromBrowserUrl();
|
||||
3
lib/pages/helpers/browser_href_stub.dart
Normal file
3
lib/pages/helpers/browser_href_stub.dart
Normal file
@ -0,0 +1,3 @@
|
||||
String currentBrowserHref() => '';
|
||||
|
||||
void stripPayloadParamFromBrowserUrl() {}
|
||||
22
lib/pages/helpers/browser_href_web.dart
Normal file
22
lib/pages/helpers/browser_href_web.dart
Normal file
@ -0,0 +1,22 @@
|
||||
import 'dart:html' as html;
|
||||
|
||||
String currentBrowserHref() => html.window.location.href;
|
||||
|
||||
/// Removes `payload` from the query string so a failed SSO return does not
|
||||
/// re-trigger handling when [GoRouter] rebuilds `/login`.
|
||||
void stripPayloadParamFromBrowserUrl() {
|
||||
final u = Uri.parse(html.window.location.href);
|
||||
if (!u.queryParameters.containsKey('payload')) return;
|
||||
final m = Map<String, String>.from(u.queryParameters);
|
||||
m.remove('payload');
|
||||
final clean = Uri(
|
||||
scheme: u.scheme,
|
||||
userInfo: u.userInfo.isEmpty ? null : u.userInfo,
|
||||
host: u.host.isEmpty ? null : u.host,
|
||||
port: u.hasPort ? u.port : null,
|
||||
path: u.path,
|
||||
queryParameters: m.isEmpty ? null : m,
|
||||
fragment: u.fragment,
|
||||
);
|
||||
html.window.history.replaceState(null, '', clean.toString());
|
||||
}
|
||||
4
lib/pages/helpers/sso_same_tab_redirect.dart
Normal file
4
lib/pages/helpers/sso_same_tab_redirect.dart
Normal file
@ -0,0 +1,4 @@
|
||||
import 'sso_same_tab_redirect_stub.dart'
|
||||
if (dart.library.html) 'sso_same_tab_redirect_web.dart' as impl;
|
||||
|
||||
void ssoRedirectSameTab(String url) => impl.ssoRedirectSameTab(url);
|
||||
2
lib/pages/helpers/sso_same_tab_redirect_stub.dart
Normal file
2
lib/pages/helpers/sso_same_tab_redirect_stub.dart
Normal file
@ -0,0 +1,2 @@
|
||||
/// Non-web: caller should not invoke same-tab redirect.
|
||||
void ssoRedirectSameTab(String url) {}
|
||||
6
lib/pages/helpers/sso_same_tab_redirect_web.dart
Normal file
6
lib/pages/helpers/sso_same_tab_redirect_web.dart
Normal file
@ -0,0 +1,6 @@
|
||||
import 'dart:html' as html;
|
||||
|
||||
/// Full-page navigation so Microsoft IdP and SAML complete in the same tab.
|
||||
void ssoRedirectSameTab(String url) {
|
||||
html.window.location.assign(url);
|
||||
}
|
||||
2444
lib/pages/login.dart
2444
lib/pages/login.dart
File diff suppressed because it is too large
Load Diff
274
lib/pages/login_saml_auth.dart
Normal file
274
lib/pages/login_saml_auth.dart
Normal file
@ -0,0 +1,274 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:jwt_decode/jwt_decode.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../config/environment.dart';
|
||||
import '../customAppBar/toastHelper.dart';
|
||||
import '../logger.dart';
|
||||
import 'service/SessionManager.dart';
|
||||
import 'service/TokenService.dart';
|
||||
|
||||
/// Where the IdP should return the browser after SAML (web).
|
||||
///
|
||||
/// Uses **`/login`** (not `/sso-login`) so static hosts that do not rewrite
|
||||
/// unknown paths to `index.html` still return **200** and Flutter can read
|
||||
/// `?payload=`. `/sso-login` is still supported via `web/index.html` redirect.
|
||||
String buildWebSsoRelayStateUrl() {
|
||||
if (!kIsWeb) return '';
|
||||
final origin = Uri.base.origin;
|
||||
final frag = Uri.base.fragment;
|
||||
// Hash routing: e.g. `http://host/#/login` → return with payload on `#/login`
|
||||
if (frag.startsWith('/')) {
|
||||
return '$origin/#/login';
|
||||
}
|
||||
final href = Environment.baseHref;
|
||||
if (href.isEmpty || href == '/') {
|
||||
return '$origin/login';
|
||||
}
|
||||
final trimmed =
|
||||
href.endsWith('/') ? href.substring(0, href.length - 1) : href;
|
||||
return '$origin$trimmed/login';
|
||||
}
|
||||
|
||||
/// Decodes the `payload` query value (base64 / base64url) into a JSON map.
|
||||
Map<String, dynamic> decodeSsoPayloadQueryValue(String encoded) {
|
||||
var normalized = encoded.trim().replaceAll('-', '+').replaceAll('_', '/');
|
||||
final mod = normalized.length % 4;
|
||||
if (mod > 0) {
|
||||
normalized = normalized.padRight(normalized.length + (4 - mod), '=');
|
||||
}
|
||||
final jsonStr = utf8.decode(base64.decode(normalized));
|
||||
final decoded = json.decode(jsonStr);
|
||||
if (decoded is Map<String, dynamic>) return decoded;
|
||||
if (decoded is Map) {
|
||||
return Map<String, dynamic>.from(decoded);
|
||||
}
|
||||
throw const FormatException('SSO payload is not a JSON object');
|
||||
}
|
||||
|
||||
String? readPayloadFromRouterUri(Uri uri) {
|
||||
final direct = uri.queryParameters['payload'];
|
||||
if (direct != null && direct.isNotEmpty) return direct;
|
||||
final frag = uri.fragment;
|
||||
if (frag.contains('?')) {
|
||||
final queryPart = frag.split('?').last;
|
||||
return Uri.splitQueryString(queryPart)['payload'];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Locates the raw `payload` query value on a SAML return URL, e.g.
|
||||
/// `https://staging.nhanceindia.in/sso-login?payload=eyJ...` or
|
||||
/// `https://host/#/sso-login?payload=eyJ...`.
|
||||
String? readSsoPayloadEncodedFromReturnUrl(String url) {
|
||||
final trimmed = url.trim();
|
||||
if (trimmed.isEmpty) return null;
|
||||
|
||||
final uri = Uri.tryParse(trimmed);
|
||||
if (uri != null) {
|
||||
final fromUri = readPayloadFromRouterUri(uri);
|
||||
if (fromUri != null && fromUri.isNotEmpty) {
|
||||
return fromUri;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: stop before `#` so `...?payload=eyJ...#/login` does not swallow the hash.
|
||||
final ampOrQ = RegExp(r'(?:\?|&)payload=([^&#]+)');
|
||||
final match = ampOrQ.firstMatch(trimmed);
|
||||
if (match != null) {
|
||||
final raw = match.group(1);
|
||||
if (raw != null && raw.isNotEmpty) {
|
||||
return Uri.decodeQueryComponent(raw);
|
||||
}
|
||||
}
|
||||
|
||||
final hashIdx = trimmed.indexOf('#');
|
||||
if (hashIdx != -1) {
|
||||
final afterHash = trimmed.substring(hashIdx + 1);
|
||||
if (afterHash.contains('payload=')) {
|
||||
final queryPart =
|
||||
afterHash.contains('?') ? afterHash.split('?').last : afterHash;
|
||||
final fromFrag = Uri.splitQueryString(queryPart)['payload'];
|
||||
if (fromFrag != null && fromFrag.isNotEmpty) {
|
||||
return fromFrag;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Map<String, dynamic>? decodeSsoPayloadFromUrl(String url) {
|
||||
try {
|
||||
final raw = readSsoPayloadEncodedFromReturnUrl(url);
|
||||
if (raw == null || raw.isEmpty) return null;
|
||||
return decodeSsoPayloadQueryValue(raw);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// JWT claim maps for pre-enrollment (`data`) and post-enrollment tokens when
|
||||
/// those fields hold a JWT string (typically after successful SAML).
|
||||
class SsoPayloadJwtDecode {
|
||||
final Map<String, dynamic>? preTokenClaims;
|
||||
final Map<String, dynamic>? postTokenClaims;
|
||||
|
||||
const SsoPayloadJwtDecode({
|
||||
this.preTokenClaims,
|
||||
this.postTokenClaims,
|
||||
});
|
||||
}
|
||||
|
||||
SsoPayloadJwtDecode decodeJwtClaimsFromSsoPayloadMap(
|
||||
Map<String, dynamic> data) {
|
||||
Map<String, dynamic>? preClaims;
|
||||
Map<String, dynamic>? postClaims;
|
||||
|
||||
final pre = data['data'];
|
||||
if (pre is String && pre.contains('.')) {
|
||||
try {
|
||||
preClaims = Jwt.parseJwt(pre);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
final rawPe = data['post_enrollment'];
|
||||
if (rawPe is Map) {
|
||||
final pt = rawPe['data'];
|
||||
if (pt is String && pt.contains('.')) {
|
||||
try {
|
||||
postClaims = Jwt.parseJwt(pt);
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
return SsoPayloadJwtDecode(
|
||||
preTokenClaims: preClaims,
|
||||
postTokenClaims: postClaims,
|
||||
);
|
||||
}
|
||||
|
||||
String prettySsoJsonForConsole(Object? value) {
|
||||
if (value == null) return 'null';
|
||||
try {
|
||||
return const JsonEncoder.withIndent(' ').convert(value);
|
||||
} catch (_) {
|
||||
return value.toString();
|
||||
}
|
||||
}
|
||||
|
||||
/// Writes JWT claim maps to stdout / the browser DevTools console. Unlike
|
||||
/// [logDebug], this is not filtered by [LoggerConfig.minLevel].
|
||||
void printSsoDecodedTokensToConsole(SsoPayloadJwtDecode jwt) {
|
||||
print(
|
||||
'[SSO] decodedToken (pre / data field):\n${prettySsoJsonForConsole(jwt.preTokenClaims)}');
|
||||
print(
|
||||
'[SSO] decodedToken (post_enrollment.data):\n${prettySsoJsonForConsole(jwt.postTokenClaims)}');
|
||||
}
|
||||
|
||||
Future<void> _persistClientBranding() async {
|
||||
final session = SessionManager();
|
||||
final pre = await TokenService.getPreToken();
|
||||
if (pre == null || pre.isEmpty) return;
|
||||
|
||||
final url = Uri.parse(Environment.apiUrlEnrollment +
|
||||
'getClientDetails?post_client_id=${session.client_id}&post_branch_id=${session.empClientBranchId}&pre_client_id=${session.enrollmentClient_id}&pre_branch_id=${session.enrollmentEmpClientBranchId}');
|
||||
|
||||
try {
|
||||
final response = await http.get(
|
||||
url,
|
||||
headers: {
|
||||
'Authorization': 'Bearer $pre',
|
||||
'APP-SIGNATURE':
|
||||
'nhance-2025-signature-T8f6gV9k5fK2wf5V5c4vId3b0J8z6cAm2x8Y',
|
||||
},
|
||||
);
|
||||
if (response.statusCode != 200) return;
|
||||
final data = json.decode(response.body) as Map<String, dynamic>;
|
||||
if (!data.containsKey('data')) return;
|
||||
final clientDetails = data['data'];
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(
|
||||
'clientLogo', clientDetails['client']['client_logo'] as String? ?? '');
|
||||
await prefs.setString(
|
||||
'clientName', clientDetails['client']['client_name'] as String? ?? '');
|
||||
await prefs.setString('addon_subheading',
|
||||
clientDetails['client']['addon_subheading'] as String? ?? '');
|
||||
} catch (e) {
|
||||
logDebug('SSO client branding fetch failed: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Completes login after SAML callback payload has been decoded to a map.
|
||||
Future<void> completeLoginFromSsoPayloadMap(
|
||||
BuildContext context, Map<String, dynamic> data) async {
|
||||
logDebug('SSO payload map: $data');
|
||||
print('[SSO] payload map (decoded from URL):\n${prettySsoJsonForConsole(data)}');
|
||||
final jwt = decodeJwtClaimsFromSsoPayloadMap(data);
|
||||
printSsoDecodedTokensToConsole(jwt);
|
||||
if (jwt.preTokenClaims != null) {
|
||||
logDebug('SSO decodedToken (pre / data): ${jwt.preTokenClaims}');
|
||||
}
|
||||
if (jwt.postTokenClaims != null) {
|
||||
logDebug('SSO decodedToken (post_enrollment / data): ${jwt.postTokenClaims}');
|
||||
}
|
||||
|
||||
if (data['status']?.toString() != 'success') {
|
||||
final msg = data['message']?.toString() ?? 'SSO sign-in was not successful';
|
||||
if (context.mounted) ToastHelper.showErrorToast(context, msg);
|
||||
if (context.mounted) context.go('/login');
|
||||
return;
|
||||
}
|
||||
|
||||
String? preToken;
|
||||
if (data['data'] is String) {
|
||||
preToken = data['data'] as String;
|
||||
}
|
||||
String? postToken;
|
||||
Map<String, dynamic>? postEnrollment;
|
||||
final rawPe = data['post_enrollment'];
|
||||
if (rawPe is Map) {
|
||||
postEnrollment = Map<String, dynamic>.from(rawPe);
|
||||
}
|
||||
if (postEnrollment != null && postEnrollment['data'] is String) {
|
||||
postToken = postEnrollment['data'] as String;
|
||||
}
|
||||
|
||||
await TokenService.saveTokens(preToken: preToken, postToken: postToken);
|
||||
|
||||
if (postEnrollment != null &&
|
||||
postEnrollment['status']?.toString() == 'success' &&
|
||||
postToken != null &&
|
||||
postToken.isNotEmpty) {
|
||||
await SessionManager().initializeFromPostToken(postToken);
|
||||
}
|
||||
if (data['status']?.toString() == 'success' &&
|
||||
preToken != null &&
|
||||
preToken.isNotEmpty) {
|
||||
await SessionManager().initializeFromPreToken(preToken);
|
||||
}
|
||||
|
||||
await _persistClientBranding();
|
||||
|
||||
if (!context.mounted) return;
|
||||
|
||||
if (postToken != null && postToken.isNotEmpty) {
|
||||
ToastHelper.showSuccessToast(context, 'Successfully Logged In');
|
||||
SessionManager().unlockMobileAppSession();
|
||||
context.go('/home');
|
||||
} else {
|
||||
SessionManager().unlockMobileAppSession();
|
||||
context.go('/empDetails');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> completeLoginFromSsoPayloadString(
|
||||
BuildContext context, String encodedPayload) async {
|
||||
final data = decodeSsoPayloadQueryValue(encodedPayload);
|
||||
await completeLoginFromSsoPayloadMap(context, data);
|
||||
}
|
||||
86
lib/pages/login_saml_webview_screen.dart
Normal file
86
lib/pages/login_saml_webview_screen.dart
Normal file
@ -0,0 +1,86 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
|
||||
import 'login_saml_auth.dart';
|
||||
|
||||
/// Mobile: in-app WebView for SAML entry; when URL contains `payload=`, decodes and pops.
|
||||
class LoginSamlWebViewScreen extends StatefulWidget {
|
||||
final String initialUrl;
|
||||
|
||||
const LoginSamlWebViewScreen({super.key, required this.initialUrl});
|
||||
|
||||
@override
|
||||
State<LoginSamlWebViewScreen> createState() => _LoginSamlWebViewScreenState();
|
||||
}
|
||||
|
||||
class _LoginSamlWebViewScreenState extends State<LoginSamlWebViewScreen> {
|
||||
late final WebViewController _controller;
|
||||
bool _finished = false;
|
||||
|
||||
void _tryFinishWithUrl(String url) {
|
||||
if (_finished || !mounted) return;
|
||||
if (!url.contains('payload=')) return;
|
||||
final decoded = decodeSsoPayloadFromUrl(url);
|
||||
if (decoded != null) {
|
||||
_finished = true;
|
||||
Navigator.of(context).pop(decoded);
|
||||
}
|
||||
}
|
||||
|
||||
void _tryFinishFromHttpError(HttpResponseError error) {
|
||||
if (_finished || !mounted) return;
|
||||
final status = error.response?.statusCode;
|
||||
if (status == null) return;
|
||||
if (status < 400) return;
|
||||
final reqUri = error.request?.uri;
|
||||
final resUri = error.response?.uri;
|
||||
if (reqUri != null) {
|
||||
_tryFinishWithUrl(reqUri.toString());
|
||||
}
|
||||
if (!_finished && resUri != null) {
|
||||
_tryFinishWithUrl(resUri.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = WebViewController()
|
||||
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
||||
..setNavigationDelegate(
|
||||
NavigationDelegate(
|
||||
onNavigationRequest: (NavigationRequest request) {
|
||||
if (!_finished && request.url.contains('payload=')) {
|
||||
_tryFinishWithUrl(request.url);
|
||||
if (_finished) return NavigationDecision.prevent;
|
||||
}
|
||||
return NavigationDecision.navigate;
|
||||
},
|
||||
onPageStarted: (String url) => _tryFinishWithUrl(url),
|
||||
onPageFinished: (String url) => _tryFinishWithUrl(url),
|
||||
onUrlChange: (UrlChange change) {
|
||||
final url = change.url;
|
||||
if (url != null && url.isNotEmpty) {
|
||||
_tryFinishWithUrl(url);
|
||||
}
|
||||
},
|
||||
onHttpError: _tryFinishFromHttpError,
|
||||
onWebResourceError: (WebResourceError error) {
|
||||
final url = error.url;
|
||||
if (url != null && url.isNotEmpty) {
|
||||
_tryFinishWithUrl(url);
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
..loadRequest(Uri.parse(widget.initialUrl));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Sign in with SSO')),
|
||||
body: WebViewWidget(controller: _controller),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1043,7 +1043,7 @@ class _claimsState extends State<claims> {
|
||||
Text(
|
||||
isRetail ? insurerShortName : policyName,
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: Responsive.isDesktop(context) ? 18 : 14,
|
||||
fontSize: Responsive.isDesktop(context) ? 18 : 11,
|
||||
fontWeight: FontWeight.w600),
|
||||
),
|
||||
],
|
||||
@ -1065,7 +1065,7 @@ class _claimsState extends State<claims> {
|
||||
Text(
|
||||
isRetail ? policyType : policyStatus,
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: Responsive.isDesktop(context) ? 18 : 14,
|
||||
fontSize: Responsive.isDesktop(context) ? 18 : 11,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: isRetail
|
||||
? Colors.black
|
||||
@ -1093,7 +1093,7 @@ class _claimsState extends State<claims> {
|
||||
Text(
|
||||
isRetail ? vehicleNo : "₹ $siValue",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: Responsive.isDesktop(context) ? 18 : 14,
|
||||
fontSize: Responsive.isDesktop(context) ? 18 : 11,
|
||||
fontWeight: FontWeight.w600),
|
||||
),
|
||||
],
|
||||
|
||||
@ -222,7 +222,7 @@ class _helpState extends State<help> {
|
||||
// trackClaimsClosedList = [];
|
||||
// });
|
||||
// } else {
|
||||
// ToastHelper.showErrorToast(context, 'Something went wrong');
|
||||
// ToastHelper.showErrorToast(context, 'Unable to process. Please try again later');
|
||||
// logDebug('API request failed with status: ${response['status']}');
|
||||
// }
|
||||
}
|
||||
@ -266,7 +266,7 @@ class _helpState extends State<help> {
|
||||
// setState(() {
|
||||
// isLoading = false;
|
||||
// });
|
||||
// ToastHelper.showErrorToast(context, 'Something went wrong');
|
||||
// ToastHelper.showErrorToast(context, 'Unable to process. Please try again later');
|
||||
// logDebug('API request failed with status: ${response['status']}');
|
||||
// }
|
||||
} catch (e) {
|
||||
|
||||
@ -236,6 +236,7 @@ class _planclaimsformState extends State<planclaimsform> {
|
||||
return {
|
||||
'id': int.parse(memberList['id']),
|
||||
'name': memberList['name'],
|
||||
'relationship': memberList['relationship'],
|
||||
};
|
||||
}).toList();
|
||||
logDebug('employeePolicyList : $employeePolicyList');
|
||||
@ -566,6 +567,7 @@ class _planclaimsformState extends State<planclaimsform> {
|
||||
return {
|
||||
'id': int.parse(memberList['id']),
|
||||
'name': memberList['name'],
|
||||
'relationship': memberList['relationship'],
|
||||
};
|
||||
}).toList();
|
||||
logDebug('employeePolicyList : $employeePolicyList');
|
||||
@ -753,59 +755,59 @@ class _planclaimsformState extends State<planclaimsform> {
|
||||
}
|
||||
|
||||
// ✅ NEW VALIDATION: At least one PDF must be uploaded
|
||||
// bool hasPdf = uploadedFiles.any((uf) {
|
||||
// final ext = uf.file.extension?.toLowerCase() ?? '';
|
||||
// return ext == 'pdf';
|
||||
// });
|
||||
//
|
||||
// if (!hasPdf) {
|
||||
// ToastHelper.showErrorToast(context, 'Please upload at least one PDF document');
|
||||
// setState(() => isLoading = false);
|
||||
// return;
|
||||
// }
|
||||
bool hasPdf = uploadedFiles.any((uf) {
|
||||
final ext = uf.file.extension?.toLowerCase() ?? '';
|
||||
return ext == 'pdf';
|
||||
});
|
||||
|
||||
if (!hasPdf) {
|
||||
ToastHelper.showErrorToast(context, 'Please upload at least one PDF document');
|
||||
setState(() => isLoading = false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Add files
|
||||
// for (var uf in uploadedFiles) {
|
||||
// final pf = uf.file;
|
||||
// if (pf.bytes != null) {
|
||||
// request.files.add(http.MultipartFile.fromBytes(
|
||||
// 'claim_docs[]',
|
||||
// pf.bytes!,
|
||||
// filename: pf.name,
|
||||
// ));
|
||||
// }
|
||||
// }
|
||||
|
||||
// Add files — convert images to PDF if needed
|
||||
for (var uf in uploadedFiles) {
|
||||
final pf = uf.file;
|
||||
final ext = pf.extension?.toLowerCase() ?? '';
|
||||
|
||||
if (pf.bytes != null) {
|
||||
Uint8List fileBytes = pf.bytes!;
|
||||
|
||||
if (['jpg', 'jpeg', 'png', 'heic'].contains(ext)) {
|
||||
// ✅ Convert image → PDF
|
||||
final pdf = pw.Document();
|
||||
final image = pw.MemoryImage(fileBytes);
|
||||
pdf.addPage(pw.Page(
|
||||
build: (pw.Context context) =>
|
||||
pw.Center(child: pw.Image(image, fit: pw.BoxFit.contain)),
|
||||
));
|
||||
fileBytes = await pdf.save();
|
||||
final pdfFileName = pf.name.replaceAll(RegExp(r'\.\w+$'), '.pdf');
|
||||
|
||||
logDebug('📄 Converted image ${pf.name} → PDF ($pdfFileName)');
|
||||
request.files.add(http.MultipartFile.fromBytes(
|
||||
'claim_docs[]', fileBytes, filename: pdfFileName));
|
||||
} else {
|
||||
// ✅ Already a PDF
|
||||
request.files.add(http.MultipartFile.fromBytes(
|
||||
'claim_docs[]', fileBytes, filename: pf.name));
|
||||
}
|
||||
request.files.add(http.MultipartFile.fromBytes(
|
||||
'claim_docs[]',
|
||||
pf.bytes!,
|
||||
filename: pf.name,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Add files — convert images to PDF if needed
|
||||
// for (var uf in uploadedFiles) {
|
||||
// final pf = uf.file;
|
||||
// final ext = pf.extension?.toLowerCase() ?? '';
|
||||
|
||||
// if (pf.bytes != null) {
|
||||
// Uint8List fileBytes = pf.bytes!;
|
||||
|
||||
// if (['jpg', 'jpeg', 'png', 'heic'].contains(ext)) {
|
||||
// // ✅ Convert image → PDF
|
||||
// final pdf = pw.Document();
|
||||
// final image = pw.MemoryImage(fileBytes);
|
||||
// pdf.addPage(pw.Page(
|
||||
// build: (pw.Context context) =>
|
||||
// pw.Center(child: pw.Image(image, fit: pw.BoxFit.contain)),
|
||||
// ));
|
||||
// fileBytes = await pdf.save();
|
||||
// final pdfFileName = pf.name.replaceAll(RegExp(r'\.\w+$'), '.pdf');
|
||||
|
||||
// logDebug('📄 Converted image ${pf.name} → PDF ($pdfFileName)');
|
||||
// request.files.add(http.MultipartFile.fromBytes(
|
||||
// 'claim_docs[]', fileBytes, filename: pdfFileName));
|
||||
// } else {
|
||||
// // ✅ Already a PDF
|
||||
// request.files.add(http.MultipartFile.fromBytes(
|
||||
// 'claim_docs[]', fileBytes, filename: pf.name));
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
// ✅ Combine all names into a JSON array string
|
||||
final claimDocNames = uploadedFiles.map((uf) => uf.label.trim()).toList();
|
||||
|
||||
@ -141,7 +141,7 @@ class _raisedTicketHistoryState extends State<raisedTicketHistory> {
|
||||
// setState(() {
|
||||
// isLoading = false;
|
||||
// });
|
||||
// ToastHelper.showErrorToast(context, 'Something went wrong');
|
||||
// ToastHelper.showErrorToast(context, 'Unable to process. Please try again later');
|
||||
// logDebug('API request failed with status: ${response['status']}');
|
||||
// }
|
||||
} catch (e) {
|
||||
|
||||
@ -193,7 +193,7 @@ class _retailClaimFormsState extends State<retailClaimForm> {
|
||||
}
|
||||
} catch (e) {
|
||||
logDebug("Retail claim submit ERROR: $e");
|
||||
ToastHelper.showErrorToast(context, "Something went wrong");
|
||||
ToastHelper.showErrorToast(context, "Unable to process. Please try again later");
|
||||
}
|
||||
|
||||
setState(() => isLoading = false);
|
||||
|
||||
@ -212,7 +212,7 @@ class _tickettracklistState extends State<tickettracklist> {
|
||||
// setState(() {
|
||||
// isLoading = false;
|
||||
// });
|
||||
// ToastHelper.showErrorToast(context, 'Something went wrong');
|
||||
// ToastHelper.showErrorToast(context, 'Unable to process. Please try again later');
|
||||
// logDebug('API request failed with status: ${response['status']}');
|
||||
// }
|
||||
} catch (e) {
|
||||
|
||||
@ -156,6 +156,22 @@ class SessionManager {
|
||||
|
||||
bool isReady = false;
|
||||
|
||||
/// Mobile only: false after a full app restart until login / MPIN succeeds.
|
||||
/// Stays true while the process stays alive (e.g. user only minimized the app).
|
||||
/// Web always behaves as unlocked ([isMobileAppSessionUnlocked] is true).
|
||||
bool _mobileAppSessionUnlocked = false;
|
||||
|
||||
bool get isMobileAppSessionUnlocked =>
|
||||
kIsWeb ? true : _mobileAppSessionUnlocked;
|
||||
|
||||
void unlockMobileAppSession() {
|
||||
if (!kIsWeb) _mobileAppSessionUnlocked = true;
|
||||
}
|
||||
|
||||
void lockMobileAppSession() {
|
||||
if (!kIsWeb) _mobileAppSessionUnlocked = false;
|
||||
}
|
||||
|
||||
// ===== Post Enrollment Fields =====
|
||||
String? mobileNo;
|
||||
String? empClientBranchId;
|
||||
@ -279,5 +295,6 @@ class SessionManager {
|
||||
await prefs.clear();
|
||||
await DataManager().clearData();
|
||||
await TokenService.clearTokens();
|
||||
lockMobileAppSession();
|
||||
}
|
||||
}
|
||||
|
||||
@ -120,12 +120,12 @@ import 'package:nhance_app_pwa/logger.dart';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logDebug('checkLoginPin Something went wrong');
|
||||
ToastHelper.showErrorToast(context, 'Something went wrong');
|
||||
logDebug('checkLoginPin Unable to process. Please try again later');
|
||||
ToastHelper.showErrorToast(context, 'Unable to process. Please try again later');
|
||||
throw Exception('Failed to verify pin number');
|
||||
}
|
||||
} catch (e) {
|
||||
ToastHelper.showErrorToast(context, 'Something went wrong');
|
||||
ToastHelper.showErrorToast(context, 'Unable to process. Please try again later');
|
||||
logDebug('Error: $e');
|
||||
}
|
||||
}
|
||||
@ -233,7 +233,7 @@ import 'package:nhance_app_pwa/logger.dart';
|
||||
throw Exception('Failed to load data');
|
||||
}
|
||||
} catch (e) {
|
||||
ToastHelper.showErrorToast(context, 'Something went wrong');
|
||||
ToastHelper.showErrorToast(context, 'Unable to process. Please try again later');
|
||||
context.go('/login');
|
||||
logDebug('Error: $e');
|
||||
}
|
||||
@ -309,6 +309,7 @@ import 'package:nhance_app_pwa/logger.dart';
|
||||
logDebug('Token12345: ${await TokenService.getPostToken()}');
|
||||
logDebug('Token00000');
|
||||
// if (context.mounted) {
|
||||
SessionManager().unlockMobileAppSession();
|
||||
context.go('/home');
|
||||
// }
|
||||
// Navigator.pushReplacementNamed(context, 'home');
|
||||
@ -353,6 +354,7 @@ import 'package:nhance_app_pwa/logger.dart';
|
||||
// enrollmentEmp_status == 'active') {
|
||||
// Navigator.pushReplacementNamed(context, 'home');
|
||||
// } else {
|
||||
SessionManager().unlockMobileAppSession();
|
||||
context.go('/empDetails');
|
||||
// Navigator.pushReplacementNamed(context, 'empDetails');
|
||||
// }
|
||||
@ -370,7 +372,7 @@ import 'package:nhance_app_pwa/logger.dart';
|
||||
try {
|
||||
await enterPinApi(_pinController.text);
|
||||
} catch (e) {
|
||||
ToastHelper.showErrorToast(context, 'Something went wrong');
|
||||
ToastHelper.showErrorToast(context, 'Unable to process. Please try again later');
|
||||
logDebug('Error: $e');
|
||||
} finally {
|
||||
if (mounted) {
|
||||
@ -469,7 +471,7 @@ import 'package:nhance_app_pwa/logger.dart';
|
||||
throw Exception('Failed to load data');
|
||||
}
|
||||
} catch (e) {
|
||||
// ToastHelper.showErrorToast(context, 'Something went wrong');
|
||||
// ToastHelper.showErrorToast(context, 'Unable to process. Please try again later');
|
||||
logDebug('Error: $e');
|
||||
} finally {
|
||||
if (mounted) {
|
||||
|
||||
@ -135,7 +135,7 @@ class _changePinState extends State<changePin> {
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
ToastHelper.showErrorToast(context, 'Something went wrong');
|
||||
ToastHelper.showErrorToast(context, 'Unable to process. Please try again later');
|
||||
logDebug('Error: $e');
|
||||
} finally {
|
||||
if (mounted) {
|
||||
|
||||
@ -143,9 +143,11 @@ class _pinSettingPageState extends State<pinSettingPage> {
|
||||
await _authService.authenticateWithBiometrics();
|
||||
if (biometricEnabled) {
|
||||
if (_postToken != null && _postToken.isNotEmpty) {
|
||||
SessionManager().unlockMobileAppSession();
|
||||
context.go('/home');
|
||||
// Navigator.pushReplacementNamed(context, 'home');
|
||||
} else {
|
||||
SessionManager().unlockMobileAppSession();
|
||||
context.go('/empDetails');
|
||||
// Navigator.pushReplacementNamed(context, 'empDetails');
|
||||
}
|
||||
@ -158,9 +160,11 @@ class _pinSettingPageState extends State<pinSettingPage> {
|
||||
}
|
||||
} else {
|
||||
if (_postToken != null && _postToken.isNotEmpty) {
|
||||
SessionManager().unlockMobileAppSession();
|
||||
context.go('/home');
|
||||
// Navigator.pushReplacementNamed(context, 'home');
|
||||
} else {
|
||||
SessionManager().unlockMobileAppSession();
|
||||
context.go('/empDetails');
|
||||
// Navigator.pushReplacementNamed(context, 'empDetails');
|
||||
}
|
||||
@ -194,7 +198,7 @@ class _pinSettingPageState extends State<pinSettingPage> {
|
||||
}
|
||||
// }
|
||||
} catch (e) {
|
||||
ToastHelper.showErrorToast(context, 'Something went wrong');
|
||||
ToastHelper.showErrorToast(context, 'Unable to process. Please try again later');
|
||||
print('Error: $e');
|
||||
} finally {
|
||||
if (mounted) {
|
||||
@ -237,9 +241,11 @@ class _pinSettingPageState extends State<pinSettingPage> {
|
||||
|
||||
await _authService.saveSkipStatus(0);
|
||||
if (_postToken != null && _postToken.isNotEmpty) {
|
||||
SessionManager().unlockMobileAppSession();
|
||||
context.go('/home');
|
||||
// Navigator.pushReplacementNamed(context, 'home');
|
||||
} else {
|
||||
SessionManager().unlockMobileAppSession();
|
||||
context.go('/empDetails');
|
||||
// Navigator.pushReplacementNamed(context, 'empDetails');
|
||||
}
|
||||
|
||||
@ -163,7 +163,7 @@ class _setPasswordState extends State<setPassword> {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
// ToastHelper.showErrorToast(context, 'Something went wrong');
|
||||
// ToastHelper.showErrorToast(context, 'Unable to process. Please try again later');
|
||||
logDebug('Error: $e');
|
||||
}
|
||||
|
||||
|
||||
62
lib/pages/sso_login_return_page.dart
Normal file
62
lib/pages/sso_login_return_page.dart
Normal file
@ -0,0 +1,62 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../customAppBar/toastHelper.dart';
|
||||
import '../logger.dart';
|
||||
import 'helpers/browser_href.dart';
|
||||
import 'login_saml_auth.dart';
|
||||
|
||||
/// Handles the browser return URL after SAML (query or hash `payload=`).
|
||||
class SsoLoginReturnPage extends StatefulWidget {
|
||||
const SsoLoginReturnPage({super.key});
|
||||
|
||||
@override
|
||||
State<SsoLoginReturnPage> createState() => _SsoLoginReturnPageState();
|
||||
}
|
||||
|
||||
class _SsoLoginReturnPageState extends State<SsoLoginReturnPage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _consumePayload());
|
||||
}
|
||||
|
||||
Future<void> _consumePayload() async {
|
||||
if (!mounted) return;
|
||||
|
||||
final routerUri = GoRouterState.of(context).uri;
|
||||
final href = kIsWeb ? currentBrowserHref() : routerUri.toString();
|
||||
|
||||
Map<String, dynamic>? decoded;
|
||||
for (final candidate in <String>[
|
||||
href,
|
||||
if (kIsWeb) Uri.base.toString(),
|
||||
routerUri.toString(),
|
||||
]) {
|
||||
if (candidate.isEmpty) continue;
|
||||
decoded = decodeSsoPayloadFromUrl(candidate);
|
||||
if (decoded != null) break;
|
||||
}
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
if (decoded == null) {
|
||||
logDebug('SSO return: no payload in href=$href router=${routerUri.toString()}');
|
||||
ToastHelper.showWarningToast(
|
||||
context, 'SSO return missing or invalid payload');
|
||||
context.go('/login');
|
||||
return;
|
||||
}
|
||||
|
||||
logDebug('SSO return: decoded payload (keys)=${decoded.keys.toList()}');
|
||||
await completeLoginFromSsoPayloadMap(context, decoded);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Scaffold(
|
||||
body: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -313,7 +313,7 @@ import 'package:nhance_app_pwa/logger.dart';
|
||||
// });
|
||||
// final SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
// prefs.clear();
|
||||
// ToastHelper.showWarningToast(context, 'Something went wrong');
|
||||
// ToastHelper.showWarningToast(context, 'Unable to process. Please try again later');
|
||||
// throw Exception('Failed to verify OTP');
|
||||
// }
|
||||
// } catch (e) {
|
||||
@ -323,7 +323,7 @@ import 'package:nhance_app_pwa/logger.dart';
|
||||
// logDebug('Error: $e');
|
||||
// final SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
// prefs.clear();
|
||||
// ToastHelper.showWarningToast(context, 'Something went wrong');
|
||||
// ToastHelper.showWarningToast(context, 'Unable to process. Please try again later');
|
||||
// // Show a Snackbar if there's an error while verifying OTP
|
||||
// logDebug('Failed to verify OTP. Please try again.');
|
||||
// }
|
||||
@ -674,12 +674,12 @@ import 'package:nhance_app_pwa/logger.dart';
|
||||
// // Navigator.pushReplacementNamed(context, 'pinSettingPage');
|
||||
// }
|
||||
// } else {
|
||||
// logDebug('checkLoginPin Something went wrong');
|
||||
// ToastHelper.showErrorToast(context, 'Something went wrong');
|
||||
// logDebug('checkLoginPin Unable to process. Please try again later');
|
||||
// ToastHelper.showErrorToast(context, 'Unable to process. Please try again later');
|
||||
// throw Exception('Failed to verify pin number');
|
||||
// }
|
||||
// } catch (e) {
|
||||
// ToastHelper.showErrorToast(context, 'Something went wrong');
|
||||
// ToastHelper.showErrorToast(context, 'Unable to process. Please try again later');
|
||||
// logDebug('Error: $e');
|
||||
// }
|
||||
// }
|
||||
|
||||
@ -17,8 +17,8 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||
# In Windows, build-name is used as the major, minor, and patch parts
|
||||
# of the product and file versions while build-number is used as the build suffix.
|
||||
#version: 1.2.41+98
|
||||
version: 1.0.32+38
|
||||
#version: 2.0.20+58
|
||||
version: 1.0.35+41
|
||||
#version: 2.0.23+61
|
||||
|
||||
environment:
|
||||
sdk: '>=3.3.3 <4.0.0'
|
||||
@ -101,6 +101,7 @@ flutter:
|
||||
# To add assets to your application, add an assets section, like this:
|
||||
assets:
|
||||
- assets/
|
||||
- assets/images/login/
|
||||
# - images/a_dot_burr.jpeg
|
||||
# - images/a_dot_ham.jpeg
|
||||
|
||||
|
||||
@ -4,6 +4,22 @@
|
||||
|
||||
<base href="$FLUTTER_BASE_HREF">
|
||||
|
||||
<!-- If the IdP redirects to /sso-login?payload=…, many CDNs return 404 (no SPA
|
||||
fallback). Send the browser to /login?payload=… so index.html loads. -->
|
||||
<script>
|
||||
(function () {
|
||||
try {
|
||||
var path = window.location.pathname || '';
|
||||
var search = window.location.search || '';
|
||||
if (!search || search.indexOf('payload=') === -1) return;
|
||||
if (!/\/sso-login\/?$/.test(path)) return;
|
||||
var origin = window.location.origin;
|
||||
var prefix = path.replace(/\/sso-login\/?$/, '') || '';
|
||||
window.location.replace(origin + prefix + '/login' + search);
|
||||
} catch (e) {}
|
||||
})();
|
||||
</script>
|
||||
|
||||
<meta charset="UTF-8">
|
||||
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
||||
<meta name="description" content="Nhance Benefits">
|
||||
@ -47,6 +63,9 @@
|
||||
<script src="flutter.js" defer></script>
|
||||
</head>
|
||||
<body style="overflow:hidden">
|
||||
<script>
|
||||
try { console.log('[SSO] page URL (before Flutter):', window.location.href); } catch (e) {}
|
||||
</script>
|
||||
<div id="loading_indicator" class="container overlay">
|
||||
<img class="indicator" src="assets/nhance-loader.gif" alt="">
|
||||
</div>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user