Deeplink implementation

haroon_dev
haroon amjad 2 days ago
parent aafe7ac96b
commit 9a50e94839

@ -168,7 +168,7 @@
android:enabled="true"
android:exported="true"
android:hardwareAccelerated="true"
android:launchMode="singleTop"
android:launchMode="singleTask"
android:showOnLockScreen="true"
android:theme="@style/LaunchTheme"
android:windowSoftInputMode="adjustResize"
@ -205,6 +205,21 @@
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<meta-data
android:name="flutter_deeplinking_enabled"
android:value="true" />
<!-- Android App Links for hmgapps.com -->
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
<data android:scheme="http" />
<data android:host="hmgapps.com" />
<data android:host="www.hmgapps.com" />
</intent-filter>
</activity> <!-- <receiver android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationBootReceiver" android:exported="true"> -->
<!-- <intent-filter> -->
<!-- <action android:name="android.intent.action.BOOT_COMPLETED"/> -->

@ -30,6 +30,8 @@
<true/>
<key>FIVKSupportVideo</key>
<true/>
<key>FlutterDeepLinkingEnabled</key>
<true/>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>comgooglemaps</string>

@ -21,5 +21,10 @@
<array>
<string>TAG</string>
</array>
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:hmgapps.com</string>
<string>applinks:www.hmgapps.com</string>
</array>
</dict>
</plist>

