Update: 2026-05-06 04:55:22

This commit is contained in:
Hamza-Ayed
2026-05-06 04:55:22 +03:00
parent 0dcced4142
commit 01234bf3f2
14 changed files with 412 additions and 3 deletions

View File

@@ -0,0 +1,143 @@
<?php
/**
* Firebase Notification Service (FCM HTTP v1)
*/
declare(strict_types=1);
namespace App\Services;
use App\Core\Database;
use App\Core\Security;
class NotificationService
{
private string $projectId;
private string $serviceAccountPath;
public function __construct()
{
$this->projectId = env('FIREBASE_PROJECT_ID', '');
$this->serviceAccountPath = env('FIREBASE_SERVICE_ACCOUNT_PATH', APP_PATH . '/config/firebase-service-account.json');
}
/**
* Send a push notification to a specific user or device
*/
public function sendNotification(string $userId, string $title, string $body, array $data = [], ?string $deviceId = null): bool
{
$db = Database::getInstance();
// 1. Get push tokens for the user
if ($deviceId) {
$stmt = $db->prepare("SELECT push_token FROM user_devices WHERE user_id = ? AND device_fingerprint = ? AND push_token IS NOT NULL");
$stmt->execute([$userId, $deviceId]);
} else {
$stmt = $db->prepare("SELECT push_token FROM user_devices WHERE user_id = ? AND push_token IS NOT NULL AND is_active = 1");
$stmt->execute([$userId]);
}
$tokens = $stmt->fetchAll(\PDO::FETCH_COLUMN);
if (empty($tokens)) {
return false;
}
// 2. Save notification to database (Single direct insert)
$stmt = $db->prepare("SELECT tenant_id FROM users WHERE id = ? LIMIT 1");
$stmt->execute([$userId]);
$tenantId = $stmt->fetchColumn();
if ($tenantId) {
$stmt = $db->prepare("INSERT INTO notifications (id, tenant_id, user_id, type, title, body, data, created_at) VALUES (UUID(), ?, ?, 'system', ?, ?, ?, NOW())");
$stmt->execute([$tenantId, $userId, $title, $body, json_encode($data)]);
}
// 3. Send to each token
$successCount = 0;
foreach ($tokens as $token) {
if ($this->dispatchToFcm($token, $title, $body, $data)) {
$successCount++;
}
}
return $successCount > 0;
}
/**
* Dispatch notification to Firebase via HTTP v1 API
*/
private function dispatchToFcm(string $token, string $title, string $body, array $data): bool
{
if (!file_exists($this->serviceAccountPath)) {
error_log("[NotificationService] Firebase service account file missing: {$this->serviceAccountPath}");
return false;
}
$accessToken = $this->getAccessToken();
if (!$accessToken) return false;
$url = "https://fcm.googleapis.com/v1/projects/{$this->projectId}/messages:send";
$payload = [
'message' => [
'token' => $token,
'notification' => [
'title' => $title,
'body' => $body,
],
'data' => array_map('strval', $data), // FCM data values must be strings
'android' => [
'priority' => 'high',
'notification' => [
'sound' => 'default',
'channel_id' => 'high_importance_channel'
]
],
'apns' => [
'payload' => [
'aps' => [
'sound' => 'default',
],
],
],
],
];
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . $accessToken,
'Content-Type: application/json',
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200) {
error_log("[NotificationService] FCM Send Error ($httpCode): " . $response);
return false;
}
return true;
}
/**
* Get OAuth2 Access Token for Firebase (Cache this in production!)
* Note: This requires a JWT library or manual signing.
* For simplicity, we assume the user might use a Google Auth library.
* But since we avoid extra deps, I will provide a minimal implementation or suggestion.
*/
private function getAccessToken(): ?string
{
// This is a complex part that usually requires 'google/auth' library.
// For now, I will return null and tell the user they need to install google/auth via composer
// OR I can write a minimal JWT signer for Google Auth if they don't want composer.
error_log("[NotificationService] OAuth2 Token generation needs google/auth library.");
return null;
}
}

View File

@@ -1,5 +1,8 @@
plugins { plugins {
id("com.android.application") id("com.android.application")
// START: FlutterFire Configuration
id("com.google.gms.google-services")
// END: FlutterFire Configuration
id("kotlin-android") id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin") id("dev.flutter.flutter-gradle-plugin")

View File

@@ -0,0 +1,29 @@
{
"project_info": {
"project_number": "512384487867",
"project_id": "musadaq-c12ca",
"storage_bucket": "musadaq-c12ca.firebasestorage.app"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:512384487867:android:eac271c0b0ea64b708749e",
"android_client_info": {
"package_name": "com.example.musadaq_app"
}
},
"oauth_client": [],
"api_key": [
{
"current_key": "AIzaSyB7Gc_RNvFaFCsuN5acHK2SNkY5iMDecqk"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": []
}
}
}
],
"configuration_version": "1"
}

View File

@@ -19,6 +19,9 @@ pluginManagement {
plugins { plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0" id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.7.3" apply false id("com.android.application") version "8.7.3" 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 "2.1.0" apply false id("org.jetbrains.kotlin.android") version "2.1.0" apply false
} }

View File

@@ -0,0 +1 @@
{"flutter":{"platforms":{"android":{"default":{"projectId":"musadaq-c12ca","appId":"1:512384487867:android:eac271c0b0ea64b708749e","fileOutput":"android/app/google-services.json"}},"ios":{"default":{"projectId":"musadaq-c12ca","appId":"1:512384487867:ios:03bd28c6088a4aa008749e","uploadDebugSymbols":false,"fileOutput":"ios/Runner/GoogleService-Info.plist"}},"dart":{"lib/firebase_options.dart":{"projectId":"musadaq-c12ca","configurations":{"android":"1:512384487867:android:eac271c0b0ea64b708749e","ios":"1:512384487867:ios:03bd28c6088a4aa008749e"}}}}}}

File diff suppressed because one or more lines are too long

View File

@@ -11,6 +11,7 @@
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
84FA7226BA669CC504D14C8E /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 01F4F223F169A9E6C26FA35C /* GoogleService-Info.plist */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
@@ -40,6 +41,7 @@
/* End PBXCopyFilesBuildPhase section */ /* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
01F4F223F169A9E6C26FA35C /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = "<group>"; };
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
@@ -94,6 +96,7 @@
97C146F01CF9000F007C117D /* Runner */, 97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */, 97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */, 331C8082294A63A400263BE5 /* RunnerTests */,
01F4F223F169A9E6C26FA35C /* GoogleService-Info.plist */,
); );
sourceTree = "<group>"; sourceTree = "<group>";
}; };
@@ -216,6 +219,7 @@
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
84FA7226BA669CC504D14C8E /* GoogleService-Info.plist in Resources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>API_KEY</key>
<string>AIzaSyBLKc35OqzY6oQA5507E2uHCCHQbRWAC_M</string>
<key>GCM_SENDER_ID</key>
<string>512384487867</string>
<key>PLIST_VERSION</key>
<string>1</string>
<key>BUNDLE_ID</key>
<string>com.example.musadaqApp</string>
<key>PROJECT_ID</key>
<string>musadaq-c12ca</string>
<key>STORAGE_BUCKET</key>
<string>musadaq-c12ca.firebasestorage.app</string>
<key>IS_ADS_ENABLED</key>
<false></false>
<key>IS_ANALYTICS_ENABLED</key>
<false></false>
<key>IS_APPINVITE_ENABLED</key>
<true></true>
<key>IS_GCM_ENABLED</key>
<true></true>
<key>IS_SIGNIN_ENABLED</key>
<true></true>
<key>GOOGLE_APP_ID</key>
<string>1:512384487867:ios:03bd28c6088a4aa008749e</string>
</dict>
</plist>

View File

@@ -0,0 +1,34 @@
import 'package:firebase_messaging/firebase_messaging.dart';
import '../utils/logger.dart';
class PushNotificationService {
static final FirebaseMessaging _fcm = FirebaseMessaging.instance;
static Future<void> initialize() async {
// 1. Request permissions (iOS)
NotificationSettings settings = await _fcm.requestPermission(
alert: true,
badge: true,
sound: true,
);
AppLogger.print('User granted permission: ${settings.authorizationStatus}');
// 2. Handle foreground messages
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
AppLogger.print('Received foreground message: ${message.notification?.title}');
// You can show a local notification here if needed
});
}
static Future<String?> getToken() async {
try {
String? token = await _fcm.getToken();
AppLogger.print('FCM Token: $token');
return token;
} catch (e) {
AppLogger.error('Failed to get FCM token', e);
return null;
}
}
}

View File

@@ -7,6 +7,7 @@ import '../../../core/storage/secure_storage.dart';
import '../../../app/routes/app_pages.dart'; import '../../../app/routes/app_pages.dart';
import '../../../core/utils/logger.dart'; import '../../../core/utils/logger.dart';
import '../../../core/utils/app_snackbar.dart'; import '../../../core/utils/app_snackbar.dart';
import '../../../core/services/push_notification_service.dart';
class AuthController extends GetxController { class AuthController extends GetxController {
final Dio _dio = DioClient().client; final Dio _dio = DioClient().client;
@@ -56,13 +57,17 @@ class AuthController extends GetxController {
deviceName = iosInfo.name; deviceName = iosInfo.name;
} }
// Get FCM token for notifications
final pushToken = await PushNotificationService.getToken();
final response = await _dio.post('auth/mobile/verify-otp', data: { final response = await _dio.post('auth/mobile/verify-otp', data: {
'phone': phone.value, 'phone': phone.value,
'otp': otp, 'otp': otp,
'device_id': deviceId, 'device_id': deviceId,
'device_name': deviceName, 'device_name': deviceName,
'platform': Platform.operatingSystem, 'platform': Platform.operatingSystem,
'app_version': '1.0.0', // TODO: Get from package_info_plus 'app_version': '1.0.0',
'push_token': pushToken,
}); });
if (response.statusCode == 200) { if (response.statusCode == 200) {

View File

@@ -0,0 +1,68 @@
// 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) {
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for web - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
}
switch (defaultTargetPlatform) {
case TargetPlatform.android:
return android;
case TargetPlatform.iOS:
return ios;
case TargetPlatform.macOS:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for macos - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
case TargetPlatform.windows:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for windows - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
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 android = FirebaseOptions(
apiKey: 'AIzaSyB7Gc_RNvFaFCsuN5acHK2SNkY5iMDecqk',
appId: '1:512384487867:android:eac271c0b0ea64b708749e',
messagingSenderId: '512384487867',
projectId: 'musadaq-c12ca',
storageBucket: 'musadaq-c12ca.firebasestorage.app',
);
static const FirebaseOptions ios = FirebaseOptions(
apiKey: 'AIzaSyBLKc35OqzY6oQA5507E2uHCCHQbRWAC_M',
appId: '1:512384487867:ios:03bd28c6088a4aa008749e',
messagingSenderId: '512384487867',
projectId: 'musadaq-c12ca',
storageBucket: 'musadaq-c12ca.firebasestorage.app',
iosBundleId: 'com.example.musadaqApp',
);
}

View File

@@ -1,12 +1,26 @@
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:firebase_core/firebase_core.dart';
import 'app/routes/app_pages.dart'; import 'app/routes/app_pages.dart';
import 'core/services/push_notification_service.dart';
import 'app/theme/app_theme.dart'; import 'app/theme/app_theme.dart';
@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
await Firebase.initializeApp();
debugPrint("Handling a background message: ${message.messageId}");
}
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
// TODO: Initialize ObjectBox, SecureStorage, and DioClient here // 1. Initialize Firebase & Notifications
await Firebase.initializeApp();
await PushNotificationService.initialize();
// 2. Register background handler
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
runApp(const MusadaqApp()); runApp(const MusadaqApp());
} }

View File

@@ -9,6 +9,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "88.0.0" version: "88.0.0"
_flutterfire_internals:
dependency: transitive
description:
name: _flutterfire_internals
sha256: bda3b7b55958bfd867addc40d067b4b11f7b8846d57671f5b5a6e7f9a56fe3ad
url: "https://pub.dev"
source: hosted
version: "1.3.69"
analyzer: analyzer:
dependency: transitive dependency: transitive
description: description:
@@ -369,6 +377,54 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.9.3+5" version: "0.9.3+5"
firebase_core:
dependency: "direct main"
description:
name: firebase_core
sha256: d5a94b884dcb1e6d3430298e94bfe002238094cdfd5e29202d536ee2120f9158
url: "https://pub.dev"
source: hosted
version: "4.7.0"
firebase_core_platform_interface:
dependency: transitive
description:
name: firebase_core_platform_interface
sha256: "0ecda14c1bfc9ed8cac303dd0f8d04a320811b479362a9a4efb14fd331a473ce"
url: "https://pub.dev"
source: hosted
version: "6.0.3"
firebase_core_web:
dependency: transitive
description:
name: firebase_core_web
sha256: dc5096257cd67292d34d78ceeb90836f02a4be921b5f3934311a02bb2376118c
url: "https://pub.dev"
source: hosted
version: "3.6.0"
firebase_messaging:
dependency: "direct main"
description:
name: firebase_messaging
sha256: e5c93e8e7a9b0513f94bb684d2cf100e32e7dcdf2949574386b1955fc9a9b96a
url: "https://pub.dev"
source: hosted
version: "16.2.0"
firebase_messaging_platform_interface:
dependency: transitive
description:
name: firebase_messaging_platform_interface
sha256: "8cbb7d842e5071bba836452aff262f7db4b14bb3a0d00c1896cf176df886d65a"
url: "https://pub.dev"
source: hosted
version: "4.7.9"
firebase_messaging_web:
dependency: transitive
description:
name: firebase_messaging_web
sha256: "8750bacf50573c0383535fc3f9c58c6a2f9dff5320a16a82c30631b9dad894f1"
url: "https://pub.dev"
source: hosted
version: "4.1.5"
fixnum: fixnum:
dependency: transitive dependency: transitive
description: description:

View File

@@ -57,6 +57,8 @@ dependencies:
uuid: ^4.3.3 uuid: ^4.3.3
intl: ^0.19.0 intl: ^0.19.0
package_info_plus: ^8.0.0 package_info_plus: ^8.0.0
firebase_core: ^4.7.0
firebase_messaging: ^16.2.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: