From bd81173559da89ac8351f3b71ee4b72c50e0fc0b Mon Sep 17 00:00:00 2001 From: Mark M2 Macbook Date: Sat, 30 May 2026 12:12:14 +0200 Subject: [PATCH] fcm --- .vscode/settings.json | 3 + analysis_options.yaml | 4 + android/app/build.gradle.kts | 3 + android/app/google-services.json | 29 ++ android/settings.gradle.kts | 3 + firebase.json | 1 + ios/Runner.xcodeproj/project.pbxproj | 4 + ios/Runner/GoogleService-Info.plist | 30 ++ lib/core/blocs/session/session_cubit.dart | 72 +++ lib/core/routes/app_router.dart | 9 +- lib/features/home/ui/home_screen.dart | 2 +- .../staff/data/staff_repository.dart | 44 +- .../settings/ui/reminder_settings_screen.dart | 9 +- lib/features/tasks/blocs/task_form_cubit.dart | 45 +- lib/features/tasks/data/task_repository.dart | 21 + lib/features/tasks/ui/task_form_screen.dart | 12 +- lib/firebase_options.dart | 89 ++++ lib/main.dart | 14 +- macos/Flutter/GeneratedPluginRegistrant.swift | 4 + macos/Runner.xcodeproj/project.pbxproj | 24 +- macos/Runner/GoogleService-Info.plist | 30 ++ pubspec.lock | 56 +++ pubspec.yaml | 2 + supabase/.gitignore | 8 + supabase/config.toml | 424 ++++++++++++++++++ supabase/functions/send-reminders/.npmrc | 3 + supabase/functions/send-reminders/deno.json | 6 + supabase/functions/send-reminders/index.ts | 116 +++++ .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 30 files changed, 1020 insertions(+), 51 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 android/app/google-services.json create mode 100644 firebase.json create mode 100644 ios/Runner/GoogleService-Info.plist create mode 100644 lib/firebase_options.dart create mode 100644 macos/Runner/GoogleService-Info.plist create mode 100644 supabase/.gitignore create mode 100644 supabase/config.toml create mode 100644 supabase/functions/send-reminders/.npmrc create mode 100644 supabase/functions/send-reminders/deno.json create mode 100644 supabase/functions/send-reminders/index.ts diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..b943dbc --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "deno.enable": true +} \ No newline at end of file diff --git a/analysis_options.yaml b/analysis_options.yaml index d78598d..799a98c 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -7,6 +7,10 @@ analyzer: - "lib/l10n/*.dart" - "**/*.g.dart" # Già che ci siamo escludiamo tutti i file generati (tipo quelli di JsonSerializable) - "**/*.freezed.dart" + - "build/**" + - "ios/**" + - "macos/**" + - ".dart_tool/**" linter: rules: diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 0380795..b381628 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -1,5 +1,8 @@ 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/app/google-services.json b/android/app/google-services.json new file mode 100644 index 0000000..49948c0 --- /dev/null +++ b/android/app/google-services.json @@ -0,0 +1,29 @@ +{ + "project_info": { + "project_number": "249756116297", + "project_id": "flux-87e49", + "storage_bucket": "flux-87e49.firebasestorage.app" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:249756116297:android:a2c3d37323752069cf2698", + "android_client_info": { + "package_name": "com.catellisrl.flux" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "AIzaSyA6-uX6504B3yofeo7YQwfQaS0cCDoZnvY" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts index ca7fe06..174f408 100644 --- a/android/settings.gradle.kts +++ b/android/settings.gradle.kts @@ -20,6 +20,9 @@ pluginManagement { plugins { id("dev.flutter.flutter-plugin-loader") version "1.0.0" id("com.android.application") version "8.11.1" 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.2.20" apply false } diff --git a/firebase.json b/firebase.json new file mode 100644 index 0000000..3e65992 --- /dev/null +++ b/firebase.json @@ -0,0 +1 @@ +{"flutter":{"platforms":{"android":{"default":{"projectId":"flux-87e49","appId":"1:249756116297:android:a2c3d37323752069cf2698","fileOutput":"android/app/google-services.json"}},"ios":{"default":{"projectId":"flux-87e49","appId":"1:249756116297:ios:fe9dadca7150da16cf2698","uploadDebugSymbols":false,"fileOutput":"ios/Runner/GoogleService-Info.plist"}},"macos":{"default":{"projectId":"flux-87e49","appId":"1:249756116297:ios:fe9dadca7150da16cf2698","uploadDebugSymbols":false,"fileOutput":"macos/Runner/GoogleService-Info.plist"}},"dart":{"lib/firebase_options.dart":{"projectId":"flux-87e49","configurations":{"android":"1:249756116297:android:a2c3d37323752069cf2698","ios":"1:249756116297:ios:fe9dadca7150da16cf2698","macos":"1:249756116297:ios:fe9dadca7150da16cf2698","web":"1:249756116297:web:7c652e51004414b7cf2698","windows":"1:249756116297:web:b094277c2fedb425cf2698"}}}}}} \ No newline at end of file diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index dc2e342..4a9bb5e 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 101B9A4BF8F30D998DDC11D4 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = D4B70082D3146D8C2B7AFA02 /* GoogleService-Info.plist */; }; 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; @@ -67,6 +68,7 @@ 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; AB44F93458B7D70EE383A3A9 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; BDDDA09E437D9C0E7B65B3B1 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + D4B70082D3146D8C2B7AFA02 /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -126,6 +128,7 @@ 331C8082294A63A400263BE5 /* RunnerTests */, F5D002C3092D87755D552D32 /* Pods */, 6A991A28CCED9666CA172E00 /* Frameworks */, + D4B70082D3146D8C2B7AFA02 /* GoogleService-Info.plist */, ); sourceTree = ""; }; @@ -267,6 +270,7 @@ 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + 101B9A4BF8F30D998DDC11D4 /* GoogleService-Info.plist in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/ios/Runner/GoogleService-Info.plist b/ios/Runner/GoogleService-Info.plist new file mode 100644 index 0000000..7df9dd3 --- /dev/null +++ b/ios/Runner/GoogleService-Info.plist @@ -0,0 +1,30 @@ + + + + + API_KEY + AIzaSyAllwaoNyqHsZtqfMMo9DxVS6-q7yBwWow + GCM_SENDER_ID + 249756116297 + PLIST_VERSION + 1 + BUNDLE_ID + com.catellisrl.flux + PROJECT_ID + flux-87e49 + STORAGE_BUCKET + flux-87e49.firebasestorage.app + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:249756116297:ios:fe9dadca7150da16cf2698 + + \ No newline at end of file diff --git a/lib/core/blocs/session/session_cubit.dart b/lib/core/blocs/session/session_cubit.dart index 5060f53..4ff9974 100644 --- a/lib/core/blocs/session/session_cubit.dart +++ b/lib/core/blocs/session/session_cubit.dart @@ -1,9 +1,14 @@ +import 'dart:io'; + import 'package:equatable/equatable.dart'; +import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/core/data/core_repository.dart'; import 'package:flux/features/company/models/company_model.dart'; import 'package:flux/features/master_data/staff/models/staff_member_model.dart'; import 'package:flux/features/master_data/store/models/store_model.dart'; +import 'package:get_it/get_it.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:collection/collection.dart'; // Per firstWhereOrNull @@ -134,6 +139,11 @@ class SessionCubit extends Cubit { onboardingStep: OnboardingStep.none, // Svuotiamo l'onboarding ), ); + // --- REGISTRAZIONE DISPOSITIVO PER NOTIFICHE PUSH --- + // Lo chiamiamo SENZA 'await' in modo che il caricamento dell'app non si blocchi. + // L'utente entrerà subito nell'app e poi vedrà comparire il popup di sistema + // per accettare i permessi delle notifiche. + _registerFcmToken(companyId: company.id!, staffId: staff.id!); } catch (e) { // Se esplode il database, non lasciamo l'app freezata in 'initial' emit( @@ -145,6 +155,68 @@ class SessionCubit extends Cubit { } } + Future _registerFcmToken({ + required String companyId, + required String staffId, + }) async { + // Scudo anti-crash per lo sviluppo su Linux / Windows + if (!kIsWeb && + !Platform.isAndroid && + !Platform.isIOS && + !Platform.isMacOS) { + return; + } + + try { + final messaging = FirebaseMessaging.instance; + + // 1. Richiesta permessi di notifica + final settings = await messaging.requestPermission( + alert: true, + badge: true, + sound: true, + ); + + if (settings.authorizationStatus == AuthorizationStatus.authorized) { + // 2. Cattura del token dal device + final String? fcmToken = await messaging.getToken(); + + if (fcmToken != null) { + final supabase = GetIt.I.get(); + + // Determiniamo la piattaforma in modo sicuro per Linux + String osPlatform = 'web'; + if (!kIsWeb) { + if (Platform.isAndroid) osPlatform = 'android'; + if (Platform.isIOS) osPlatform = 'ios'; + if (Platform.isMacOS) osPlatform = 'macos'; + } + + // 3. UPSERT su Supabase condizionato dal vincolo 'fcm_token' + await supabase.from('staff_devices').upsert( + { + 'company_id': companyId, + 'staff_id': staffId, + 'fcm_token': fcmToken, + 'os_platform': osPlatform, + 'updated_at': DateTime.now().toIso8601String(), + }, + onConflict: + 'fcm_token', // Se il token esiste già, aggiorna questa riga! + ); + + debugPrint( + 'Dispositivo registrato con successo su FLUX Cloud. Platform: $osPlatform', + ); + } + } else { + debugPrint('Permesso push negato dall\'utente.'); + } + } catch (e) { + debugPrint('Errore durante la registrazione del dispositivo: $e'); + } + } + void updateCurrentCompany(CompanyModel newCompany) { emit(state.copyWith(company: newCompany)); } diff --git a/lib/core/routes/app_router.dart b/lib/core/routes/app_router.dart index 43cc005..f824ef1 100644 --- a/lib/core/routes/app_router.dart +++ b/lib/core/routes/app_router.dart @@ -545,16 +545,21 @@ class AppRouter { realTaskId = pathId; } - final allStaffList = context.read().state.allStaff; + List? preloadedStaff; + try { + preloadedStaff = context.read().state.allStaff; + } catch (_) { + preloadedStaff = null; // Fallback se la rotta è isolata + } // Creiamo il BLoC "al volo" solo per questa schermata return MultiBlocProvider( providers: [ BlocProvider( create: (context) => TaskFormCubit( - globalStaff: allStaffList, existingTask: task, initialTaskId: realTaskId, + allStaff: preloadedStaff, ), ), ], diff --git a/lib/features/home/ui/home_screen.dart b/lib/features/home/ui/home_screen.dart index f99352f..644a4a8 100644 --- a/lib/features/home/ui/home_screen.dart +++ b/lib/features/home/ui/home_screen.dart @@ -232,7 +232,7 @@ class HomeScreen extends StatelessWidget { QuickActionButton( icon: Icons.task_alt, label: context.l10n.commonTask, - color: Colors.teal, + color: Colors.orange, onTap: () { context.pushNamed(Routes.taskForm, pathParameters: {'id': 'new'}); }, diff --git a/lib/features/master_data/staff/data/staff_repository.dart b/lib/features/master_data/staff/data/staff_repository.dart index 9617f99..006e8a2 100644 --- a/lib/features/master_data/staff/data/staff_repository.dart +++ b/lib/features/master_data/staff/data/staff_repository.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:flux/core/enums_and_consts/consts.dart'; import 'package:flux/features/master_data/staff/models/staff_member_model.dart'; import 'package:flux/features/master_data/store/models/store_model.dart'; @@ -10,19 +11,38 @@ class StaffRepository { // --- ANAGRAFICA PURA --- // Prende tutto lo staff della Company (per l'Hub Anagrafiche) - Future> getStaffMembers(String companyId) async { - final response = await _supabase - .from(Tables.staffMembers) - .select(''' - *, - store_assignments:${Tables.staffInStores} ( - ${Tables.stores}(*) - ) - ''') - .eq('company_id', companyId) - .order('name', ascending: true); + Future> getStaffMembers( + String companyId, { + String? storeId, + }) async { + try { + var filterBuilder = _supabase + .from(Tables.staffMembers) + .select(''' + *, + store_assignments:${Tables.staffInStores} ( + ${Tables.stores}(*) + ) + ''') + .eq('company_id', companyId); - return (response as List).map((s) => StaffMemberModel.fromMap(s)).toList(); + if (storeId != null) { + filterBuilder = filterBuilder.or( + 'store_id.eq.$storeId,store_id.is.null', + ); + } + + var transformBuilder = filterBuilder.order('name', ascending: true); + + final response = await transformBuilder; + + return (response as List) + .map((s) => StaffMemberModel.fromMap(s)) + .toList(); + } on Exception catch (e) { + debugPrint('Errore nel recupero della lista di staff: $e'); + throw Exception('Errore nel recupero della lista di staff: $e'); + } } Future getStaffMemberById(String staffId) async { diff --git a/lib/features/settings/ui/reminder_settings_screen.dart b/lib/features/settings/ui/reminder_settings_screen.dart index 5ec4a31..5f8fb63 100644 --- a/lib/features/settings/ui/reminder_settings_screen.dart +++ b/lib/features/settings/ui/reminder_settings_screen.dart @@ -18,6 +18,7 @@ class _ReminderSettingsScreenState extends State { } void _showAddReminderBottomSheet(BuildContext context) { + final cubit = context.read(); // Valori preselezionati int selectedMinutes = 15; String selectedChannel = 'push'; @@ -73,8 +74,9 @@ class _ReminderSettingsScreenState extends State { ), ], onChanged: (val) { - if (val != null) + if (val != null) { setModalState(() => selectedMinutes = val); + } }, ), const SizedBox(height: 16), @@ -113,8 +115,9 @@ class _ReminderSettingsScreenState extends State { ), ], onChanged: (val) { - if (val != null) + if (val != null) { setModalState(() => selectedChannel = val); + } }, ), const SizedBox(height: 32), @@ -125,7 +128,7 @@ class _ReminderSettingsScreenState extends State { padding: const EdgeInsets.symmetric(vertical: 16), ), onPressed: () { - context.read().addReminder( + cubit.addReminder( minutesBefore: selectedMinutes, channel: selectedChannel, ); diff --git a/lib/features/tasks/blocs/task_form_cubit.dart b/lib/features/tasks/blocs/task_form_cubit.dart index 449e340..7d79140 100644 --- a/lib/features/tasks/blocs/task_form_cubit.dart +++ b/lib/features/tasks/blocs/task_form_cubit.dart @@ -1,12 +1,13 @@ import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/core/blocs/session/session_cubit.dart'; +import 'package:flux/features/master_data/staff/data/staff_repository.dart'; import 'package:flux/features/master_data/staff/models/staff_member_model.dart'; import 'package:flux/features/settings/data/settings_repository.dart'; import 'package:flux/features/tasks/data/task_repository.dart'; import 'package:flux/features/tasks/models/task_model.dart'; import 'package:flux/features/tasks/models/task_reminder_config.dart'; -import 'package:flux/features/tasks/models/task_status.dart'; import 'package:get_it/get_it.dart'; part 'task_form_state.dart'; @@ -14,12 +15,16 @@ class TaskFormCubit extends Cubit { final TasksRepository _repository = GetIt.I.get(); final SettingsRepository _settingsRepository = GetIt.I .get(); + final _staffRepository = GetIt.I.get(); final SessionCubit _sessionCubit = GetIt.I.get(); + final List? _preloadedStaff; TaskFormCubit({ String? initialTaskId, // <-- RIPRISTINATO PER DEEP LINK TaskModel? existingTask, - }) : super(const TaskFormState()) { + List? allStaff, + }) : _preloadedStaff = allStaff, + super(const TaskFormState()) { // Avviamo l'inizializzazione centralizzata (gestisce sia mem, sia deep link, sia nuovo) initForm(initialTaskId: initialTaskId, existingTask: existingTask); } @@ -77,18 +82,34 @@ class TaskFormCubit extends Cubit { // --- LOGICA GESTIONE STAFF (GLOBAL STAFF / STORE STAFF) --- Future _loadAndGroupStaff() async { - // Se isGlobal è true, passiamo null come storeId al repo per tirare giù tutta l'azienda - final List staffList = await _repository - .fetchAvailableStaff( - companyId: _companyId, - storeId: state.isGlobal ? null : _currentStoreId, - ); + final List staffList; + + // SE C'È LO STAFF PASCIUTO DALL'APP USA QUELLO, ALTRIMENTI CHIAMA IL REPO + if (_preloadedStaff != null && _preloadedStaff.isNotEmpty) { + staffList = _preloadedStaff; + } else { + staffList = await _staffRepository.getStaffMembers(_companyId); + } - // Raggruppamento per nome del negozio (Mappa { "Nome Negozio": [Membri] }) final Map> grouped = {}; + for (var staff in staffList) { - final storeName = staff.storeName ?? 'Senza Sede'; - grouped.putIfAbsent(storeName, () => []).add(staff); + if (!state.isGlobal) { + final belongsToCurrentStore = staff.assignedStores.any( + (store) => store.id == _currentStoreId, + ); + if (!belongsToCurrentStore) continue; + } + + if (staff.assignedStores.isEmpty) { + grouped.putIfAbsent('Direzione / Senza Sede', () => []).add(staff); + } else { + for (var store in staff.assignedStores) { + if (!state.isGlobal && store.id != _currentStoreId) continue; + final storeName = store.name; + grouped.putIfAbsent(storeName, () => []).add(staff); + } + } } emit(state.copyWith(groupedAvailableStaff: grouped)); @@ -138,7 +159,7 @@ class TaskFormCubit extends Cubit { ); emit(state.copyWith(reminders: existingConfigs)); } catch (e) { - print('Errore caricamento reminder: $e'); + debugPrint('Errore caricamento reminder: $e'); } } diff --git a/lib/features/tasks/data/task_repository.dart b/lib/features/tasks/data/task_repository.dart index 7a39a64..0989f91 100644 --- a/lib/features/tasks/data/task_repository.dart +++ b/lib/features/tasks/data/task_repository.dart @@ -39,6 +39,7 @@ class TasksRepository { ) .toList(); } catch (e) { + debugPrint('Errore fetch personal reminders: $e'); throw Exception('Errore fetch personal reminders: $e'); } } @@ -98,6 +99,26 @@ class TasksRepository { } } + Future fetchTaskById(String taskId) async { + try { + final response = await _supabase + .from(Tables.tasks) + .select(''' + *, + task_assignments:${Tables.taskAssignments} ( + ${Tables.staffMembers} (*) + ) + ''') + .eq('id', taskId) + .single(); + + return TaskModel.fromMap(response); + } catch (e) { + debugPrint('Errore fetch task by id: $e'); + throw Exception('Errore fetch task by id: $e'); + } + } + // ========================================================================= // REALTIME STREAM (La sentinella per la bacheca) // ========================================================================= diff --git a/lib/features/tasks/ui/task_form_screen.dart b/lib/features/tasks/ui/task_form_screen.dart index f244508..3c5771d 100644 --- a/lib/features/tasks/ui/task_form_screen.dart +++ b/lib/features/tasks/ui/task_form_screen.dart @@ -1,8 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:flux/features/tasks/blocs/task_form_cubit.dart'; -import 'package:get_it/get_it.dart'; import 'package:go_router/go_router.dart'; class TaskFormScreen extends StatefulWidget { @@ -136,15 +134,7 @@ class _TaskFormScreenState extends State { ) else TextButton.icon( - onPressed: state.isFormValid - ? () => cubit.saveTask( - currentUserId: GetIt.I - .get() - .state - .currentStaffMember! - .id!, - ) - : null, + onPressed: state.isFormValid ? () => cubit.saveTask() : null, icon: const Icon(Icons.save), label: const Text('Salva'), style: TextButton.styleFrom( diff --git a/lib/firebase_options.dart b/lib/firebase_options.dart new file mode 100644 index 0000000..e9f8ae7 --- /dev/null +++ b/lib/firebase_options.dart @@ -0,0 +1,89 @@ +// 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: 'AIzaSyACOLz2mY8fHd5RWfJmDvN53LCd5_TxI6o', + appId: '1:249756116297:web:7c652e51004414b7cf2698', + messagingSenderId: '249756116297', + projectId: 'flux-87e49', + authDomain: 'flux-87e49.firebaseapp.com', + storageBucket: 'flux-87e49.firebasestorage.app', + measurementId: 'G-6V4VN8GWWZ', + ); + + static const FirebaseOptions android = FirebaseOptions( + apiKey: 'AIzaSyA6-uX6504B3yofeo7YQwfQaS0cCDoZnvY', + appId: '1:249756116297:android:a2c3d37323752069cf2698', + messagingSenderId: '249756116297', + projectId: 'flux-87e49', + storageBucket: 'flux-87e49.firebasestorage.app', + ); + + static const FirebaseOptions ios = FirebaseOptions( + apiKey: 'AIzaSyAllwaoNyqHsZtqfMMo9DxVS6-q7yBwWow', + appId: '1:249756116297:ios:fe9dadca7150da16cf2698', + messagingSenderId: '249756116297', + projectId: 'flux-87e49', + storageBucket: 'flux-87e49.firebasestorage.app', + iosBundleId: 'com.catellisrl.flux', + ); + + static const FirebaseOptions macos = FirebaseOptions( + apiKey: 'AIzaSyAllwaoNyqHsZtqfMMo9DxVS6-q7yBwWow', + appId: '1:249756116297:ios:fe9dadca7150da16cf2698', + messagingSenderId: '249756116297', + projectId: 'flux-87e49', + storageBucket: 'flux-87e49.firebasestorage.app', + iosBundleId: 'com.catellisrl.flux', + ); + + static const FirebaseOptions windows = FirebaseOptions( + apiKey: 'AIzaSyACOLz2mY8fHd5RWfJmDvN53LCd5_TxI6o', + appId: '1:249756116297:web:b094277c2fedb425cf2698', + messagingSenderId: '249756116297', + projectId: 'flux-87e49', + authDomain: 'flux-87e49.firebaseapp.com', + storageBucket: 'flux-87e49.firebasestorage.app', + measurementId: 'G-8E29KT6RWX', + ); + +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 011d7d0..07cb950 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -22,6 +23,7 @@ import 'package:flux/features/tickets/blocs/ticket_list_cubit.dart'; import 'package:flux/features/tickets/data/ticket_repository.dart'; import 'package:flux/features/tracking/blocs/tracking_cubit.dart'; import 'package:flux/features/tracking/data/tracking_repository.dart'; +import 'package:flux/firebase_options.dart'; import 'package:flux/l10n/app_localizations.dart'; import 'package:get_it/get_it.dart'; import 'package:go_router/go_router.dart'; @@ -53,6 +55,16 @@ void main() async { await setupLocator(); // RIMUOVE IL CARATTERE # DAGLI URL WEB! usePathUrlStrategy(); + // Lo Scudo Ninja: Inizializziamo Firebase SOLO sulle piattaforme supportate + if (kIsWeb || Platform.isAndroid || Platform.isIOS || Platform.isMacOS) { + try { + await Firebase.initializeApp( + options: DefaultFirebaseOptions.currentPlatform, + ); + } catch (e) { + debugPrint('Errore inizializzazione Firebase: $e'); + } + } runApp( MultiBlocProvider( providers: [ @@ -138,7 +150,7 @@ Future setupLocator() async { () => TicketsShippingRepository(), ); getIt.registerLazySingleton(() => NotesRepository()); - getIt.registerLazySingleton(() => TaskRepository()); + getIt.registerLazySingleton(() => TasksRepository()); getIt.registerLazySingleton(() => SettingsRepository()); } diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 9d0506c..63d8a3e 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -8,6 +8,8 @@ import Foundation import app_links import file_picker import file_selector_macos +import firebase_core +import firebase_messaging import package_info_plus import pdfx import printing @@ -18,6 +20,8 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin")) FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) + FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) + FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) PdfxPlugin.register(with: registry.registrar(forPlugin: "PdfxPlugin")) PrintingPlugin.register(with: registry.registrar(forPlugin: "PrintingPlugin")) diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index 1d3d986..2a55705 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -28,8 +28,9 @@ 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; 47B861EC08643C31319819EE /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9AD6F0633B38D7C51DB0A44A /* Pods_RunnerTests.framework */; }; - BC7B14BF366111D5491A16DE /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F9C1847336C291D2358A2A03 /* Pods_Runner.framework */; }; + 654626D9777B906635ABD770 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 5541A1B1FDEB6E0619C1BD7E /* GoogleService-Info.plist */; }; 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; + BC7B14BF366111D5491A16DE /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F9C1847336C291D2358A2A03 /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -79,7 +80,9 @@ 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 5541A1B1FDEB6E0619C1BD7E /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = ""; }; 64CEA5375FF158D0C9BAC5E3 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; 7B4E4499661A94FDCB94A882 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; @@ -89,7 +92,6 @@ EF12DE99A4F7A47E54D9243F /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; F673AA005B814BAB5FA49C69 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; F9C1847336C291D2358A2A03 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -141,6 +143,7 @@ 33CC10EE2044A3C60003C045 /* Products */, D73912EC22F37F3D000D13A0 /* Frameworks */, BABCD4273B81B107DD58605D /* Pods */, + 5541A1B1FDEB6E0619C1BD7E /* GoogleService-Info.plist */, ); sourceTree = ""; }; @@ -235,9 +238,6 @@ productType = "com.apple.product-type.bundle.unit-test"; }; 33CC10EC2044A3C60003C045 /* Runner */ = { - packageProductDependencies = ( - 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, - ); isa = PBXNativeTarget; buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( @@ -255,6 +255,9 @@ 33CC11202044C79F0003C045 /* PBXTargetDependency */, ); name = Runner; + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + ); productName = Runner; productReference = 33CC10ED2044A3C60003C045 /* flux.app */; productType = "com.apple.product-type.application"; @@ -263,9 +266,6 @@ /* Begin PBXProject section */ 33CC10E52044A3C60003C045 /* Project object */ = { - packageReferences = ( - 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */, - ); isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; @@ -302,6 +302,9 @@ Base, ); mainGroup = 33CC10E42044A3C60003C045; + packageReferences = ( + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */, + ); productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; projectDirPath = ""; projectRoot = ""; @@ -327,6 +330,7 @@ files = ( 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + 654626D9777B906635ABD770 /* GoogleService-Info.plist in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -806,12 +810,14 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + /* Begin XCLocalSwiftPackageReference section */ - 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = { + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = { isa = XCLocalSwiftPackageReference; relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; }; /* End XCLocalSwiftPackageReference section */ + /* Begin XCSwiftPackageProductDependency section */ 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { isa = XCSwiftPackageProductDependency; diff --git a/macos/Runner/GoogleService-Info.plist b/macos/Runner/GoogleService-Info.plist new file mode 100644 index 0000000..7df9dd3 --- /dev/null +++ b/macos/Runner/GoogleService-Info.plist @@ -0,0 +1,30 @@ + + + + + API_KEY + AIzaSyAllwaoNyqHsZtqfMMo9DxVS6-q7yBwWow + GCM_SENDER_ID + 249756116297 + PLIST_VERSION + 1 + BUNDLE_ID + com.catellisrl.flux + PROJECT_ID + flux-87e49 + STORAGE_BUCKET + flux-87e49.firebasestorage.app + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:249756116297:ios:fe9dadca7150da16cf2698 + + \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index ec07cf0..15e9f8f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,14 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + _flutterfire_internals: + dependency: transitive + description: + name: _flutterfire_internals + sha256: "8f89e371e2883de35cdc78f648e725fa4da5f3b6c927269f00fa68f1ea92b598" + url: "https://pub.dev" + source: hosted + version: "1.3.71" app_links: dependency: transitive description: @@ -257,6 +265,54 @@ packages: url: "https://pub.dev" source: hosted version: "0.9.3+5" + firebase_core: + dependency: "direct main" + description: + name: firebase_core + sha256: "93a5bde9775fd5adcc937f39dfa04ae0bc89c4d79bea6abc49de3f7b049d9ff6" + url: "https://pub.dev" + source: hosted + version: "4.9.0" + firebase_core_platform_interface: + dependency: transitive + description: + name: firebase_core_platform_interface + sha256: "4a120366dbf7d5a8ee9438978530b664b855728fb8dcc3a201017660817e555b" + url: "https://pub.dev" + source: hosted + version: "7.0.1" + firebase_core_web: + dependency: transitive + description: + name: firebase_core_web + sha256: "7c98f10b8c8e5adedc0b810b66a877120696675e2c22d9ca9caca092da0d9e57" + url: "https://pub.dev" + source: hosted + version: "3.7.0" + firebase_messaging: + dependency: "direct main" + description: + name: firebase_messaging + sha256: "8d0dc81a31cd030170508dc3e89bfd14355b20a1b991340af5f018e37daab5d7" + url: "https://pub.dev" + source: hosted + version: "16.2.2" + firebase_messaging_platform_interface: + dependency: transitive + description: + name: firebase_messaging_platform_interface + sha256: "37abb0b0535c5497605ee94c12470e1ebbbe47e71a22d0c20bffcc912311f8cb" + url: "https://pub.dev" + source: hosted + version: "4.7.11" + firebase_messaging_web: + dependency: transitive + description: + name: firebase_messaging_web + sha256: "54e22b43e2c26a2728a3f68c188de0f9011993ae19ae959a06d476dad935c776" + url: "https://pub.dev" + source: hosted + version: "4.1.7" fixnum: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index bb8b7ab..491471b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -39,6 +39,8 @@ dependencies: flutter_launcher_icons: ^0.14.4 package_info_plus: ^9.0.1 flutter_staggered_grid_view: ^0.7.0 + firebase_core: ^4.9.0 + firebase_messaging: ^16.2.2 dependency_overrides: pdfx: diff --git a/supabase/.gitignore b/supabase/.gitignore new file mode 100644 index 0000000..ad9264f --- /dev/null +++ b/supabase/.gitignore @@ -0,0 +1,8 @@ +# Supabase +.branches +.temp + +# dotenvx +.env.keys +.env.local +.env.*.local diff --git a/supabase/config.toml b/supabase/config.toml new file mode 100644 index 0000000..4a4654e --- /dev/null +++ b/supabase/config.toml @@ -0,0 +1,424 @@ +# For detailed configuration reference documentation, visit: +# https://supabase.com/docs/guides/local-development/cli/config +# A string used to distinguish different Supabase projects on the same host. Defaults to the +# working directory name when running `supabase init`. +project_id = "flux" + +[api] +enabled = true +# Port to use for the API URL. +port = 54321 +# Schemas to expose in your API. Tables, views and stored procedures in this schema will get API +# endpoints. `public` and `graphql_public` schemas are included by default. +schemas = ["public", "graphql_public"] +# Extra schemas to add to the search_path of every request. +extra_search_path = ["public", "extensions"] +# The maximum number of rows returns from a view, table, or stored procedure. Limits payload size +# for accidental or malicious requests. +max_rows = 1000 +# Controls whether new tables, views, sequences and functions created in the `public` schema by +# `postgres` are reachable through the Data API roles (`anon`, `authenticated`, `service_role`) +# without explicit GRANTs. Leave unset today to preserve local behaviour. The implicit default +# flips to `false` on 2026-05-30 to match the new cloud default, and the field is removed in +# 2026-10-30 once the always-revoked behaviour is permanent. Set to `false` to opt in early. +# auto_expose_new_tables = false + +[api.tls] +# Enable HTTPS endpoints locally using a self-signed certificate. +enabled = false +# Paths to self-signed certificate pair. +# cert_path = "../certs/my-cert.pem" +# key_path = "../certs/my-key.pem" + +[db] +# Port to use for the local database URL. +port = 54322 +# Port used by db diff command to initialize the shadow database. +shadow_port = 54320 +# Maximum amount of time to wait for health check when starting the local database. +health_timeout = "2m" +# The database major version to use. This has to be the same as your remote database's. Run `SHOW +# server_version;` on the remote database to check. +major_version = 17 + +[db.pooler] +enabled = false +# Port to use for the local connection pooler. +port = 54329 +# Specifies when a server connection can be reused by other clients. +# Configure one of the supported pooler modes: `transaction`, `session`. +pool_mode = "transaction" +# How many server connections to allow per user/database pair. +default_pool_size = 20 +# Maximum number of client connections allowed. +max_client_conn = 100 + +# [db.vault] +# secret_key = "env(SECRET_VALUE)" + +[db.migrations] +# If disabled, migrations will be skipped during a db push or reset. +enabled = true +# Specifies an ordered list of schema files that describe your database. +# Supports glob patterns relative to supabase directory: "./schemas/*.sql" +schema_paths = [] + +[db.seed] +# If enabled, seeds the database after migrations during a db reset. +enabled = true +# Specifies an ordered list of seed files to load during db reset. +# Supports glob patterns relative to supabase directory: "./seeds/*.sql" +sql_paths = ["./seed.sql"] + +[db.network_restrictions] +# Enable management of network restrictions. +enabled = false +# List of IPv4 CIDR blocks allowed to connect to the database. +# Defaults to allow all IPv4 connections. Set empty array to block all IPs. +allowed_cidrs = ["0.0.0.0/0"] +# List of IPv6 CIDR blocks allowed to connect to the database. +# Defaults to allow all IPv6 connections. Set empty array to block all IPs. +allowed_cidrs_v6 = ["::/0"] + +# Uncomment to reject non-secure connections to the database. +# [db.ssl_enforcement] +# enabled = true + +[realtime] +enabled = true +# Bind realtime via either IPv4 or IPv6. (default: IPv4) +# ip_version = "IPv6" +# The maximum length in bytes of HTTP request headers. (default: 4096) +# max_header_length = 4096 + +[studio] +enabled = true +# Port to use for Supabase Studio. +port = 54323 +# External URL of the API server that frontend connects to. +api_url = "http://127.0.0.1" +# OpenAI API Key to use for Supabase AI in the Supabase Studio. +openai_api_key = "env(OPENAI_API_KEY)" + +# Email testing server. Emails sent with the local dev setup are not actually sent - rather, they +# are monitored, and you can view the emails that would have been sent from the web interface. +[inbucket] +enabled = true +# Port to use for the email testing server web interface. +port = 54324 +# Uncomment to expose additional ports for testing user applications that send emails. +# smtp_port = 54325 +# pop3_port = 54326 +# admin_email = "admin@email.com" +# sender_name = "Admin" + +[storage] +enabled = true +# The maximum file size allowed (e.g. "5MB", "500KB"). +file_size_limit = "50MiB" + +# Uncomment to configure local storage buckets +# [storage.buckets.images] +# public = false +# file_size_limit = "50MiB" +# allowed_mime_types = ["image/png", "image/jpeg"] +# objects_path = "./images" + +# Allow connections via S3 compatible clients +[storage.s3_protocol] +enabled = true + +# Image transformation API is available to Supabase Pro plan. +# [storage.image_transformation] +# enabled = true + +# Store analytical data in S3 for running ETL jobs over Iceberg Catalog +# This feature is only available on the hosted platform. +[storage.analytics] +enabled = false +max_namespaces = 5 +max_tables = 10 +max_catalogs = 2 + +# Analytics Buckets is available to Supabase Pro plan. +# [storage.analytics.buckets.my-warehouse] + +# Store vector embeddings in S3 for large and durable datasets +[storage.vector] +enabled = false +max_buckets = 10 +max_indexes = 5 + +# Vector Buckets is available to Supabase Pro plan. +# [storage.vector.buckets.documents-openai] + +[auth] +enabled = true +# The base URL of your website. Used as an allow-list for redirects and for constructing URLs used +# in emails. +site_url = "http://127.0.0.1:3000" +# The public URL that Auth serves on. Defaults to the API external URL with `/auth/v1` appended. +# external_url = "" +# A list of *exact* URLs that auth providers are permitted to redirect to post authentication. +additional_redirect_urls = ["https://127.0.0.1:3000"] +# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week). +jwt_expiry = 3600 +# JWT issuer URL. If not set, defaults to auth.external_url. +# jwt_issuer = "" +# Path to JWT signing key. DO NOT commit your signing keys file to git. +# signing_keys_path = "./signing_keys.json" +# If disabled, the refresh token will never expire. +enable_refresh_token_rotation = true +# Allows refresh tokens to be reused after expiry, up to the specified interval in seconds. +# Requires enable_refresh_token_rotation = true. +refresh_token_reuse_interval = 10 +# Allow/disallow new user signups to your project. +enable_signup = true +# Allow/disallow anonymous sign-ins to your project. +enable_anonymous_sign_ins = false +# Allow/disallow testing manual linking of accounts +enable_manual_linking = false +# Passwords shorter than this value will be rejected as weak. Minimum 6, recommended 8 or more. +minimum_password_length = 6 +# Passwords that do not meet the following requirements will be rejected as weak. Supported values +# are: `letters_digits`, `lower_upper_letters_digits`, `lower_upper_letters_digits_symbols` +password_requirements = "" + +# Configure passkey sign-ins. +# [auth.passkey] +# enabled = false + +# Configure WebAuthn relying party settings (required when passkey is enabled). +# [auth.webauthn] +# rp_display_name = "Supabase" +# rp_id = "localhost" +# rp_origins = ["http://127.0.0.1:3000"] + +[auth.rate_limit] +# Number of emails that can be sent per hour. Requires auth.email.smtp to be enabled. +email_sent = 2 +# Number of SMS messages that can be sent per hour. Requires auth.sms to be enabled. +sms_sent = 30 +# Number of anonymous sign-ins that can be made per hour per IP address. Requires enable_anonymous_sign_ins = true. +anonymous_users = 30 +# Number of sessions that can be refreshed in a 5 minute interval per IP address. +token_refresh = 150 +# Number of sign up and sign-in requests that can be made in a 5 minute interval per IP address (excludes anonymous users). +sign_in_sign_ups = 30 +# Number of OTP / Magic link verifications that can be made in a 5 minute interval per IP address. +token_verifications = 30 +# Number of Web3 logins that can be made in a 5 minute interval per IP address. +web3 = 30 + +# Configure one of the supported captcha providers: `hcaptcha`, `turnstile`. +# [auth.captcha] +# enabled = true +# provider = "hcaptcha" +# secret = "" + +[auth.email] +# Allow/disallow new user signups via email to your project. +enable_signup = true +# If enabled, a user will be required to confirm any email change on both the old, and new email +# addresses. If disabled, only the new email is required to confirm. +double_confirm_changes = true +# If enabled, users need to confirm their email address before signing in. +enable_confirmations = false +# If enabled, users will need to reauthenticate or have logged in recently to change their password. +secure_password_change = false +# Controls the minimum amount of time that must pass before sending another signup confirmation or password reset email. +max_frequency = "1s" +# Number of characters used in the email OTP. +otp_length = 6 +# Number of seconds before the email OTP expires (defaults to 1 hour). +otp_expiry = 3600 + +# Use a production-ready SMTP server +# [auth.email.smtp] +# enabled = true +# host = "smtp.sendgrid.net" +# port = 587 +# user = "apikey" +# pass = "env(SENDGRID_API_KEY)" +# admin_email = "admin@email.com" +# sender_name = "Admin" + +# Uncomment to customize email template +# [auth.email.template.invite] +# subject = "You have been invited" +# content_path = "./supabase/templates/invite.html" + +# Uncomment to customize notification email template +# [auth.email.notification.password_changed] +# enabled = true +# subject = "Your password has been changed" +# content_path = "./templates/password_changed_notification.html" + +[auth.sms] +# Allow/disallow new user signups via SMS to your project. +enable_signup = false +# If enabled, users need to confirm their phone number before signing in. +enable_confirmations = false +# Template for sending OTP to users +template = "Your code is {{ .Code }}" +# Controls the minimum amount of time that must pass before sending another sms otp. +max_frequency = "5s" + +# Use pre-defined map of phone number to OTP for testing. +# [auth.sms.test_otp] +# 4152127777 = "123456" + +# Configure logged in session timeouts. +# [auth.sessions] +# Force log out after the specified duration. +# timebox = "24h" +# Force log out if the user has been inactive longer than the specified duration. +# inactivity_timeout = "8h" + +# This hook runs before a new user is created and allows developers to reject the request based on the incoming user object. +# [auth.hook.before_user_created] +# enabled = true +# uri = "pg-functions://postgres/auth/before-user-created-hook" + +# This hook runs before a token is issued and allows you to add additional claims based on the authentication method used. +# [auth.hook.custom_access_token] +# enabled = true +# uri = "pg-functions:////" + +# Configure one of the supported SMS providers: `twilio`, `twilio_verify`, `messagebird`, `textlocal`, `vonage`. +[auth.sms.twilio] +enabled = false +account_sid = "" +message_service_sid = "" +# DO NOT commit your Twilio auth token to git. Use environment variable substitution instead: +auth_token = "env(SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN)" + +# Multi-factor-authentication is available to Supabase Pro plan. +[auth.mfa] +# Control how many MFA factors can be enrolled at once per user. +max_enrolled_factors = 10 + +# Control MFA via App Authenticator (TOTP) +[auth.mfa.totp] +enroll_enabled = false +verify_enabled = false + +# Configure MFA via Phone Messaging +[auth.mfa.phone] +enroll_enabled = false +verify_enabled = false +otp_length = 6 +template = "Your code is {{ .Code }}" +max_frequency = "5s" + +# Configure MFA via WebAuthn +# [auth.mfa.web_authn] +# enroll_enabled = true +# verify_enabled = true + +# Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`, +# `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin_oidc`, `notion`, `twitch`, +# `twitter`, `x`, `slack`, `spotify`, `workos`, `zoom`. +[auth.external.apple] +enabled = false +client_id = "" +# DO NOT commit your OAuth provider secret to git. Use environment variable substitution instead: +secret = "env(SUPABASE_AUTH_EXTERNAL_APPLE_SECRET)" +# Overrides the default auth callback URL derived from auth.external_url. +redirect_uri = "" +# Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure, +# or any other third-party OIDC providers. +url = "" +# If enabled, the nonce check will be skipped. Required for local sign in with Google auth. +skip_nonce_check = false +# If enabled, it will allow the user to successfully authenticate when the provider does not return an email address. +email_optional = false + +# Allow Solana wallet holders to sign in to your project via the Sign in with Solana (SIWS, EIP-4361) standard. +# You can configure "web3" rate limit in the [auth.rate_limit] section and set up [auth.captcha] if self-hosting. +[auth.web3.solana] +enabled = false + +# Use Firebase Auth as a third-party provider alongside Supabase Auth. +[auth.third_party.firebase] +enabled = false +# project_id = "my-firebase-project" + +# Use Auth0 as a third-party provider alongside Supabase Auth. +[auth.third_party.auth0] +enabled = false +# tenant = "my-auth0-tenant" +# tenant_region = "us" + +# Use AWS Cognito (Amplify) as a third-party provider alongside Supabase Auth. +[auth.third_party.aws_cognito] +enabled = false +# user_pool_id = "my-user-pool-id" +# user_pool_region = "us-east-1" + +# Use Clerk as a third-party provider alongside Supabase Auth. +[auth.third_party.clerk] +enabled = false +# Obtain from https://clerk.com/setup/supabase +# domain = "example.clerk.accounts.dev" + +# OAuth server configuration +[auth.oauth_server] +# Enable OAuth server functionality +enabled = false +# Path for OAuth consent flow UI +authorization_url_path = "/oauth/consent" +# Allow dynamic client registration +allow_dynamic_registration = false + +[edge_runtime] +enabled = true +# Supported request policies: `oneshot`, `per_worker`. +# `per_worker` (default) — enables hot reload during local development. +# `oneshot` — fallback mode if hot reload causes issues (e.g. in large repos or with symlinks). +policy = "per_worker" +# Port to attach the Chrome inspector for debugging edge functions. +inspector_port = 8083 +# The Deno major version to use. +deno_version = 2 + +# [edge_runtime.secrets] +# secret_key = "env(SECRET_VALUE)" + +[analytics] +enabled = true +port = 54327 +# Configure one of the supported backends: `postgres`, `bigquery`. +backend = "postgres" + +# Experimental features may be deprecated any time +[experimental] +# Configures Postgres storage engine to use OrioleDB (S3) +orioledb_version = "" +# Configures S3 bucket URL, eg. .s3-.amazonaws.com +s3_host = "env(S3_HOST)" +# Configures S3 bucket region, eg. us-east-1 +s3_region = "env(S3_REGION)" +# Configures AWS_ACCESS_KEY_ID for S3 bucket +s3_access_key = "env(S3_ACCESS_KEY)" +# Configures AWS_SECRET_ACCESS_KEY for S3 bucket +s3_secret_key = "env(S3_SECRET_KEY)" + +# [experimental.pgdelta] +# When enabled, pg-delta becomes the active engine for supported schema flows. +# enabled = false +# Directory under `supabase/` where declarative files are written. +# declarative_schema_path = "./database" +# JSON string passed through to pg-delta SQL formatting. +# format_options = "{\"keywordCase\":\"upper\",\"indent\":2,\"maxWidth\":80,\"commaStyle\":\"trailing\"}" + +[functions.send-reminders] +enabled = true +verify_jwt = false +import_map = "./functions/send-reminders/deno.json" +# Uncomment to specify a custom file path to the entrypoint. +# Supported file extensions are: .ts, .js, .mjs, .jsx, .tsx +entrypoint = "./functions/send-reminders/index.ts" +# Specifies static files to be bundled with the function. Supports glob patterns. +# For example, if you want to serve static HTML pages in your function: +# static_files = [ "./functions/send-reminders/*.html" ] diff --git a/supabase/functions/send-reminders/.npmrc b/supabase/functions/send-reminders/.npmrc new file mode 100644 index 0000000..48c6388 --- /dev/null +++ b/supabase/functions/send-reminders/.npmrc @@ -0,0 +1,3 @@ +# Configuration for private npm package dependencies +# For more information on using private registries with Edge Functions, see: +# https://supabase.com/docs/guides/functions/import-maps#importing-from-private-registries diff --git a/supabase/functions/send-reminders/deno.json b/supabase/functions/send-reminders/deno.json new file mode 100644 index 0000000..db206e8 --- /dev/null +++ b/supabase/functions/send-reminders/deno.json @@ -0,0 +1,6 @@ +{ + "imports": { + "@supabase/functions-js": "jsr:@supabase/functions-js@^2", + "@supabase/server": "npm:@supabase/server@^1" + } +} diff --git a/supabase/functions/send-reminders/index.ts b/supabase/functions/send-reminders/index.ts new file mode 100644 index 0000000..b582e3d --- /dev/null +++ b/supabase/functions/send-reminders/index.ts @@ -0,0 +1,116 @@ +import { serve } from "https://deno.land/std@0.168.0/http/server.ts" +import { createClient } from "https://esm.sh/@supabase/supabase-js@2" +import { JWT } from "https://esm.sh/google-auth-library@8.9.0" + +const corsHeaders = { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Headers': 'authorization, x-client-info, apiKey, content-type', +} + +serve(async (req) => { + // Gestione CORS per sicurezza + if (req.method === 'OPTIONS') { + return new Response('ok', { headers: corsHeaders }) + } + + try { + // 1. Inizializziamo il client Supabase con i poteri di Service Role (per scavalcare le RLS) + const supabaseClient = createClient( + Deno.env.get('SUPABASE_URL') ?? '', + Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? '' + ) + + // 2. Recuperiamo la chiave di Firebase dai Secrets + const firebaseSecret = Deno.env.get('FIREBASE_SERVICE_ACCOUNT') + if (!firebaseSecret) throw new Error('Missing FIREBASE_SERVICE_ACCOUNT secret') + const credentials = JSON.parse(firebaseSecret) + + // 3. Prepariamo l'autenticazione OAuth2 per Firebase HTTP v1 + const jwtClient = new JWT({ + email: credentials.client_email, + key: credentials.private_key, + scopes: ['https://www.googleapis.com/auth/firebase.messaging'], + }) + const tokens = await jwtClient.getAccessToken() + const fcmAccessToken = tokens.token + + // 4. Selezioniamo i reminder da inviare (trigger_at passato e non ancora inviati) + const { data: reminders, error: fetchError } = await supabaseClient + .from('task_reminders') + .select('id, task_id, staff_id, channel, tasks(title, description)') + .eq('channel', 'push') + .eq('is_sent', false) + .lte('trigger_at', new Date().toISOString()) + + if (fetchError) throw fetchError + + if (!reminders || reminders.length === 0) { + return new Response(JSON.stringify({ message: 'Nessun promemoria push da inviare.' }), { + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + status: 200, + }) + } + + // 5. Ciclo sui promemoria per raccogliere i token ed inviare + for (const reminder of reminders) { + const taskTitle = reminder.tasks?.title || 'Nuovo Task!'; + const taskBody = reminder.tasks?.description || 'Hai un task da completare.'; + + // Preleviamo tutti i dispositivi registrati per questo specifico membro dello staff + const { data: devices } = await supabaseClient + .from('staff_devices') + .select('fcm_token') + .eq('staff_id', reminder.staff_id) + + if (devices && devices.length > 0) { + // Spediamo la notifica a OGNI dispositivo associato all'utente + for (const device of devices) { + try { + await fetch( + `https://fcm.googleapis.com/v1/projects/${credentials.project_id}/messages:send`, + { + method: 'POST', + headers: { + 'Authorization': `Bearer ${fcmAccessToken}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + message: { + token: device.fcm_token, + notification: { + title: taskTitle, + body: taskBody, + }, + data: { + click_action: 'FLUTTER_NOTIFICATION_CLICK', + taskId: reminder.task_id, // Fondamentale per far scattare il Deep Link all'apertura! + }, + }, + }), + } + ) + } catch (pushErr) { + console.error(`Errore invio push al token ${device.fcm_token}:`, pushErr) + } + } + } + + // 6. Segnamo il reminder come inviato per non riprocessarlo al prossimo giro + await supabaseClient + .from('task_reminders') + .update({ is_sent: true, updated_at: new Date().toISOString() }) + .eq('id', reminder.id) + } + + return new Response(JSON.stringify({ success: true, processed: reminders.length }), { + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + status: 200, + }) + + } catch (error) { + return new Response(JSON.stringify({ error: error.message }), { + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + status: 500, + }) + } +}) \ No newline at end of file diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 9adb38d..f83e35b 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -18,6 +19,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("AppLinksPluginCApi")); FileSelectorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("FileSelectorWindows")); + FirebaseCorePluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FirebaseCorePluginCApi")); PdfxPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("PdfxPlugin")); PermissionHandlerWindowsPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 0ca023d..fa71da7 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -5,6 +5,7 @@ list(APPEND FLUTTER_PLUGIN_LIST app_links file_selector_windows + firebase_core pdfx permission_handler_windows printing