saml flow

This commit is contained in:
Surendiran 2026-05-19 12:30:28 +05:30
parent 750a3becad
commit 1288a67256
34 changed files with 4281 additions and 1179 deletions

View File

@ -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()

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 B

View File

@ -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) {

View File

@ -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);
}

View File

@ -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,
);
}
}
}

View File

@ -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');
}

View File

@ -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

View File

@ -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 ?? {},

View File

@ -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),

View 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();

View File

@ -0,0 +1,3 @@
String currentBrowserHref() => '';
void stripPayloadParamFromBrowserUrl() {}

View 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());
}

View 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);

View File

@ -0,0 +1,2 @@
/// Non-web: caller should not invoke same-tab redirect.
void ssoRedirectSameTab(String url) {}

View 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);
}

File diff suppressed because it is too large Load Diff

View 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);
}

View 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),
);
}
}

View File

@ -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),
),
],

View File

@ -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) {

View File

@ -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();

View File

@ -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) {

View File

@ -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);

View File

@ -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) {

View File

@ -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();
}
}

View File

@ -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) {

View File

@ -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) {

View File

@ -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');
}

View File

@ -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');
}

View 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()),
);
}
}

View File

@ -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');
// }
// }

View File

@ -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

View File

@ -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>