preTesting

This commit is contained in:
2026-05-05 03:56:11 -05:00
parent 14d026841f
commit 39428b4451
12 changed files with 142 additions and 181 deletions

View File

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

View File

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

View File

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

View File

@@ -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<void> register() async {
if (defaultTargetPlatform != TargetPlatform.iOS &&
defaultTargetPlatform != TargetPlatform.macOS) {
return;
}
install();
try {
final granted =
await _channel.invokeMethod<bool>('requestPermission') ?? false;
if (!granted) {
debugPrint('APNS: notifications denied — enable in Settings');
return;
}
final token = await _channel.invokeMethod<String?>('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');
}
}
}

View File

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

View File

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

View File

@@ -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<LoginScreen> {
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<LoginScreen> {
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;

View File

@@ -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<SplashScreen> {
}
nextScreen = HomeScreen();
await FcmService.register();
await ApnsService.register();
} else {
nextScreen = LoginScreen();
}

View File

@@ -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<bool> 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<void> 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,

View File

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

View File

@@ -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<void> _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<String, Color> getBackgroundBasedOnTime() {
final hour = DateTime.now().hour;
Color secondaryLight;
Color primary;
Color secondaryDark;
@@ -43,7 +34,7 @@ Map<String, Color> 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 {
);
}
}

View File

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