@ -6,7 +6,7 @@ import 'package:hmg_patient_app_new/core/enums.dart';
class ApiConsts {
static const maxSmallScreen = 660;
static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.uat;
static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.prod;
// static String baseUrl = 'https://uat.hmgwebservices.com/'; // HIS API URL UAT

@ -51,6 +51,9 @@ class AppState {
bool isMyFatoorahEnabled = false;
// bool isMyFatoorahEnabled = true;
String? deepLinkJoinSessionId;
bool isSplashInitializationDone = false;
void setIsPaytabsEnabled(bool value) {
isPaytabsEnabled = false;
// isPaytabsEnabled = true;

@ -11,6 +11,7 @@ import 'package:flutter/services.dart';
import 'package:hmg_patient_app_new/core/api/api_client.dart';
import 'package:hmg_patient_app_new/core/api_consts.dart';
import 'package:hmg_patient_app_new/core/app_state.dart';
import 'package:hmg_patient_app_new/core/cache_consts.dart';
import 'package:hmg_patient_app_new/core/dependencies.dart';
import 'package:hmg_patient_app_new/core/enums.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart';
@ -55,11 +56,14 @@ import 'package:hmg_patient_app_new/features/todo_section/todo_section_view_mode
import 'package:hmg_patient_app_new/features/water_monitor/water_monitor_view_model.dart';
import 'package:hmg_patient_app_new/features/weather/weather_view_model.dart';
import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/health_calculator_view_model.dart';
import 'package:hmg_patient_app_new/presentation/home/navigation_screen.dart';
import 'package:hmg_patient_app_new/presentation/tele_consultation/zoom/call_screen.dart';
import 'package:hmg_patient_app_new/routes/app_routes.dart';
import 'package:hmg_patient_app_new/services/app_lifecycle_service.dart';
import 'package:hmg_patient_app_new/services/logger_service.dart';
import 'package:hmg_patient_app_new/services/navigation_service.dart';
import 'package:hmg_patient_app_new/services/security_service.dart';
import 'package:hmg_patient_app_new/splashPage.dart';
import 'package:hmg_patient_app_new/theme/app_theme.dart';
import 'package:hmg_patient_app_new/unsafe_device.dart';
import 'package:hmg_patient_app_new/widgets/date_range_selector/viewmodel/date_range_calendar_model.dart';
@ -142,13 +146,13 @@ Future<void> callInitializations() async {
await callAppStateInitializations();
// Initialize Security Service early to catch threats before app logic runs
if (kReleaseMode) {
try {
await getIt.get<SecurityService>().initialize();
} catch (e) {
getIt.get<LoggerService>().logError('SecurityService.initialize failed, continuing app startup: $e');
}
}
// if (kReleaseMode) {
// try {
// await getIt.get<SecurityService>().initialize();
// } catch (e) {
// getIt.get<LoggerService>().logError('SecurityService.initialize failed, continuing app startup: $e');
// }
// }
// Set up critical threat callback to navigate to unsafe device page
getIt.get<SecurityService>().setOnCriticalThreatCallback(() {
@ -386,6 +390,16 @@ class MyApp extends StatelessWidget {
}
}
String? _extractJoinSessionId(String? routeName) {
if (routeName == null || routeName.isEmpty) return null;
// Matches both "https://hmgapps.com/join/{id}" and "/join/{id}"
final match = RegExp(r'/join/([^?#]+)').firstMatch(routeName);
if (match != null && match.group(1)!.isNotEmpty) {
return Uri.decodeComponent(match.group(1)!);
}
return null;
}
@override
Widget build(BuildContext context) {
return SafeArea(
@ -422,6 +436,51 @@ class MyApp extends StatelessWidget {
locale: context.locale,
initialRoute: AppRoutes.initialRoute,
routes: AppRoutes.routes,
onGenerateInitialRoutes: (String initialRouteName) {
final defaultRoute = WidgetsBinding.instance.platformDispatcher.defaultRouteName;
final sessionId = _extractJoinSessionId(initialRouteName) ?? _extractJoinSessionId(defaultRoute);
if (sessionId != null) {
getIt.get<AppState>().deepLinkJoinSessionId = sessionId;
Utils.saveStringFromPrefs(CacheConst.zoomRoomID, sessionId);
debugPrint('DeepLink (Cold Start) Join Session ID: $sessionId');
}
return [
MaterialPageRoute(builder: (_) => const SplashPage()),
];
},
onGenerateRoute: (RouteSettings settings) {
final sessionId = _extractJoinSessionId(settings.name);
if (sessionId != null) {
final appState = getIt.get<AppState>();
Utils.saveStringFromPrefs(CacheConst.zoomRoomID, sessionId);
debugPrint('DeepLink (Route) Join Session ID: $sessionId');
// If SplashPage is still initializing, save it so SplashPage navigates when ready
if (!appState.isSplashInitializationDone) {
appState.deepLinkJoinSessionId = sessionId;
return MaterialPageRoute(builder: (_) => const SplashPage());
}
// Warm Start: App is already initialized, open CallScreen directly
return MaterialPageRoute(
settings: RouteSettings(
name: AppRoutes.zoomCallPage,
arguments: CallArguments(sessionId, "123", "Patient", "40", "0", true, 1),
),
builder: (_) => const CallScreen(),
);
}
// Fallback for normal named routes if any reach onGenerateRoute
final builder = AppRoutes.routes[settings.name];
if (builder != null) {
return MaterialPageRoute(builder: builder, settings: settings);
}
return null;
},
onUnknownRoute: (RouteSettings settings) {
// Prevents crash when opened via root link https://hmgapps.com
return MaterialPageRoute(
builder: (_) => getIt.get<AppState>().isSplashInitializationDone ? LandingNavigation() : const SplashPage(),
);
},
theme: AppTheme.getTheme(isArabic),
darkTheme: AppTheme.getDarkTheme(isArabic),
themeMode: profileVm.isDarkMode ? ThemeMode.dark : ThemeMode.light,

@ -115,6 +115,7 @@ class AppRoutes {
static const String refundRequestDetailScreen = '/refundRequestDetailScreen';
static Map<String, WidgetBuilder> get routes => {
'/': (context) => SplashPage(),
initialRoute: (context) => SplashPage(),
loginScreen: (context) => LoginScreen(),
landingScreen: (context) => LandingNavigation(),

@ -1,5 +1,12 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_state.dart';
import 'package:hmg_patient_app_new/core/cache_consts.dart';
import 'package:hmg_patient_app_new/core/dependencies.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/presentation/tele_consultation/zoom/call_screen.dart';
import 'package:hmg_patient_app_new/routes/app_routes.dart';
import 'package:hmg_patient_app_new/services/navigation_service.dart';
/// Service to monitor and track app lifecycle state changes
/// This helps detect when app goes to background/foreground
@ -60,7 +67,52 @@ class AppLifecycleService with WidgetsBindingObserver {
_stateController.add(state);
}
String? _extractJoinSessionId(String? routeName) {
if (routeName == null || routeName.isEmpty) return null;
final match = RegExp(r'/join/([^?#]+)').firstMatch(routeName);
if (match != null && match.group(1)!.isNotEmpty) {
return Uri.decodeComponent(match.group(1)!);
}
return null;
}
Future<bool> _handleIncomingDeepLink(String? route) async {
debugPrint('🔗 DeepLink received in AppLifecycleService: $route');
final sessionId = _extractJoinSessionId(route);
if (sessionId != null) {
final appState = getIt.get<AppState>();
await Utils.saveStringFromPrefs(CacheConst.zoomRoomID, sessionId);
debugPrint('🔗 DeepLink Join Session ID parsed: $sessionId (splashDone=${appState.isSplashInitializationDone})');
if (!appState.isSplashInitializationDone) {
appState.deepLinkJoinSessionId = sessionId;
return true;
}
final navState = getIt.get<NavigationService>().navigatorKey.currentState;
if (navState != null) {
String callTypeID = await Utils.getStringFromPrefs(CacheConst.callTypeID);
navState.pushNamed(
AppRoutes.zoomCallPage,
arguments: CallArguments(sessionId, "123", "Patient", "40", "0", true, int.tryParse(callTypeID) ?? 1),
);
return true;
}
}
return false;
}
@override
Future<bool> didPushRouteInformation(RouteInformation routeInformation) {
return _handleIncomingDeepLink(routeInformation.uri.toString());
}
@override
Future<bool> didPushRoute(String route) {
return _handleIncomingDeepLink(route);
}
/// Initialize the lifecycle observer
void initialize() {
WidgetsBinding.instance.addObserver(this);

@ -90,8 +90,13 @@ class _SplashScreenState extends State<SplashPage> {
_initializeClarity();
}
getIt.get<AppState>().isSplashInitializationDone = true;
if (isAppOpenedFromCall) {
navigateToTeleConsult();
} else if (getIt.get<AppState>().deepLinkJoinSessionId != null) {
getIt.get<AppState>().deepLinkJoinSessionId = null;
navigateToTeleConsult();
} else {
if (await Utils.getBoolFromPrefs(CacheConst.firstLaunch)) {
// Navigator.of(context).pushReplacement(FadePage(page: SplashAnimationScreen(routeWidget: OnboardingScreen())));
@ -166,13 +171,13 @@ class _SplashScreenState extends State<SplashPage> {
// GetIt.instance<CacheService>().remove(key: CacheConst.isAppOpenedFromCall);
Utils.removeFromPrefs(CacheConst.isAppOpenedFromCall);
Navigator.of(GetIt.instance<NavigationService>().navigatorKey.currentContext!).pushReplacement(FadePage(page: SplashAnimationScreen(routeWidget: LandingNavigation())));
Navigator.pushReplacementNamed(
Navigator.of(GetIt.instance<NavigationService>().navigatorKey.currentContext!).pushReplacement(FadePage(page: LandingNavigation()));
Navigator.pushNamed(
// context,
GetIt.instance<NavigationService>().navigatorKey.currentContext!,
AppRoutes.zoomCallPage,
// arguments: CallArguments(appointmentID, "111", "Patient", "40", "1", true, 1),
arguments: CallArguments(roomID, "123", "Patient", "40", "0", true, int.parse(callTypeID)),
arguments: CallArguments(roomID, "123", "Patient", "40", "0", true, int.tryParse(callTypeID) ?? 1),
// arguments: CallArguments("SmallDailyStandup9875", "123", "Patient", "40", "0", false, int.parse(widget.incomingCallData!.appointmentNo!)),
);
}

Loading…
Cancel
Save