295 lines
9.9 KiB
Dart
Executable File
295 lines
9.9 KiB
Dart
Executable File
import 'dart:async';
|
||
import 'dart:io';
|
||
// import 'package:firebase_core/firebase_core.dart';
|
||
// import 'package:firebase_messaging/firebase_messaging.dart';
|
||
import 'package:appsflyer_sdk/appsflyer_sdk.dart';
|
||
import 'package:firebase_core/firebase_core.dart';
|
||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||
import 'package:flutter/foundation.dart';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||
import 'package:go_router/go_router.dart';
|
||
import 'package:hive_flutter/hive_flutter.dart';
|
||
import 'package:app_links/app_links.dart';
|
||
import 'package:uae_stat/config/connectivity_provider.dart'
|
||
as legacy_connectivity;
|
||
import 'package:uae_stat/config/injector.dart';
|
||
import 'package:uae_stat/lib/core/routing/app_router.dart';
|
||
import 'package:uae_stat/lib/core/theme/app_theme.dart';
|
||
import 'package:uae_stat/presentation/components/indicators/locale_provider.dart';
|
||
import 'package:uae_stat/l10n/app_localizations.dart';
|
||
|
||
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
|
||
await Firebase.initializeApp();
|
||
}
|
||
|
||
void main() async {
|
||
WidgetsFlutterBinding.ensureInitialized();
|
||
await Hive.initFlutter();
|
||
if (!Hive.isBoxOpen('indicator_cache')) {
|
||
await Hive.openBox<String>('indicator_cache');
|
||
}
|
||
if (kIsWeb) {
|
||
await Firebase.initializeApp(
|
||
options: const FirebaseOptions(
|
||
apiKey: 'AIzaSyB-QQbnq366f7rrKLVVFJKnSZgR9LcDWFI',
|
||
appId: '1:534109861443:android:25d267f0c986b2042f46f6',
|
||
messagingSenderId: '534109861443',
|
||
projectId: 'fcsc-uae-stats-9cb1e',
|
||
),
|
||
);
|
||
} else {
|
||
await Firebase.initializeApp();
|
||
}
|
||
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
|
||
await FirebaseMessaging.instance.requestPermission(
|
||
alert: true,
|
||
badge: true,
|
||
sound: true,
|
||
);
|
||
FirebaseMessaging.onMessage.listen((RemoteMessage _) {});
|
||
|
||
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage _) {});
|
||
|
||
// final themeProvider = ThemeProvider();
|
||
// themeProvider.setUserId("USER_ID_FROM_LOGIN"); // set this properly
|
||
// await themeProvider.loadThemeFromPocketBase();
|
||
|
||
//
|
||
// Request permission for notifications
|
||
// NotificationSettings settings = await messaging.requestPermission(
|
||
// alert: true,
|
||
// badge: true,
|
||
// sound: true,
|
||
// );
|
||
//
|
||
configureDependencies();
|
||
runApp(
|
||
const ProviderScope(
|
||
child: MainApp(),
|
||
),
|
||
);
|
||
}
|
||
|
||
// Future<void> triggerLocalNetworkAccess() async {
|
||
// try {
|
||
// await Socket.connect('127.0.0.1', 9).timeout(const Duration(seconds: 1));
|
||
// } catch (e) {
|
||
// // Ignore errors – this is just to trigger the local network prompt
|
||
// }
|
||
// }
|
||
|
||
class MainApp extends ConsumerStatefulWidget {
|
||
const MainApp({super.key});
|
||
|
||
@override
|
||
ConsumerState<MainApp> createState() => _MainAppState();
|
||
}
|
||
|
||
class _MainAppState extends ConsumerState<MainApp> {
|
||
late AppsflyerSdk appsflyerSdk;
|
||
bool _listenerAttached = false;
|
||
String? pendingDeepLink;
|
||
Map<String, dynamic>? pendingDeepLinkData;
|
||
StreamSubscription<Uri>? _appLinkSubscription;
|
||
Timer? _oneLinkFallbackTimer;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
|
||
_initAppsFlyer();
|
||
_initUaePassDeepLinks();
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
_appLinkSubscription?.cancel();
|
||
_oneLinkFallbackTimer?.cancel();
|
||
super.dispose();
|
||
}
|
||
|
||
/// Handle deep links from AppsFlyer / custom URL schemes (no auth).
|
||
void _initUaePassDeepLinks() {
|
||
void handleIncomingLink(Uri? uri) {
|
||
if (uri == null) return;
|
||
// AppsFlyer OneLink fallback: fcscappstates://callback?deep_link_value=...
|
||
if (uri.scheme == 'fcscappstates') {
|
||
final deepLinkValue = uri.queryParameters['deep_link_value'];
|
||
if (deepLinkValue != null && deepLinkValue.isNotEmpty) {
|
||
_oneLinkFallbackTimer?.cancel();
|
||
final path =
|
||
deepLinkValue.startsWith('/') ? deepLinkValue : '/$deepLinkValue';
|
||
appRouter.go('/?intendedPath=${Uri.encodeComponent(path)}');
|
||
} else {
|
||
// Wait briefly for AppsFlyer SDK callback, then fallback to app root.
|
||
_oneLinkFallbackTimer?.cancel();
|
||
_oneLinkFallbackTimer = Timer(const Duration(milliseconds: 1500), () {
|
||
if (mounted) appRouter.go('/');
|
||
});
|
||
}
|
||
return;
|
||
}
|
||
// AppsFlyer OneLink universal link can arrive directly as https://fcscstats.onelink.me/...
|
||
// Handle it as a fallback in case SDK callback is delayed/missed in internal builds.
|
||
if (uri.scheme == 'https' && uri.host == 'fcscstats.onelink.me') {
|
||
final deepLinkValue = uri.queryParameters['deep_link_value'];
|
||
if (deepLinkValue != null && deepLinkValue.isNotEmpty) {
|
||
_oneLinkFallbackTimer?.cancel();
|
||
final path =
|
||
deepLinkValue.startsWith('/') ? deepLinkValue : '/$deepLinkValue';
|
||
appRouter.go('/?intendedPath=${Uri.encodeComponent(path)}');
|
||
} else {
|
||
// Some OneLink opens arrive without query params; AppsFlyer SDK usually
|
||
// delivers deep_link_value shortly after. Delay root fallback to avoid
|
||
// overriding the real deep-link route.
|
||
_oneLinkFallbackTimer?.cancel();
|
||
_oneLinkFallbackTimer = Timer(const Duration(milliseconds: 1500), () {
|
||
if (mounted) appRouter.go('/');
|
||
});
|
||
}
|
||
return;
|
||
}
|
||
}
|
||
|
||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||
final appLinks = AppLinks();
|
||
final initial = await appLinks.getInitialLink();
|
||
handleIncomingLink(initial);
|
||
|
||
_appLinkSubscription = appLinks.uriLinkStream.listen((uri) {
|
||
handleIncomingLink(uri);
|
||
});
|
||
});
|
||
}
|
||
|
||
_initAppsFlyer() async {
|
||
// AppsFlyer dev key; OneLink template in share uses https://fcscstats.onelink.me/V6mR
|
||
// In AppsFlyer dashboard (OneLink template V6mR): set Android package ae.gov.fcsc.stats,
|
||
// iOS App ID (numeric from App Store Connect), and store fallback URLs to avoid app_not_found:
|
||
// Android: https://play.google.com/store/apps/details?id=ae.gov.fcsc.stats
|
||
// iOS: https://apps.apple.com/app/id1234567890 (replace with real App Store ID when live)
|
||
final options = Platform.isIOS
|
||
? AppsFlyerOptions(
|
||
afDevKey: 'K2unWqVxgJo84PDFVLiebC',
|
||
appId:
|
||
'6744919453', // Apple numeric App ID from App Store Connect (replace for production)
|
||
showDebug: true,
|
||
)
|
||
: AppsFlyerOptions(
|
||
afDevKey: 'K2unWqVxgJo84PDFVLiebC',
|
||
showDebug: true,
|
||
);
|
||
|
||
appsflyerSdk = AppsflyerSdk(options);
|
||
|
||
await appsflyerSdk.initSdk(
|
||
registerOnDeepLinkingCallback: true,
|
||
registerConversionDataCallback: true,
|
||
);
|
||
appsflyerSdk.startSDK();
|
||
|
||
// await appsflyerSdk.setDebugLog(true);
|
||
|
||
// Direct Deep Link (App already installed)
|
||
appsflyerSdk.onDeepLinking((DeepLinkResult result) {
|
||
if (result.status == Status.FOUND && result.deepLink != null) {
|
||
final deepLinkValue = result.deepLink!.deepLinkValue;
|
||
final data = result.deepLink!.clickEvent;
|
||
|
||
_handleDeepLink(deepLinkValue, data);
|
||
}
|
||
});
|
||
|
||
// Deferred Deep Link (First install case)
|
||
appsflyerSdk.onInstallConversionData((data) {
|
||
if (data['deep_link_value'] != null) {
|
||
_handleDeepLink(
|
||
data['deep_link_value'],
|
||
data,
|
||
);
|
||
}
|
||
});
|
||
}
|
||
|
||
void _handleDeepLink(String? deepLinkValue, Map<String, dynamic> data) {
|
||
_oneLinkFallbackTimer?.cancel();
|
||
|
||
String? resolved = deepLinkValue;
|
||
|
||
// AppsFlyer may not always populate deepLinkValue for complex links.
|
||
// Fallback to clickEvent payload keys.
|
||
resolved ??= data['deep_link_value']?.toString();
|
||
resolved ??= data['af_dp']?.toString();
|
||
resolved = resolved?.trim();
|
||
|
||
if (resolved == null || resolved.isEmpty) return;
|
||
|
||
// If AppsFlyer gives a full URL, preserve path+query only.
|
||
if (resolved.startsWith('http://') || resolved.startsWith('https://')) {
|
||
final parsed = Uri.tryParse(resolved);
|
||
if (parsed != null) {
|
||
resolved = parsed.path + (parsed.hasQuery ? '?${parsed.query}' : '');
|
||
}
|
||
}
|
||
|
||
// Decode once so encoded query paths like
|
||
// %2FpopulationGrowth%2Fgdp%3FbgColor%3D... become route-friendly.
|
||
try {
|
||
resolved = Uri.decodeComponent(resolved);
|
||
} catch (_) {}
|
||
|
||
// Use full path as intendedPath so shared links open the right screen.
|
||
final String intendedPath =
|
||
resolved!.startsWith('/') ? resolved : '/$resolved';
|
||
|
||
context.go('/?intendedPath=${Uri.encodeComponent(intendedPath)}');
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final locale = ref.watch(localeProvider);
|
||
|
||
// final connectivityProvider = Provider<bool>((ref) {
|
||
// return true; // Replace with actual logic
|
||
// });
|
||
//
|
||
// ref.listen<bool>(connectivityProvider, (previous, hasInternet) {
|
||
// print('Previous Internet Status: $previous');
|
||
// print('Current Internet Status: $hasInternet');
|
||
// if (previous != null && hasInternet != previous) {
|
||
// handleConnectivityChange(context, hasInternet);
|
||
// }
|
||
// });
|
||
/// ✅ SAFE PLACE FOR ref.listen
|
||
if (!_listenerAttached) {
|
||
_listenerAttached = true;
|
||
|
||
ref.listen<AsyncValue<bool>>(legacy_connectivity.connectivityProvider, (
|
||
previous,
|
||
next,
|
||
) {
|
||
final hasInternet = next.valueOrNull;
|
||
if (hasInternet == null) return;
|
||
handleConnectivityChange(hasInternet);
|
||
});
|
||
}
|
||
return MaterialApp.router(
|
||
debugShowCheckedModeBanner: false,
|
||
routerConfig: appRouter,
|
||
locale: locale,
|
||
theme: AppTheme.light,
|
||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||
supportedLocales: AppLocalizations.supportedLocales,
|
||
);
|
||
}
|
||
|
||
void handleConnectivityChange(bool hasInternet) {
|
||
final currentPath =
|
||
GoRouter.of(context).routerDelegate.currentConfiguration.fullPath;
|
||
if (!hasInternet && currentPath != AppRoutes.internetCheck) {
|
||
context.go(AppRoutes.internetCheck);
|
||
}
|
||
}
|
||
}
|