diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 611ea01..5c2b151 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -1,8 +1,5 @@ plugins { id("com.android.application") - // START: FlutterFire Configuration - id("com.google.gms.google-services") - // END: FlutterFire Configuration id("kotlin-android") // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. id("dev.flutter.flutter-gradle-plugin") diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts index 9e2d35c..a439442 100644 --- a/android/settings.gradle.kts +++ b/android/settings.gradle.kts @@ -19,9 +19,6 @@ pluginManagement { plugins { id("dev.flutter.flutter-plugin-loader") version "1.0.0" id("com.android.application") version "8.7.0" apply false - // START: FlutterFire Configuration - id("com.google.gms.google-services") version("4.3.15") apply false - // END: FlutterFire Configuration id("org.jetbrains.kotlin.android") version "1.8.22" apply false } diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 0c024f1..62f81a9 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -1,32 +1,83 @@ import Flutter -import Firebase -import FirebaseMessaging import UIKit +import UserNotifications +// Mirrors LockInBroMobile/Services/NotificationService.swift: pure native APNs, +// no Firebase. The hex device token is forwarded to Dart over a MethodChannel +// (`blindmaster/apns`) which then POSTs it to /register_apns_token. @main -@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate { +@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate, UNUserNotificationCenterDelegate { + private var apnsChannel: FlutterMethodChannel? + private var pendingToken: String? + override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { - // Explicitly register for remote notifications on every launch so iOS always - // calls didRegisterForRemoteNotificationsWithDeviceToken, ensuring Firebase - // receives a fresh APNs token regardless of whether permission was already granted. + UNUserNotificationCenter.current().delegate = self + // Trigger APNs registration on every launch so iOS calls + // didRegisterForRemoteNotificationsWithDeviceToken with a fresh token. application.registerForRemoteNotifications() return super.application(application, didFinishLaunchingWithOptions: launchOptions) } func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) { GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry) + + let registrar = engineBridge.pluginRegistry.registrar(forPlugin: "BlindMasterApns") + let channel = FlutterMethodChannel( + name: "blindmaster/apns", + binaryMessenger: registrar.messenger() + ) + channel.setMethodCallHandler { [weak self] call, result in + switch call.method { + case "requestPermission": + UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, error in + if let error = error { print("[APNs] permission error: \(error)") } + DispatchQueue.main.async { + UIApplication.shared.registerForRemoteNotifications() + } + result(granted) + } + case "getToken": + result(self?.pendingToken) + default: + result(FlutterMethodNotImplemented) + } + } + apnsChannel = channel + + // If iOS already delivered the token before Dart attached the channel, + // flush it now so the upload still happens. + if let token = pendingToken { + channel.invokeMethod("onToken", arguments: token) + } } - // FlutterImplicitEngineDelegate can interfere with Firebase's method swizzling, - // preventing it from capturing the APNs token. Forward it explicitly instead. override func application( _ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data ) { - Messaging.messaging().apnsToken = deviceToken + let token = deviceToken.map { String(format: "%02.2hhx", $0) }.joined() + pendingToken = token + apnsChannel?.invokeMethod("onToken", arguments: token) super.application(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken) } + + override func application( + _ application: UIApplication, + didFailToRegisterForRemoteNotificationsWithError error: Error + ) { + print("[APNs] registration failed: \(error)") + super.application(application, didFailToRegisterForRemoteNotificationsWithError: error) + } + + // Show banners while the app is in the foreground (matches LockInBroMobile). + override func userNotificationCenter( + _ center: UNUserNotificationCenter, + willPresent notification: UNNotification, + withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void + ) { + completionHandler([.banner, .sound, .badge]) + } } diff --git a/lib/BlindMasterResources/apns_service.dart b/lib/BlindMasterResources/apns_service.dart new file mode 100644 index 0000000..1664b81 --- /dev/null +++ b/lib/BlindMasterResources/apns_service.dart @@ -0,0 +1,66 @@ +import 'package:blind_master/BlindMasterResources/secure_transmissions.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; + +/// Bridge to the native iOS APNs registration in `ios/Runner/AppDelegate.swift`. +/// The Swift side captures the device token in +/// `didRegisterForRemoteNotificationsWithDeviceToken`, hex-encodes it, and +/// pushes it back here over the `blindmaster/apns` MethodChannel. We then POST +/// it to `/register_apns_token` so the server can target this device. +/// +/// Mirrors the LockInBroMobile NotificationService flow — Android has no APNs +/// equivalent and the backend only supports APNs, so this is a no-op there. +class ApnsService { + static const _channel = MethodChannel('blindmaster/apns'); + static bool _handlerInstalled = false; + + /// Wire the channel handler once at startup so a token delivered before the + /// first `register()` call (or after, asynchronously) is still uploaded. + static void install() { + if (_handlerInstalled) return; + if (defaultTargetPlatform != TargetPlatform.iOS && + defaultTargetPlatform != TargetPlatform.macOS) { + return; + } + _handlerInstalled = true; + _channel.setMethodCallHandler((call) async { + if (call.method == 'onToken') { + final token = call.arguments as String?; + if (token == null || token.isEmpty) return; + debugPrint('APNS: token received'); + try { + await securePost({'token': token}, 'register_apns_token'); + } catch (e) { + debugPrint('APNS: token upload failed: $e'); + } + } + }); + } + + /// Request notification permission and upload the cached token if APNs has + /// already produced one. Safe to call repeatedly (login + session-restore) — + /// the server upserts. + static Future register() async { + if (defaultTargetPlatform != TargetPlatform.iOS && + defaultTargetPlatform != TargetPlatform.macOS) { + return; + } + install(); + try { + final granted = + await _channel.invokeMethod('requestPermission') ?? false; + if (!granted) { + debugPrint('APNS: notifications denied — enable in Settings'); + return; + } + final token = await _channel.invokeMethod('getToken'); + if (token == null || token.isEmpty) { + debugPrint('APNS: token not ready yet — onToken handler will catch it'); + return; + } + await securePost({'token': token}, 'register_apns_token'); + } catch (e) { + debugPrint('APNS registration failed: $e'); + } + } +} diff --git a/lib/BlindMasterResources/fcm_service.dart b/lib/BlindMasterResources/fcm_service.dart deleted file mode 100644 index 0b91db7..0000000 --- a/lib/BlindMasterResources/fcm_service.dart +++ /dev/null @@ -1,50 +0,0 @@ -import 'package:blind_master/BlindMasterResources/secure_transmissions.dart'; -import 'package:firebase_messaging/firebase_messaging.dart'; -import 'package:flutter/foundation.dart'; - -class FcmService { - /// Request permission, fetch the FCM token, and register it with the server. - /// Safe to call on every login/session-restore — the server just upserts the value. - static Future register() async { - debugPrint('FCM: register() called'); - try { - final messaging = FirebaseMessaging.instance; - final settings = await messaging.requestPermission(alert: true, badge: true, sound: true); - debugPrint('FCM: authorization status: ${settings.authorizationStatus}'); - if (settings.authorizationStatus == AuthorizationStatus.denied) { - debugPrint('FCM: notifications denied — enable in Settings > [App] > Notifications'); - return; - } - await messaging.setForegroundNotificationPresentationOptions( - alert: true, - badge: true, - sound: true, - ); - // On iOS, APNs token must be available before FCM token can be fetched. - // getAPNSToken() can block if iOS hasn't finished APNs registration yet, - // so cap it with a timeout and retry once after a short delay. - if (defaultTargetPlatform == TargetPlatform.iOS || - defaultTargetPlatform == TargetPlatform.macOS) { - String? apnsToken = await messaging.getAPNSToken() - .timeout(const Duration(seconds: 3), onTimeout: () => null); - if (apnsToken == null) { - debugPrint('FCM: APNs token not ready, retrying in 5s...'); - await Future.delayed(const Duration(seconds: 5)); - apnsToken = await messaging.getAPNSToken() - .timeout(const Duration(seconds: 5), onTimeout: () => null); - } - if (apnsToken == null) { - debugPrint('FCM: APNs token unavailable — simulator or APNs not configured'); - return; - } - debugPrint('FCM: APNs token acquired'); - } - final token = await messaging.getToken(); - debugPrint('FCM TOKEN: ${token ?? "null — likely running on simulator"}'); - if (token == null) return; - await securePost({'token': token}, 'register_fcm_token'); - } catch (e) { - debugPrint('FCM registration failed: $e'); - } - } -} diff --git a/lib/BlindMasterResources/secure_transmissions.dart b/lib/BlindMasterResources/secure_transmissions.dart index 7ca8959..0e51312 100644 --- a/lib/BlindMasterResources/secure_transmissions.dart +++ b/lib/BlindMasterResources/secure_transmissions.dart @@ -13,7 +13,7 @@ String host = local; int port = 3000; String priv = "$scheme://$host:$port"; -String pub = "https://wahwa.com"; +String pub = "https://blindmaster.wahwa.com"; String socketString = pub; diff --git a/lib/BlindMasterScreens/Startup/login_screen.dart b/lib/BlindMasterScreens/Startup/login_screen.dart index a01b959..b92c3fb 100644 --- a/lib/BlindMasterScreens/Startup/login_screen.dart +++ b/lib/BlindMasterScreens/Startup/login_screen.dart @@ -1,4 +1,4 @@ -import 'package:blind_master/BlindMasterResources/fcm_service.dart'; +import 'package:blind_master/BlindMasterResources/apns_service.dart'; import 'package:blind_master/BlindMasterResources/secure_transmissions.dart'; import 'package:blind_master/BlindMasterScreens/Startup/create_user_screen.dart'; import 'package:blind_master/BlindMasterScreens/Startup/forgot_password_screen.dart'; @@ -62,7 +62,7 @@ class _LoginScreenState extends State { backgroundColor: Colors.orange[700], duration: Duration(seconds: 4), content: Text( - "Your account has not been verified. Please check your email from blindmasterapp@wahwa.com and verify your account.", + "Your account has not been verified. Please check your email from account-services@blindmaster.wahwa.com and verify your account.", textAlign: TextAlign.center, style: TextStyle(fontSize: 15), ), @@ -85,7 +85,7 @@ class _LoginScreenState extends State { if (token.isEmpty) throw Exception('Token Not Received'); final storage = FlutterSecureStorage(); await storage.write(key: 'token', value: token); - await FcmService.register(); + await ApnsService.register(); } catch(e) { if (!mounted) return; diff --git a/lib/BlindMasterScreens/Startup/splash_screen.dart b/lib/BlindMasterScreens/Startup/splash_screen.dart index c327280..e4e2877 100644 --- a/lib/BlindMasterScreens/Startup/splash_screen.dart +++ b/lib/BlindMasterScreens/Startup/splash_screen.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'dart:convert'; -import 'package:blind_master/BlindMasterResources/fcm_service.dart'; +import 'package:blind_master/BlindMasterResources/apns_service.dart'; import 'package:blind_master/BlindMasterResources/secure_transmissions.dart'; import 'package:blind_master/BlindMasterScreens/home_screen.dart'; import 'package:blind_master/BlindMasterScreens/Startup/login_screen.dart'; @@ -51,7 +51,7 @@ class _SplashScreenState extends State { } nextScreen = HomeScreen(); - await FcmService.register(); + await ApnsService.register(); } else { nextScreen = LoginScreen(); } diff --git a/lib/BlindMasterScreens/Startup/verify_new_email_screen.dart b/lib/BlindMasterScreens/Startup/verify_new_email_screen.dart index 5f07831..1c47c17 100644 --- a/lib/BlindMasterScreens/Startup/verify_new_email_screen.dart +++ b/lib/BlindMasterScreens/Startup/verify_new_email_screen.dart @@ -19,7 +19,7 @@ class _VerificationWaitingScreenState extends BaseVerificationWaitingScreenState String get title => "Verify Your Email"; @override - String get mainMessage => "We've sent a verification link to your email from blindmasterapp@wahwa.com"; + String get mainMessage => "We've sent a verification link to your email from account-services@blindmaster.wahwa.com"; @override String get instructionMessage => "Click the link in the email to verify your account. This page will automatically update once verified."; @@ -29,7 +29,7 @@ class _VerificationWaitingScreenState extends BaseVerificationWaitingScreenState @override Future checkStatus() async { - final uri = Uri.parse('https://wahwa.com').replace(path: 'verification_status'); + final uri = Uri.parse('https://blindmaster.wahwa.com').replace(path: 'verification_status'); final response = await http.get( uri, @@ -49,7 +49,7 @@ class _VerificationWaitingScreenState extends BaseVerificationWaitingScreenState @override Future resendVerification() async { final localHour = DateTime.now().hour; - final uri = Uri.parse('https://wahwa.com').replace(path: 'resend_verification'); + final uri = Uri.parse('https://blindmaster.wahwa.com').replace(path: 'resend_verification'); final response = await http.post( uri, diff --git a/lib/firebase_options.dart b/lib/firebase_options.dart deleted file mode 100644 index bf90eb9..0000000 --- a/lib/firebase_options.dart +++ /dev/null @@ -1,88 +0,0 @@ -// File generated by FlutterFire CLI. -// ignore_for_file: type=lint -import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; -import 'package:flutter/foundation.dart' - show defaultTargetPlatform, kIsWeb, TargetPlatform; - -/// Default [FirebaseOptions] for use with your Firebase apps. -/// -/// Example: -/// ```dart -/// import 'firebase_options.dart'; -/// // ... -/// await Firebase.initializeApp( -/// options: DefaultFirebaseOptions.currentPlatform, -/// ); -/// ``` -class DefaultFirebaseOptions { - static FirebaseOptions get currentPlatform { - if (kIsWeb) { - return web; - } - switch (defaultTargetPlatform) { - case TargetPlatform.android: - return android; - case TargetPlatform.iOS: - return ios; - case TargetPlatform.macOS: - return macos; - case TargetPlatform.windows: - return windows; - case TargetPlatform.linux: - throw UnsupportedError( - 'DefaultFirebaseOptions have not been configured for linux - ' - 'you can reconfigure this by running the FlutterFire CLI again.', - ); - default: - throw UnsupportedError( - 'DefaultFirebaseOptions are not supported for this platform.', - ); - } - } - - static const FirebaseOptions web = FirebaseOptions( - apiKey: 'AIzaSyDnRirxd46eWwUkK9cKi-g0mKaIBY3LODM', - appId: '1:956683546941:web:f66e35806267907c121554', - messagingSenderId: '956683546941', - projectId: 'blindmaster-54055', - authDomain: 'blindmaster-54055.firebaseapp.com', - storageBucket: 'blindmaster-54055.firebasestorage.app', - measurementId: 'G-Y31FSGG3KP', - ); - - static const FirebaseOptions android = FirebaseOptions( - apiKey: 'AIzaSyBJzL-jeo4xa_rQkHymzku_2lIJ6WJ8hoI', - appId: '1:956683546941:android:828055a1f543b75f121554', - messagingSenderId: '956683546941', - projectId: 'blindmaster-54055', - storageBucket: 'blindmaster-54055.firebasestorage.app', - ); - - static const FirebaseOptions ios = FirebaseOptions( - apiKey: 'AIzaSyAH5KvipSKH5J6dkjd6Ft7ALAqBYANB-Jo', - appId: '1:956683546941:ios:1059be0ae683894b121554', - messagingSenderId: '956683546941', - projectId: 'blindmaster-54055', - storageBucket: 'blindmaster-54055.firebasestorage.app', - iosBundleId: 'com.adipu.blindMaster', - ); - - static const FirebaseOptions macos = FirebaseOptions( - apiKey: 'AIzaSyAH5KvipSKH5J6dkjd6Ft7ALAqBYANB-Jo', - appId: '1:956683546941:ios:1059be0ae683894b121554', - messagingSenderId: '956683546941', - projectId: 'blindmaster-54055', - storageBucket: 'blindmaster-54055.firebasestorage.app', - iosBundleId: 'com.adipu.blindMaster', - ); - - static const FirebaseOptions windows = FirebaseOptions( - apiKey: 'AIzaSyDnRirxd46eWwUkK9cKi-g0mKaIBY3LODM', - appId: '1:956683546941:web:f5d94d05b6ce6bef121554', - messagingSenderId: '956683546941', - projectId: 'blindmaster-54055', - authDomain: 'blindmaster-54055.firebaseapp.com', - storageBucket: 'blindmaster-54055.firebasestorage.app', - measurementId: 'G-WR6581J4P4', - ); -} diff --git a/lib/main.dart b/lib/main.dart index 6e93358..bd36c99 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,29 +1,20 @@ -import 'package:blind_master/BlindMasterResources/fcm_service.dart'; +import 'package:blind_master/BlindMasterResources/apns_service.dart'; import 'package:blind_master/BlindMasterScreens/Startup/splash_screen.dart'; -import 'package:firebase_core/firebase_core.dart'; -import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/material.dart'; -import 'firebase_options.dart'; // generated by: flutterfire configure - -// Handles FCM messages that arrive when the app is terminated or in the background. -@pragma('vm:entry-point') -Future _firebaseMessagingBackgroundHandler(RemoteMessage message) async { - // The system notification tray handles display automatically — nothing to do here. -} enum DaysOfWeek {Su, M, Tu, W, Th, F, Sa} -void main() async { +void main() { WidgetsFlutterBinding.ensureInitialized(); - await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); - FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler); - FirebaseMessaging.instance.onTokenRefresh.listen((_) => FcmService.register()); + // Wire the APNs MethodChannel handler before any UI runs so a token + // delivered by iOS during launch is still picked up. + ApnsService.install(); runApp(const MyApp()); } Map getBackgroundBasedOnTime() { final hour = DateTime.now().hour; - + Color secondaryLight; Color primary; Color secondaryDark; @@ -43,7 +34,7 @@ Map getBackgroundBasedOnTime() { secondaryLight = const Color.fromARGB(255, 186, 130, 255); secondaryDark = const Color.fromARGB(255, 40, 0, 89); } - + return { 'primary': primary, 'secondaryLight': secondaryLight, @@ -78,4 +69,3 @@ class MyApp extends StatelessWidget { ); } } - diff --git a/pubspec.yaml b/pubspec.yaml index d0f7519..c58d9b3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -42,8 +42,6 @@ dependencies: flutter_secure_storage: ^9.2.4 flutter_xlider: ^3.5.0 socket_io_client: ^3.1.2 - firebase_core: ^3.0.0 - firebase_messaging: ^15.0.0 dev_dependencies: flutter_test: