From 754eeffdf0f562939ccd6ed72095554907d38362 Mon Sep 17 00:00:00 2001 From: ZanderCowboy Date: Thu, 16 May 2024 23:29:51 +0200 Subject: [PATCH 01/13] add firebase_analytics and firebase_remote_config to pubspec --- apps/multichoice/pubspec.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/multichoice/pubspec.yaml b/apps/multichoice/pubspec.yaml index f1b2e688..c12a43ab 100644 --- a/apps/multichoice/pubspec.yaml +++ b/apps/multichoice/pubspec.yaml @@ -11,7 +11,9 @@ dependencies: auto_route: ^7.8.4 bloc: ^8.1.3 core: ^0.0.1 + firebase_analytics: ^10.10.5 firebase_core: ^2.27.2 + firebase_remote_config: ^4.4.5 flutter: sdk: flutter flutter_bloc: ^8.1.4 From 3a01e75d97158a2080130e124066bbb4a9fdd04a Mon Sep 17 00:00:00 2001 From: ZanderCowboy Date: Sat, 25 May 2024 23:52:32 +0200 Subject: [PATCH 02/13] wip add firebase remote config --- apps/multichoice/android/app/build.gradle | 9 ++- apps/multichoice/android/settings.gradle | 2 +- .../lib/app/enums/app_bar_title.dart | 6 ++ .../lib/presentation/home/home_page.dart | 52 +++++++------ lib/firebase_options.dart | 75 +++++++++++++++++++ .../src/application/export_application.dart | 1 + .../application/firebase/firebase_bloc.dart | 51 +++++++++++++ .../application/firebase/firebase_event.dart | 6 ++ .../application/firebase/firebase_state.dart | 12 +++ packages/core/pubspec.yaml | 3 + 10 files changed, 190 insertions(+), 27 deletions(-) create mode 100644 apps/multichoice/lib/app/enums/app_bar_title.dart create mode 100644 lib/firebase_options.dart create mode 100644 packages/core/lib/src/application/firebase/firebase_bloc.dart create mode 100644 packages/core/lib/src/application/firebase/firebase_event.dart create mode 100644 packages/core/lib/src/application/firebase/firebase_state.dart diff --git a/apps/multichoice/android/app/build.gradle b/apps/multichoice/android/app/build.gradle index dde67d8b..91057bd4 100644 --- a/apps/multichoice/android/app/build.gradle +++ b/apps/multichoice/android/app/build.gradle @@ -2,7 +2,7 @@ plugins { id "com.android.application" id "kotlin-android" id "dev.flutter.flutter-gradle-plugin" - // id 'com.google.gms.google-services' + // id "com.google.gms.google-services" } def localProperties = new Properties() @@ -49,7 +49,7 @@ android { defaultConfig { applicationId "co.za.zanderkotze.multichoice" - minSdkVersion flutter.minSdkVersion + minSdkVersion 21 targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName @@ -81,6 +81,7 @@ flutter { } dependencies { - // implementation platform('com.google.firebase:firebase-bom:32.7.3') - // implementation 'com.google.firebase:firebase-analytics' + implementation platform('com.google.firebase:firebase-bom:32.7.3') + implementation 'com.google.firebase:firebase-analytics' + implementation 'com.android.support:multidex:2.0.1' } diff --git a/apps/multichoice/android/settings.gradle b/apps/multichoice/android/settings.gradle index 7b0505be..b8a9c682 100644 --- a/apps/multichoice/android/settings.gradle +++ b/apps/multichoice/android/settings.gradle @@ -20,7 +20,7 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" id "com.android.application" version "7.4.2" apply false - id "org.jetbrains.kotlin.android" version "1.7.10" apply false + id "org.jetbrains.kotlin.android" version "1.9.24" apply false } include ":app" diff --git a/apps/multichoice/lib/app/enums/app_bar_title.dart b/apps/multichoice/lib/app/enums/app_bar_title.dart new file mode 100644 index 00000000..d3385b94 --- /dev/null +++ b/apps/multichoice/lib/app/enums/app_bar_title.dart @@ -0,0 +1,6 @@ +// ignore_for_file: constant_identifier_names + +enum AppBarTitle { + backup_appbar_title, + main_app_title, +} diff --git a/apps/multichoice/lib/presentation/home/home_page.dart b/apps/multichoice/lib/presentation/home/home_page.dart index 949b948c..676704bd 100644 --- a/apps/multichoice/lib/presentation/home/home_page.dart +++ b/apps/multichoice/lib/presentation/home/home_page.dart @@ -33,30 +33,38 @@ class HomePage extends StatelessWidget { ..add( const HomeEvent.onGetTabs(), ), - child: BlocBuilder( - builder: (context, state) { - return Scaffold( - appBar: AppBar( - title: const Text('Multichoice'), - actions: [ - IconButton( - onPressed: () { - ScaffoldMessenger.of(context) - ..clearSnackBars() - ..showSnackBar( - const SnackBar( - content: Text('Search has not been implemented yet.'), - ), - ); + child: BlocProvider( + create: (context) => coreSl(), + child: BlocBuilder( + builder: (context, state) { + return Scaffold( + appBar: AppBar( + title: BlocBuilder( + builder: (context, state) { + return Text(state.color); }, - icon: const Icon(Icons.search_outlined), ), - ], - ), - drawer: const _HomeDrawer(), - body: const _HomePage(), - ); - }, + actions: [ + IconButton( + onPressed: () { + ScaffoldMessenger.of(context) + ..clearSnackBars() + ..showSnackBar( + const SnackBar( + content: + Text('Search has not been implemented yet.'), + ), + ); + }, + icon: const Icon(Icons.search_outlined), + ), + ], + ), + drawer: const _HomeDrawer(), + body: const _HomePage(), + ); + }, + ), ), ); } diff --git a/lib/firebase_options.dart b/lib/firebase_options.dart new file mode 100644 index 00000000..0279deba --- /dev/null +++ b/lib/firebase_options.dart @@ -0,0 +1,75 @@ +// // File generated by FlutterFire CLI. +// // ignore_for_file: lines_longer_than_80_chars, avoid_classes_with_only_static_members +// 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: +// 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 web = FirebaseOptions( +// apiKey: 'AIzaSyA3_VXMz5bCPhDzQ4psfOHl5mEZt8s_w-4', +// appId: '1:82796040762:web:15fecbf2203c038ee4db03', +// messagingSenderId: '82796040762', +// projectId: 'multichoice-412309', +// authDomain: 'multichoice-412309.firebaseapp.com', +// storageBucket: 'multichoice-412309.appspot.com', +// measurementId: 'G-RKRDGDJMDK', +// ); + +// static const FirebaseOptions android = FirebaseOptions( +// apiKey: 'AIzaSyA9oQ4EKnvNdGnh_cVRoTcQ62tNij9YNSY', +// appId: '1:82796040762:android:7d86369ea3e82786e4db03', +// messagingSenderId: '82796040762', +// projectId: 'multichoice-412309', +// storageBucket: 'multichoice-412309.appspot.com', +// ); + +// static const FirebaseOptions ios = FirebaseOptions( +// apiKey: 'AIzaSyCbM12acc3yLQbmEHxxqMij_EVJB9FJN8E', +// appId: '1:82796040762:ios:60f8a21e5479962fe4db03', +// messagingSenderId: '82796040762', +// projectId: 'multichoice-412309', +// storageBucket: 'multichoice-412309.appspot.com', +// iosBundleId: 'co.za.zanderkotze.multichoice', +// ); +// } diff --git a/packages/core/lib/src/application/export_application.dart b/packages/core/lib/src/application/export_application.dart index ada040c5..f0bc66da 100644 --- a/packages/core/lib/src/application/export_application.dart +++ b/packages/core/lib/src/application/export_application.dart @@ -1 +1,2 @@ +export 'firebase/firebase_bloc.dart'; export 'home/home_bloc.dart'; diff --git a/packages/core/lib/src/application/firebase/firebase_bloc.dart b/packages/core/lib/src/application/firebase/firebase_bloc.dart new file mode 100644 index 00000000..17bb9619 --- /dev/null +++ b/packages/core/lib/src/application/firebase/firebase_bloc.dart @@ -0,0 +1,51 @@ +import 'package:bloc/bloc.dart'; +import 'package:firebase_remote_config/firebase_remote_config.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:injectable/injectable.dart'; + +part 'firebase_bloc.freezed.dart'; +part 'firebase_event.dart'; +part 'firebase_state.dart'; + +enum AppBarTitle { + backup_appbar_title, + main_app_title, +} + +@Injectable() +class FirebaseBloc extends Bloc { + FirebaseBloc() : super(FirebaseState.initial()) { + on((event, emit) { + event.map( + onChangeColor: (e) { + final FirebaseRemoteConfig? remoteConfig = + FirebaseRemoteConfig.instance; + + String titleKey; + switch (e.color) { + case AppBarTitle.backup_appbar_title: + titleKey = 'backup_appbar_title'; + break; + case AppBarTitle.main_app_title: + titleKey = 'main_app_title'; + break; + } + + final titleName = remoteConfig?.getString(titleKey); + final title = availableBackgroundColors[titleName] ?? 'Default'; + + emit( + state.copyWith( + color: title, + ), + ); + }, + ); + }); + } + + final Map availableBackgroundColors = { + "main": 'Multichoice', + "backup": 'Keep It Together', + }; +} diff --git a/packages/core/lib/src/application/firebase/firebase_event.dart b/packages/core/lib/src/application/firebase/firebase_event.dart new file mode 100644 index 00000000..83fefd04 --- /dev/null +++ b/packages/core/lib/src/application/firebase/firebase_event.dart @@ -0,0 +1,6 @@ +part of 'firebase_bloc.dart'; + +@freezed +class FirebaseEvent with _$FirebaseEvent { + const factory FirebaseEvent.onChangeColor(AppBarTitle color) = OnChangeColor; +} diff --git a/packages/core/lib/src/application/firebase/firebase_state.dart b/packages/core/lib/src/application/firebase/firebase_state.dart new file mode 100644 index 00000000..fa26d558 --- /dev/null +++ b/packages/core/lib/src/application/firebase/firebase_state.dart @@ -0,0 +1,12 @@ +part of 'firebase_bloc.dart'; + +@freezed +class FirebaseState with _$FirebaseState { + const factory FirebaseState({ + required String color, + }) = _FirebaseState; + + factory FirebaseState.initial() => FirebaseState( + color: '', + ); +} diff --git a/packages/core/pubspec.yaml b/packages/core/pubspec.yaml index 45016750..c2972020 100644 --- a/packages/core/pubspec.yaml +++ b/packages/core/pubspec.yaml @@ -13,6 +13,9 @@ dependencies: sdk: flutter flutter_bloc: ^8.1.4 freezed_annotation: ^2.4.1 + firebase_analytics: ^10.10.5 + firebase_core: ^2.27.2 + firebase_remote_config: ^4.4.5 get_it: ^7.6.4 injectable: ^2.3.2 isar: ^3.1.0+1 From d7f7bffaa29289a6580250f833b482d337f57857 Mon Sep 17 00:00:00 2001 From: Zander Kotze Date: Wed, 4 Feb 2026 21:56:07 +0200 Subject: [PATCH 03/13] chore: linting and package updates --- .gitignore | 1 + apps/multichoice/lib/app/enums/app_bar_title.dart | 2 +- apps/multichoice/pubspec.yaml | 6 +++--- .../core/lib/src/application/firebase/firebase_event.dart | 2 +- .../core/lib/src/application/firebase/firebase_state.dart | 6 +++--- packages/core/pubspec.yaml | 6 +++--- 6 files changed, 12 insertions(+), 11 deletions(-) diff --git a/.gitignore b/.gitignore index a5de5c5c..57367f25 100644 --- a/.gitignore +++ b/.gitignore @@ -80,6 +80,7 @@ upload-keystore.* .svn/ migrate_working_dir/ devtools_options.yaml +**/node_modules/ # IntelliJ related *.iml diff --git a/apps/multichoice/lib/app/enums/app_bar_title.dart b/apps/multichoice/lib/app/enums/app_bar_title.dart index d3385b94..ffb7c5ee 100644 --- a/apps/multichoice/lib/app/enums/app_bar_title.dart +++ b/apps/multichoice/lib/app/enums/app_bar_title.dart @@ -1,4 +1,4 @@ -// ignore_for_file: constant_identifier_names +// ignore_for_file: constant_identifier_names, document_ignores enum AppBarTitle { backup_appbar_title, diff --git a/apps/multichoice/pubspec.yaml b/apps/multichoice/pubspec.yaml index c9a408b8..f7a4d5eb 100644 --- a/apps/multichoice/pubspec.yaml +++ b/apps/multichoice/pubspec.yaml @@ -8,13 +8,13 @@ environment: sdk: ">=3.10.8 <4.0.0" dependencies: - auto_route: ^10.0.1 + auto_route: ^11.1.0 bloc: ^9.0.0 core: ^0.0.1 file_picker: ^10.1.9 - firebase_analytics: ^10.10.5 + firebase_analytics: ^12.1.1 firebase_core: ^4.4.0 - firebase_remote_config: ^4.4.5 + firebase_remote_config: ^6.1.4 flutter: sdk: flutter flutter_bloc: ^9.1.0 diff --git a/packages/core/lib/src/application/firebase/firebase_event.dart b/packages/core/lib/src/application/firebase/firebase_event.dart index 83fefd04..911ff953 100644 --- a/packages/core/lib/src/application/firebase/firebase_event.dart +++ b/packages/core/lib/src/application/firebase/firebase_event.dart @@ -1,6 +1,6 @@ part of 'firebase_bloc.dart'; @freezed -class FirebaseEvent with _$FirebaseEvent { +abstract class FirebaseEvent with _$FirebaseEvent { const factory FirebaseEvent.onChangeColor(AppBarTitle color) = OnChangeColor; } diff --git a/packages/core/lib/src/application/firebase/firebase_state.dart b/packages/core/lib/src/application/firebase/firebase_state.dart index fa26d558..80bec2fc 100644 --- a/packages/core/lib/src/application/firebase/firebase_state.dart +++ b/packages/core/lib/src/application/firebase/firebase_state.dart @@ -1,12 +1,12 @@ part of 'firebase_bloc.dart'; @freezed -class FirebaseState with _$FirebaseState { +abstract class FirebaseState with _$FirebaseState { const factory FirebaseState({ required String color, }) = _FirebaseState; factory FirebaseState.initial() => FirebaseState( - color: '', - ); + color: '', + ); } diff --git a/packages/core/pubspec.yaml b/packages/core/pubspec.yaml index 9d908558..545d62e6 100644 --- a/packages/core/pubspec.yaml +++ b/packages/core/pubspec.yaml @@ -18,9 +18,9 @@ dependencies: sdk: flutter flutter_bloc: ^9.1.0 freezed_annotation: ^3.1.0 - firebase_analytics: ^10.10.5 - firebase_core: ^2.27.2 - firebase_remote_config: ^4.4.5 + firebase_analytics: ^12.1.1 + firebase_core: ^4.4.0 + firebase_remote_config: ^6.1.4 get_it: ^8.0.3 injectable: ^2.3.2 isar_community: ^3.3.0 From d50592e68e482cc267e3885e3ac941c5e4e64b97 Mon Sep 17 00:00:00 2001 From: Zander Kotze Date: Wed, 4 Feb 2026 22:10:28 +0200 Subject: [PATCH 04/13] chore: remove app_bar_title --- apps/multichoice/lib/app/enums/app_bar_title.dart | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 apps/multichoice/lib/app/enums/app_bar_title.dart diff --git a/apps/multichoice/lib/app/enums/app_bar_title.dart b/apps/multichoice/lib/app/enums/app_bar_title.dart deleted file mode 100644 index ffb7c5ee..00000000 --- a/apps/multichoice/lib/app/enums/app_bar_title.dart +++ /dev/null @@ -1,6 +0,0 @@ -// ignore_for_file: constant_identifier_names, document_ignores - -enum AppBarTitle { - backup_appbar_title, - main_app_title, -} From 9faf433980021161ecd32e93ff379209913cd081 Mon Sep 17 00:00:00 2001 From: Zander Kotze Date: Fri, 6 Feb 2026 01:32:30 +0200 Subject: [PATCH 05/13] feat: firebase_service --- packages/core/lib/src/services/export.dart | 1 + .../implementations/firebase_service.dart | 109 ++++++++++++++++++ .../interfaces/i_firebase_service.dart | 32 +++++ 3 files changed, 142 insertions(+) create mode 100644 packages/core/lib/src/services/implementations/firebase_service.dart create mode 100644 packages/core/lib/src/services/interfaces/i_firebase_service.dart diff --git a/packages/core/lib/src/services/export.dart b/packages/core/lib/src/services/export.dart index 4cdc213f..223d9321 100644 --- a/packages/core/lib/src/services/export.dart +++ b/packages/core/lib/src/services/export.dart @@ -1,3 +1,4 @@ export 'interfaces/i_app_info_service.dart'; export 'interfaces/i_app_storage_service.dart'; export 'interfaces/i_data_exchange_service.dart'; +export 'interfaces/i_firebase_service.dart'; diff --git a/packages/core/lib/src/services/implementations/firebase_service.dart b/packages/core/lib/src/services/implementations/firebase_service.dart new file mode 100644 index 00000000..d023de1e --- /dev/null +++ b/packages/core/lib/src/services/implementations/firebase_service.dart @@ -0,0 +1,109 @@ +import 'dart:convert'; +import 'dart:developer'; + +import 'package:core/src/services/interfaces/i_firebase_service.dart'; +import 'package:firebase_remote_config/firebase_remote_config.dart'; +import 'package:injectable/injectable.dart'; +import 'package:models/models.dart'; + +@LazySingleton(as: IFirebaseService) +class FirebaseService implements IFirebaseService { + FirebaseService() { + _remoteConfig = FirebaseRemoteConfig.instance; + } + + late final FirebaseRemoteConfig _remoteConfig; + bool _isInitialized = false; + + @override + Future initialize() async { + if (_isInitialized) { + return; + } + + try { + await _remoteConfig.setConfigSettings( + RemoteConfigSettings( + fetchTimeout: const Duration(seconds: 10), + minimumFetchInterval: const Duration(hours: 1), + ), + ); + + // Set default values if needed + await _remoteConfig.setDefaults({}); + + _isInitialized = true; + } catch (e) { + log('Error initializing Firebase Remote Config: $e'); + rethrow; + } + } + + @override + Future fetchAndActivate() async { + if (!_isInitialized) { + await initialize(); + } + + try { + await _remoteConfig.fetchAndActivate(); + } catch (e) { + log('Error fetching and activating Remote Config: $e'); + rethrow; + } + } + + @override + Future getConfig( + FirebaseConfigKeys key, + T Function(Map) fromJson, + ) async { + if (!_isInitialized) { + await initialize(); + } + + try { + final jsonString = _remoteConfig.getString(key.key); + + if (jsonString.isEmpty) { + log('Config key "${key.key}" not found or empty'); + return null; + } + + final jsonMap = jsonDecode(jsonString) as Map; + return fromJson(jsonMap); + } catch (e) { + log('Error parsing config for key "${key.key}": $e'); + return null; + } + } + + @override + bool isEnabled(FirebaseConfigKeys key) { + if (!_isInitialized) { + log('FirebaseService not initialized. Call initialize() first.'); + return false; + } + + try { + return _remoteConfig.getBool(key.key); + } catch (e) { + log('Error getting feature flag for key "${key.key}": $e'); + return false; + } + } + + @override + Future getString(FirebaseConfigKeys key) async { + if (!_isInitialized) { + await initialize(); + } + + try { + return _remoteConfig.getString(key.key); + } catch (e) { + log('Error getting string for key "${key.key}": $e'); + return null; + } + } +} diff --git a/packages/core/lib/src/services/interfaces/i_firebase_service.dart b/packages/core/lib/src/services/interfaces/i_firebase_service.dart new file mode 100644 index 00000000..1c00c604 --- /dev/null +++ b/packages/core/lib/src/services/interfaces/i_firebase_service.dart @@ -0,0 +1,32 @@ +import 'package:models/models.dart'; + +abstract class IFirebaseService { + /// Initialize Firebase Remote Config with default settings + Future initialize(); + + /// Fetch and activate the latest config from Firebase + Future fetchAndActivate(); + + /// Get a JSON config value and parse it as a model object + /// Returns null if the config doesn't exist or parsing fails + /// + /// Example: + /// ```dart + /// final config = await service.getConfig( + /// FirebaseConfigKeys.appConfig, + /// (json) => AppConfig.fromJson(json), + /// ); + /// ``` + Future getConfig( + FirebaseConfigKeys key, + T Function(Map) fromJson, + ); + + /// Check if a feature flag is enabled + /// Returns false if the config doesn't exist or is not a boolean + bool isEnabled(FirebaseConfigKeys key); + + /// Get a string config value + /// Returns null if the config doesn't exist or is not a string + Future getString(FirebaseConfigKeys key); +} From 564df3cf0ac07f59c00bf4c007c3e332d9ca55f8 Mon Sep 17 00:00:00 2001 From: Zander Kotze Date: Fri, 6 Feb 2026 01:32:49 +0200 Subject: [PATCH 06/13] chore: firebase_config_keys enum --- packages/models/lib/src/enums/export.dart | 1 + .../src/enums/firebase/firebase_config_keys.dart | 15 +++++++++++++++ 2 files changed, 16 insertions(+) create mode 100644 packages/models/lib/src/enums/firebase/firebase_config_keys.dart diff --git a/packages/models/lib/src/enums/export.dart b/packages/models/lib/src/enums/export.dart index ba1ff637..d43841ec 100644 --- a/packages/models/lib/src/enums/export.dart +++ b/packages/models/lib/src/enums/export.dart @@ -1,4 +1,5 @@ export 'feedback/feedback_field.dart'; +export 'firebase/firebase_config_keys.dart'; export 'menu/menu_items.dart'; export 'product_tour/product_tour_step.dart'; export 'storage/storage_keys.dart'; diff --git a/packages/models/lib/src/enums/firebase/firebase_config_keys.dart b/packages/models/lib/src/enums/firebase/firebase_config_keys.dart new file mode 100644 index 00000000..48a19e69 --- /dev/null +++ b/packages/models/lib/src/enums/firebase/firebase_config_keys.dart @@ -0,0 +1,15 @@ +enum FirebaseConfigKeys { + // Feature flags (bools) + // example: enableNewFeature('enable_new_feature'), + + // JSON configs + // example: appConfig('app_config'), + + // Strings + welcomeMessage('welcome_message') + ; + + const FirebaseConfigKeys(this.key); + + final String key; +} From 18a922b0cc5ec82b0abb41be6b5d9e5bdb94f747 Mon Sep 17 00:00:00 2001 From: Zander Kotze Date: Fri, 6 Feb 2026 01:33:01 +0200 Subject: [PATCH 07/13] chore: update bootstrap --- apps/multichoice/lib/bootstrap.dart | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/apps/multichoice/lib/bootstrap.dart b/apps/multichoice/lib/bootstrap.dart index 401082f5..8fc16f73 100644 --- a/apps/multichoice/lib/bootstrap.dart +++ b/apps/multichoice/lib/bootstrap.dart @@ -45,4 +45,14 @@ Future bootstrap() async { await Firebase.initializeApp( options: DefaultFirebaseOptions.currentPlatform, ); + + // Initialize Firebase Remote Config service + try { + final firebaseService = coreSl(); + await firebaseService.initialize(); + await firebaseService.fetchAndActivate(); + } catch (e) { + log('Error initializing Firebase Remote Config: $e'); + // Continue app startup even if Remote Config fails + } } From b4bf8ad3e2876df99714c49c298ca695876c59a0 Mon Sep 17 00:00:00 2001 From: Zander Kotze Date: Fri, 6 Feb 2026 01:34:16 +0200 Subject: [PATCH 08/13] chore: add example cursor commands and plans --- .cursor/commands/create-pr.md | 0 .cursor/commands/update-changelog.md | 0 .cursor/plans/example-feature-plan.mdc | 46 ++++++++++++++++++++++ .cursor/plans/example-refactoring-plan.mdc | 29 ++++++++++++++ 4 files changed, 75 insertions(+) create mode 100644 .cursor/commands/create-pr.md create mode 100644 .cursor/commands/update-changelog.md create mode 100644 .cursor/plans/example-feature-plan.mdc create mode 100644 .cursor/plans/example-refactoring-plan.mdc diff --git a/.cursor/commands/create-pr.md b/.cursor/commands/create-pr.md new file mode 100644 index 00000000..e69de29b diff --git a/.cursor/commands/update-changelog.md b/.cursor/commands/update-changelog.md new file mode 100644 index 00000000..e69de29b diff --git a/.cursor/plans/example-feature-plan.mdc b/.cursor/plans/example-feature-plan.mdc new file mode 100644 index 00000000..43b10487 --- /dev/null +++ b/.cursor/plans/example-feature-plan.mdc @@ -0,0 +1,46 @@ +--- +title: Example Feature Plan +status: planning +priority: high +tags: [feature, ui, backend] +--- + +# Example Feature Plan + +This is an example of how to structure a feature plan in the `.cursor/plans/` directory. + +## Overview +Brief description of what this feature will accomplish. + +## Goals +- Primary goal 1 +- Primary goal 2 +- Secondary goal 1 + +## Requirements +1. Requirement 1 +2. Requirement 2 +3. Requirement 3 + +## Implementation Steps +- [ ] Step 1: Setup and preparation +- [ ] Step 2: Core implementation +- [ ] Step 3: Testing +- [ ] Step 4: Documentation +- [ ] Step 5: Review and merge + +## Technical Considerations +- Consideration 1 +- Consideration 2 + +## Dependencies +- Package/feature dependency 1 +- Package/feature dependency 2 + +## Testing Strategy +- Unit tests for business logic +- Widget tests for UI components +- Integration tests for user flows + +## Notes +Additional notes, questions, or concerns about this feature. diff --git a/.cursor/plans/example-refactoring-plan.mdc b/.cursor/plans/example-refactoring-plan.mdc new file mode 100644 index 00000000..571fdd68 --- /dev/null +++ b/.cursor/plans/example-refactoring-plan.mdc @@ -0,0 +1,29 @@ +--- +title: Example Refactoring Plan +status: in-progress +priority: medium +tags: [refactoring, technical-debt] +--- + +# Example Refactoring Plan + +This demonstrates how to plan a refactoring effort. + +## Current State +Description of the current implementation and its issues. + +## Target State +Description of the desired state after refactoring. + +## Risks +- Risk 1 and mitigation strategy +- Risk 2 and mitigation strategy + +## Migration Strategy +1. Phase 1: Preparation +2. Phase 2: Incremental changes +3. Phase 3: Cleanup + +## Success Criteria +- Criterion 1 +- Criterion 2 From 49e7d78b76d751d439332438bb7ed90011aa5be7 Mon Sep 17 00:00:00 2001 From: Zander Kotze Date: Fri, 6 Feb 2026 01:34:24 +0200 Subject: [PATCH 09/13] docs: changelog --- CHANGELOG.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 44553dfd..93bb4bfa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ -#213 Implement Firebase Performance Monitoring +#118 - Add Remote Config to Codebase -- Add package `firebase_performance` to pubspec +- Add `FirebaseService` in core package for Firebase Remote Config management +- Add `FirebaseConfigKeys` enum in models package for type-safe config keys +- Support automatic JSON to model conversion via `getConfig()` +- Support feature flags via `isEnabled()` +- Support strings via `getString()` +- Auto-initialize service in bootstrap \ No newline at end of file From 986e036e5f536ecccd5458cf33238ef2b4db388f Mon Sep 17 00:00:00 2001 From: Zander Kotze Date: Fri, 6 Feb 2026 01:34:46 +0200 Subject: [PATCH 10/13] chore: .cursor files - examples only --- .cursor/README.md | 198 ++++++++++++++++++ .cursor/rules/example-always-apply.mdc | 27 +++ .cursor/rules/example-api-rules.mdc | 36 ++++ .cursor/rules/example-complex-globs.mdc | 46 ++++ .cursor/rules/example-conditional-rules.mdc | 26 +++ .cursor/rules/example-reusable-patterns.mdc | 138 ++++++++++++ .cursor/rules/example-shortcuts.mdc | 42 ++++ .cursor/rules/example-testing-rules.mdc | 39 ++++ .cursor/rules/example-ui-rules.mdc | 47 +++++ .cursor/skills/example-skill/SKILL.md | 0 .../example-freezed-model-template.dart | 20 ++ .cursor/templates/example-page-template.dart | 27 +++ .../templates/example-service-template.dart | 32 +++ .cursor/templates/example-snippets.json | 161 ++++++++++++++ .cursor/templates/example-test-template.dart | 28 +++ 15 files changed, 867 insertions(+) create mode 100644 .cursor/README.md create mode 100644 .cursor/rules/example-always-apply.mdc create mode 100644 .cursor/rules/example-api-rules.mdc create mode 100644 .cursor/rules/example-complex-globs.mdc create mode 100644 .cursor/rules/example-conditional-rules.mdc create mode 100644 .cursor/rules/example-reusable-patterns.mdc create mode 100644 .cursor/rules/example-shortcuts.mdc create mode 100644 .cursor/rules/example-testing-rules.mdc create mode 100644 .cursor/rules/example-ui-rules.mdc create mode 100644 .cursor/skills/example-skill/SKILL.md create mode 100644 .cursor/templates/example-freezed-model-template.dart create mode 100644 .cursor/templates/example-page-template.dart create mode 100644 .cursor/templates/example-service-template.dart create mode 100644 .cursor/templates/example-snippets.json create mode 100644 .cursor/templates/example-test-template.dart diff --git a/.cursor/README.md b/.cursor/README.md new file mode 100644 index 00000000..af5f223f --- /dev/null +++ b/.cursor/README.md @@ -0,0 +1,198 @@ +# .cursor Folder Guide + +This directory contains rules and plans that help Cursor AI understand your project better and provide more accurate assistance. + +## Directory Structure + +``` +.cursor/ +├── rules/ # AI assistant rules and guidelines +├── plans/ # Project plans and feature documentation +├── templates/ # Code templates for reuse +└── README.md # This file +``` + +## Rules Directory (`rules/`) + +Rules are markdown files (`.mdc`) that guide the AI assistant's behavior. Each rule file has frontmatter metadata: + +### Frontmatter Options + +```yaml +--- +description: Brief description of what this rule covers +globs: ["**/*.dart"] # File patterns this rule applies to +alwaysApply: false # Whether to always apply this rule +--- +``` + +### Rule File Examples + +1. **Language-Specific Rules** (`example-language-specific.mdc`) + - Apply to specific file types using glob patterns + - Example: Dart-specific conventions + +2. **Always Apply Rules** (`example-always-apply.mdc`) + - Rules that apply to every conversation + - Use `alwaysApply: true` + - Good for project-wide conventions + +3. **Conditional Rules** (`example-conditional-rules.mdc`) + - Apply to specific directories or patterns + - Example: Core package rules, feature module rules + +4. **Testing Rules** (`example-testing-rules.mdc`) + - Rules for test files + - Testing conventions and best practices + +5. **API/Service Rules** (`example-api-rules.mdc`) + - Rules for service layer code + - Interface/implementation patterns + +6. **UI Rules** (`example-ui-rules.mdc`) + - Widget and UI development guidelines + - Page structure conventions + +7. **Complex Globs** (`example-complex-globs.mdc`) + - Advanced glob pattern examples + - Inclusion and exclusion patterns + +8. **Shortcuts** (`example-shortcuts.mdc`) + - Quick reference for common tasks + - Build commands, testing commands, etc. + +9. **Reusable Patterns** (`example-reusable-patterns.mdc`) + - Common code patterns the AI can reference + - Service, page, test, repository patterns + +### Glob Pattern Examples + +```yaml +# Single pattern +globs: ["**/*.dart"] + +# Multiple patterns +globs: ["**/*.dart", "**/*.ts"] + +# Directory-specific +globs: ["packages/core/**"] + +# Exclude patterns (use !) +globs: + - "**/*.dart" + - "!**/*.g.dart" # Exclude generated files + - "!**/test/**" # Exclude test directories + +# Multiple file types +globs: ["**/*.{dart,ts,js}"] +``` + +## Plans Directory (`plans/`) + +Plans are markdown files that document features, refactoring efforts, or project goals. + +### Plan File Examples + +1. **Feature Plan** (`example-feature-plan.mdc`) + - Structure for planning new features + - Includes goals, requirements, implementation steps + +2. **Refactoring Plan** (`example-refactoring-plan.mdc`) + - Structure for refactoring efforts + - Includes current state, target state, migration strategy + +### Plan Frontmatter + +```yaml +--- +title: Plan Title +status: planning | in-progress | completed | cancelled +priority: low | medium | high +tags: [tag1, tag2, tag3] +--- +``` + +## Templates Directory (`templates/`) + +Template files are complete code files you can copy and customize. + +### Available Templates + +1. **Service Template** (`example-service-template.dart`) + - Service interface + implementation structure + - Includes dependency injection setup + +2. **Page Template** (`example-page-template.dart`) + - Flutter page with Scaffold structure + - Follows project conventions + +3. **Test Template** (`example-test-template.dart`) + - Test file structure with setup/teardown + - Arrange-Act-Assert pattern + +4. **Freezed Model Template** (`example-freezed-model-template.dart`) + - Freezed model with JSON serialization + - Ready for code generation + +5. **Snippets Examples** (`example-snippets.json`) + - Example snippets for `.vscode/snippets.code-snippets` + - Service, page, widget, model snippets + +## Best Practices + +1. **Keep Rules Focused** + - One rule file per concern or domain + - Don't mix unrelated guidelines + +2. **Use Descriptive Names** + - Name files clearly: `testing-rules.mdc`, `ui-guidelines.mdc` + - Use kebab-case for file names + +3. **Organize by Domain** + - Group related rules together + - Use subdirectories if needed (e.g., `rules/ui/`, `rules/testing/`) + +4. **Keep Plans Updated** + - Update plan status as work progresses + - Add notes and learnings + +5. **Use Always Apply Sparingly** + - Only use `alwaysApply: true` for critical, universal rules + - Most rules should be context-specific + +## Creating Your Own Rules + +1. Create a new `.mdc` file in `rules/` +2. Add frontmatter with appropriate metadata +3. Write clear, actionable guidelines +4. Use code examples when helpful +5. Reference existing patterns in your codebase + +## Creating Your Own Plans + +1. Create a new `.mdc` file in `plans/` +2. Add frontmatter with title, status, priority, tags +3. Document the plan following the examples +4. Update status as work progresses + +## Reusable Content + +For information on creating reusable code snippets, templates, and patterns, see: +- **[REUSABLE-CONTENT-GUIDE.md](REUSABLE-CONTENT-GUIDE.md)** - Complete guide on all reusable content options + +### Quick Summary + +1. **Code Snippets** (`.vscode/snippets.code-snippets`) - Fast code insertion +2. **Templates** (`.cursor/templates/`) - Complete file templates +3. **Reusable Patterns** (`.cursor/rules/example-reusable-patterns.mdc`) - AI-referenced patterns +4. **Example Snippets** (`.cursor/templates/example-snippets.json`) - More snippet examples + +## Tips + +- Start with a few key rules and expand as needed +- Review and update rules regularly +- Remove outdated or conflicting rules +- Use plans to track complex features or refactoring +- Reference existing code patterns in your rules +- Use snippets for frequently used code blocks +- Use templates for complete file structures diff --git a/.cursor/rules/example-always-apply.mdc b/.cursor/rules/example-always-apply.mdc new file mode 100644 index 00000000..2a2d2353 --- /dev/null +++ b/.cursor/rules/example-always-apply.mdc @@ -0,0 +1,27 @@ +--- +description: Rules that always apply to all conversations +globs: +alwaysApply: true +--- + +# Always Apply Rules Example + +This rule file demonstrates how to create rules that **always apply** to every conversation, regardless of context. + +## Usage +- `alwaysApply: true` - Makes these rules active in every conversation +- `globs` can be empty when using `alwaysApply: true` +- Use this for project-wide conventions and critical guidelines + +## Project-Wide Rules +1. **Never commit secrets or API keys** - Always use environment variables +2. **Write tests for new features** - Maintain at least 80% coverage +3. **Follow conventional commits** - Use format: `type(scope): message` +4. **Run linter before committing** - Fix all warnings and errors +5. **Document public APIs** - Add doc comments for exported functions/classes + +## Code Quality Standards +- All code must pass static analysis +- All tests must pass before merging +- Code reviews are required for all PRs +- Keep functions under 50 lines when possible diff --git a/.cursor/rules/example-api-rules.mdc b/.cursor/rules/example-api-rules.mdc new file mode 100644 index 00000000..fff08009 --- /dev/null +++ b/.cursor/rules/example-api-rules.mdc @@ -0,0 +1,36 @@ +--- +description: API and service layer conventions +globs: ["**/services/**", "**/api/**", "**/repositories/**"] +alwaysApply: false +--- + +# API and Service Rules Example + +This rule file demonstrates rules for API and service layer code. + +## Service Layer Conventions + +### Interface Requirements +- Every service implementation must have an interface +- Interface files: `i__service.dart` +- Implementation files: `_service.dart` +- Interfaces in `interfaces/` directory +- Implementations in `implementations/` directory + +### Error Handling +- Always handle network errors gracefully +- Return `Result` or `Either` types +- Log errors appropriately +- Never expose internal error details to UI + +### Async Operations +- Use `Future` for async operations +- Always handle timeouts +- Use proper cancellation tokens +- Document expected response times + +### Testing Services +- Mock external dependencies +- Test error scenarios +- Test timeout handling +- Verify proper error propagation diff --git a/.cursor/rules/example-complex-globs.mdc b/.cursor/rules/example-complex-globs.mdc new file mode 100644 index 00000000..25c91263 --- /dev/null +++ b/.cursor/rules/example-complex-globs.mdc @@ -0,0 +1,46 @@ +--- +description: Example of complex glob patterns +globs: + - "packages/**/*.dart" + - "apps/**/lib/features/**/*.dart" + - "!**/*.g.dart" + - "!**/*.freezed.dart" + - "!**/*.mocks.dart" +alwaysApply: false +--- + +# Complex Glob Patterns Example + +This demonstrates advanced glob pattern usage. + +## Glob Pattern Features + +### Inclusion Patterns +- `packages/**/*.dart` - All Dart files in packages directory +- `apps/**/lib/features/**/*.dart` - All Dart files in feature directories + +### Exclusion Patterns (using `!`) +- `!**/*.g.dart` - Exclude generated files +- `!**/*.freezed.dart` - Exclude Freezed generated files +- `!**/*.mocks.dart` - Exclude mock files + +## Use Cases +- Apply rules to source files but not generated files +- Target specific directory structures +- Exclude test files or build artifacts +- Create rules for specific package types + +## Pattern Examples +```yaml +globs: + # Include patterns + - "**/*.dart" # All Dart files + - "packages/core/**" # Everything in core package + - "apps/**/lib/**" # All lib directories in apps + - "**/*.{dart,ts,js}" # Multiple file extensions + + # Exclude patterns (must come after includes) + - "!**/test/**" # Exclude test directories + - "!**/*.g.dart" # Exclude generated files + - "!**/node_modules/**" # Exclude dependencies +``` diff --git a/.cursor/rules/example-conditional-rules.mdc b/.cursor/rules/example-conditional-rules.mdc new file mode 100644 index 00000000..87fcdde0 --- /dev/null +++ b/.cursor/rules/example-conditional-rules.mdc @@ -0,0 +1,26 @@ +--- +description: Rules that apply to specific directories or file patterns +globs: ["packages/core/**", "apps/multichoice/lib/features/**"] +alwaysApply: false +--- + +# Conditional Rules Example + +This rule file demonstrates how to create rules that apply only to specific directories or file patterns. + +## Usage +- `globs`: Can target specific directories or file patterns +- This example applies to core packages and feature directories +- Useful for domain-specific conventions + +## Core Package Rules +- All services must have corresponding interfaces +- Interfaces go in `interfaces/` directory +- Implementations go in `implementations/` directory +- Use dependency injection for all services + +## Feature Module Rules +- Each feature should be self-contained +- Follow the data/domain/presentation structure +- Use barrel exports (`export.dart`) for public API +- Keep feature dependencies minimal diff --git a/.cursor/rules/example-reusable-patterns.mdc b/.cursor/rules/example-reusable-patterns.mdc new file mode 100644 index 00000000..30ec3808 --- /dev/null +++ b/.cursor/rules/example-reusable-patterns.mdc @@ -0,0 +1,138 @@ +--- +description: Reusable code patterns and templates that can be referenced +globs: +alwaysApply: true +--- + +# Reusable Patterns and Templates + +This rule file documents reusable patterns that can be referenced when creating new code. + +## Service Pattern + +When creating a new service, use this pattern: + +### Interface File (`interfaces/i__service.dart`) +```dart +abstract class IService { + Future> methodName(); +} +``` + +### Implementation File (`implementations/_service.dart`) +```dart +@injectable +class Service implements IService { + @override + Future> methodName() { + // Implementation + } +} +``` + +## Page Pattern + +When creating a new page, use this structure: + +```dart +class Page extends StatelessWidget { + const Page({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text(''), + ), + body: _Page(), + ); + } +} + +class _Page extends StatelessWidget { + @override + Widget build(BuildContext context) { + return const Placeholder(); + } +} +``` + +## Test Pattern + +When creating a new test file, use this structure: + +```dart +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('', () { + test('should ', () async { + // Arrange + // Act + // Assert + }); + }); +} +``` + +## Repository Pattern + +When creating a new repository: + +```dart +abstract class IRepository { + Future>>> getAll(); + Future>> getById(String id); + Future>> create( entity); + Future>> update( entity); + Future> delete(String id); +} +``` + +## Freezed Model Pattern + +When creating a new Freezed model: + +```dart +import 'package:freezed_annotation/freezed_annotation.dart'; + +part '.freezed.dart'; +part '.g.dart'; + +@freezed +class with _$ { + const factory ({ + required String id, + // Add fields here + }) = _; + + factory .fromJson(Map json) => + _$FromJson(json); +} +``` + +## Widget Pattern + +When creating a reusable widget: + +```dart +class extends StatelessWidget { + const ({ + super.key, + // Add parameters + }); + + @override + Widget build(BuildContext context) { + return const Placeholder(); + } +} +``` + +## Usage Instructions + +When creating new code: +1. Find the appropriate pattern above +2. Copy and adapt it to your needs +3. Follow the naming conventions +4. Ensure proper imports and dependencies diff --git a/.cursor/rules/example-shortcuts.mdc b/.cursor/rules/example-shortcuts.mdc new file mode 100644 index 00000000..dbe88a95 --- /dev/null +++ b/.cursor/rules/example-shortcuts.mdc @@ -0,0 +1,42 @@ +--- +description: Common shortcuts and quick reference +globs: +alwaysApply: true +--- + +# Quick Reference Shortcuts + +This file provides quick reference for common tasks and shortcuts. + +## Build Commands +- `make db` - Run build_runner for code generation +- `make fb` - Flutter build with code generation +- `make frb` - Full Flutter rebuild (clean + code generation) +- `make clean` - Clean all generated files +- `make mr` - Melos rebuild all packages + +## Testing Commands +- `melos test:all` - Run all tests +- `melos test:core` - Run core package tests +- `melos test:multichoice` - Run main app tests +- `melos coverage:all` - Generate coverage reports + +## Code Generation +- When mocks are needed: Check for `mocks.dart` first, then use `melos` for build_runner +- Generated file patterns: + - `*.g.dart` - General generated code + - `*.freezed.dart` - Freezed models + - `*.mocks.dart` - Test mocks + - `*.auto_mappr.dart` - Object mapping + +## File Organization +- Services: `interfaces/` and `implementations/` directories +- Constants: Check `/constants` folder first +- Tests: Mirror source structure, use `mocks.dart` when available +- Exports: Use `export.dart` for barrel files + +## Quick Tips +- Use `const` constructors when possible +- Check existing constants before creating new ones +- Use `part`/`part of` for modular widget classes +- Keep `part` sections alphabetical diff --git a/.cursor/rules/example-testing-rules.mdc b/.cursor/rules/example-testing-rules.mdc new file mode 100644 index 00000000..73527f4a --- /dev/null +++ b/.cursor/rules/example-testing-rules.mdc @@ -0,0 +1,39 @@ +--- +description: Testing guidelines and conventions +globs: ["**/*.test.dart", "**/*_test.dart", "**/test/**"] +alwaysApply: false +--- + +# Testing Rules Example + +This rule file demonstrates rules that apply only to test files. + +## Usage +- Targets all test files using multiple glob patterns +- Ensures consistent testing practices across the project + +## Testing Conventions +1. **Test File Naming** + - Unit tests: `*_test.dart` or `*.test.dart` + - Widget tests: `*_widget_test.dart` + - Integration tests: `*_integration_test.dart` + +2. **Test Structure** + - Use `setUp()` and `tearDown()` for common setup + - Group related tests with `group()` + - Use descriptive test names: `test('should return error when user is null')` + +3. **Mocking** + - Check for existing `mocks.dart` files first + - Use `mockito` for generating mocks + - Run `melos` for build_runner when mocks are needed + +4. **Coverage** + - Aim for 80%+ coverage + - Focus on business logic coverage + - Don't test implementation details + +## Test Organization +- Place tests next to the code they test +- Use `test/` directory for test files +- Mirror the source directory structure in tests diff --git a/.cursor/rules/example-ui-rules.mdc b/.cursor/rules/example-ui-rules.mdc new file mode 100644 index 00000000..e3d4afff --- /dev/null +++ b/.cursor/rules/example-ui-rules.mdc @@ -0,0 +1,47 @@ +--- +description: UI and widget development guidelines +globs: ["**/widgets/**", "**/pages/**", "**/screens/**", "**/ui_kit/**"] +alwaysApply: false +--- + +# UI Development Rules Example + +This rule file demonstrates rules for UI and widget development. + +## Widget Conventions + +### Page Structure +- Place `Scaffold` and `AppBar` in parent class +- Keep main body in private child class (e.g., `_HomePage`) +- Example: +```dart +class HomePage extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(...), + body: _HomePage(), + ); + } +} + +class _HomePage extends StatelessWidget { ... } +``` + +### Constants Usage +- **Spacing**: Use `spacing_constants.dart` for all spacing values +- **Borders**: Use `border_constants.dart` for `BorderRadius.circular()` +- **All UI constants**: Check `/constants` folder first +- If constant doesn't exist, add it to appropriate file in `/constants` + +### Widget Modularity +- Break large widgets into smaller, modular classes +- Create separate files for modular classes +- Use `part`/`part of` for file linkage +- Keep `part` sections alphabetical +- Place modular classes in `/widgets` folder + +### Testing UI +- Use keys from `widget_keys.dart` for consistency +- Test user interactions, not implementation +- Keep widget tests focused and fast diff --git a/.cursor/skills/example-skill/SKILL.md b/.cursor/skills/example-skill/SKILL.md new file mode 100644 index 00000000..e69de29b diff --git a/.cursor/templates/example-freezed-model-template.dart b/.cursor/templates/example-freezed-model-template.dart new file mode 100644 index 00000000..398f9a69 --- /dev/null +++ b/.cursor/templates/example-freezed-model-template.dart @@ -0,0 +1,20 @@ +// Template for creating a new Freezed model +// Copy this file and replace with your actual model name +// Run: melos build_runner to generate code + +import 'package:freezed_annotation/freezed_annotation.dart'; + +part '.freezed.dart'; +part '.g.dart'; + +@freezed +class with _$ { + const factory ({ + required String id, + // Add your fields here + String? optionalField, + }) = _; + + factory .fromJson(Map json) => + _$FromJson(json); +} diff --git a/.cursor/templates/example-page-template.dart b/.cursor/templates/example-page-template.dart new file mode 100644 index 00000000..26904128 --- /dev/null +++ b/.cursor/templates/example-page-template.dart @@ -0,0 +1,27 @@ +// Template for creating a new page +// Copy this file and replace with your actual page name + +import 'package:flutter/material.dart'; + +class Page extends StatelessWidget { + const Page({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text(''), + ), + body: _Page(), + ); + } +} + +class _Page extends StatelessWidget { + const _Page(); + + @override + Widget build(BuildContext context) { + return const Placeholder(); + } +} diff --git a/.cursor/templates/example-service-template.dart b/.cursor/templates/example-service-template.dart new file mode 100644 index 00000000..caa06788 --- /dev/null +++ b/.cursor/templates/example-service-template.dart @@ -0,0 +1,32 @@ +// Template for creating a new service +// Copy this file and replace with your actual service name + +import 'package:core/core.dart'; + +// 1. Create interface file: interfaces/i__service.dart +abstract class IService { + Future> exampleMethod(); +} + +// 2. Create implementation file: implementations/_service.dart +@injectable +class Service implements IService { + Service(); + + @override + Future> exampleMethod() async { + try { + // Implementation here + return Result.success(null); + } catch (e) { + return Result.failure(Exception(e.toString())); + } + } +} + +// 3. Register in injectable_module.dart +// Add: @module +// abstract class Module { +// @lazySingleton +// IService get Service => Service(); +// } diff --git a/.cursor/templates/example-snippets.json b/.cursor/templates/example-snippets.json new file mode 100644 index 00000000..78fb3429 --- /dev/null +++ b/.cursor/templates/example-snippets.json @@ -0,0 +1,161 @@ +{ + "_comment": "Example snippets you can add to .vscode/snippets.code-snippets", + "_instructions": "Copy the snippets you want into your snippets.code-snippets file", + + "Service Interface": { + "scope": "dart", + "prefix": "iservice", + "body": [ + "abstract class I${1:ServiceName}Service {", + " Future> ${3:methodName}();", + "}" + ], + "description": "Create a service interface" + }, + + "Service Implementation": { + "scope": "dart", + "prefix": "service", + "body": [ + "@injectable", + "class ${1:ServiceName}Service implements I${1:ServiceName}Service {", + " ${1:ServiceName}Service();", + "", + " @override", + " Future> ${3:methodName}() async {", + " try {", + " // Implementation", + " return Result.success(${4:value});", + " } catch (e) {", + " return Result.failure(Exception(e.toString()));", + " }", + " }", + "}" + ], + "description": "Create a service implementation" + }, + + "Flutter Page": { + "scope": "dart", + "prefix": "fpage", + "body": [ + "class ${1:PageName}Page extends StatelessWidget {", + " const ${1:PageName}Page({super.key});", + "", + " @override", + " Widget build(BuildContext context) {", + " return Scaffold(", + " appBar: AppBar(", + " title: const Text('${2:Page Title}'),", + " ),", + " body: _${1:PageName}Page(),", + " );", + " }", + "}", + "", + "class _${1:PageName}Page extends StatelessWidget {", + " const _${1:PageName}Page();", + "", + " @override", + " Widget build(BuildContext context) {", + " return const Placeholder();", + " }", + "}" + ], + "description": "Create a Flutter page with scaffold" + }, + + "Freezed Model": { + "scope": "dart", + "prefix": "fmodel", + "body": [ + "import 'package:freezed_annotation/freezed_annotation.dart';", + "", + "part '${1:model_name}.freezed.dart';", + "part '${1:model_name}.g.dart';", + "", + "@freezed", + "class ${2:ModelName} with _$${2:ModelName} {", + " const factory ${2:ModelName}({", + " required String id,", + " ${3:// Add fields here}", + " }) = _${2:ModelName};", + "", + " factory ${2:ModelName}.fromJson(Map json) =>", + " _$${2:ModelName}FromJson(json);", + "}" + ], + "description": "Create a Freezed model" + }, + + "Repository Interface": { + "scope": "dart", + "prefix": "irepo", + "body": [ + "abstract class I${1:EntityName}Repository {", + " Future>> getAll();", + " Future> getById(String id);", + " Future> create(${1:EntityName} entity);", + " Future> update(${1:EntityName} entity);", + " Future> delete(String id);", + "}" + ], + "description": "Create a repository interface" + }, + + "Stateless Widget": { + "scope": "dart", + "prefix": "swidget", + "body": [ + "class ${1:WidgetName} extends StatelessWidget {", + " const ${1:WidgetName}({", + " super.key,", + " ${2:// Add parameters}", + " });", + "", + " @override", + " Widget build(BuildContext context) {", + " return ${3:const Placeholder();}", + " }", + "}" + ], + "description": "Create a stateless widget" + }, + + "Stateful Widget": { + "scope": "dart", + "prefix": "fwidget", + "body": [ + "class ${1:WidgetName} extends StatefulWidget {", + " const ${1:WidgetName}({super.key});", + "", + " @override", + " State<${1:WidgetName}> createState() => _${1:WidgetName}State();", + "}", + "", + "class _${1:WidgetName}State extends State<${1:WidgetName}> {", + " @override", + " Widget build(BuildContext context) {", + " return ${2:const Placeholder();}", + " }", + "}" + ], + "description": "Create a stateful widget" + }, + + "Result Type": { + "scope": "dart", + "prefix": "result", + "body": [ + "Future> ${2:methodName}() async {", + " try {", + " ${3:// Implementation}", + " return Result.success(${4:value});", + " } catch (e) {", + " return Result.failure(Exception(e.toString()));", + " }", + "}" + ], + "description": "Create a method returning Result type" + } +} diff --git a/.cursor/templates/example-test-template.dart b/.cursor/templates/example-test-template.dart new file mode 100644 index 00000000..95efbf81 --- /dev/null +++ b/.cursor/templates/example-test-template.dart @@ -0,0 +1,28 @@ +// Template for creating a new test file +// Copy this file and replace with the class you're testing + +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('', () { + setUp(() { + // Setup code here + }); + + tearDown(() { + // Cleanup code here + }); + + test('should when ', () async { + // Arrange + // Act + // Assert + }); + + test('should handle ', () async { + // Arrange + // Act + // Assert + }); + }); +} From 1e95c0afede4722decaf0997b2e4fa6c57af7ce1 Mon Sep 17 00:00:00 2001 From: Zander Kotze Date: Fri, 6 Feb 2026 01:46:05 +0200 Subject: [PATCH 11/13] chore: clean up - remove firebase_options --- lib/firebase_options.dart | 75 --------------------------------------- 1 file changed, 75 deletions(-) delete mode 100644 lib/firebase_options.dart diff --git a/lib/firebase_options.dart b/lib/firebase_options.dart deleted file mode 100644 index 0279deba..00000000 --- a/lib/firebase_options.dart +++ /dev/null @@ -1,75 +0,0 @@ -// // File generated by FlutterFire CLI. -// // ignore_for_file: lines_longer_than_80_chars, avoid_classes_with_only_static_members -// 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: -// 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 web = FirebaseOptions( -// apiKey: 'AIzaSyA3_VXMz5bCPhDzQ4psfOHl5mEZt8s_w-4', -// appId: '1:82796040762:web:15fecbf2203c038ee4db03', -// messagingSenderId: '82796040762', -// projectId: 'multichoice-412309', -// authDomain: 'multichoice-412309.firebaseapp.com', -// storageBucket: 'multichoice-412309.appspot.com', -// measurementId: 'G-RKRDGDJMDK', -// ); - -// static const FirebaseOptions android = FirebaseOptions( -// apiKey: 'AIzaSyA9oQ4EKnvNdGnh_cVRoTcQ62tNij9YNSY', -// appId: '1:82796040762:android:7d86369ea3e82786e4db03', -// messagingSenderId: '82796040762', -// projectId: 'multichoice-412309', -// storageBucket: 'multichoice-412309.appspot.com', -// ); - -// static const FirebaseOptions ios = FirebaseOptions( -// apiKey: 'AIzaSyCbM12acc3yLQbmEHxxqMij_EVJB9FJN8E', -// appId: '1:82796040762:ios:60f8a21e5479962fe4db03', -// messagingSenderId: '82796040762', -// projectId: 'multichoice-412309', -// storageBucket: 'multichoice-412309.appspot.com', -// iosBundleId: 'co.za.zanderkotze.multichoice', -// ); -// } From 58181b2600dba4a0e42b9393d47c79de43887c92 Mon Sep 17 00:00:00 2001 From: Zander Kotze Date: Fri, 6 Feb 2026 02:02:21 +0200 Subject: [PATCH 12/13] chore: add usage for remote config in drawer --- .../drawer/widgets/drawer_header_section.dart | 92 +++++++++++-------- .../presentation/drawer/widgets/export.dart | 1 + .../implementations/firebase_service.dart | 4 +- 3 files changed, 57 insertions(+), 40 deletions(-) diff --git a/apps/multichoice/lib/presentation/drawer/widgets/drawer_header_section.dart b/apps/multichoice/lib/presentation/drawer/widgets/drawer_header_section.dart index 46ed3ce2..f53ca1b6 100644 --- a/apps/multichoice/lib/presentation/drawer/widgets/drawer_header_section.dart +++ b/apps/multichoice/lib/presentation/drawer/widgets/drawer_header_section.dart @@ -5,52 +5,66 @@ class DrawerHeaderSection extends StatelessWidget { @override Widget build(BuildContext context) { - return DrawerHeader( - padding: allPadding12, - child: Row( - children: [ - ClipRRect( - borderRadius: borderCircular12, - child: Image.asset( - Assets.images.playstore.path, - width: 48, - height: 48, - ), - ), - gap16, - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, + final welcomeMessage = coreSl().getString( + FirebaseConfigKeys.welcomeMessage, + ); + + return FutureBuilder( + future: welcomeMessage, + builder: (context, asyncSnapshot) { + if (asyncSnapshot.connectionState == ConnectionState.done && + asyncSnapshot.hasData) { + return DrawerHeader( + padding: allPadding12, + child: Row( children: [ - Text( - 'Multichoice', - style: AppTypography.titleLarge.copyWith( - color: Colors.white, + ClipRRect( + borderRadius: borderCircular12, + child: Image.asset( + Assets.images.playstore.path, + width: 48, + height: 48, ), ), - gap4, - Text( - 'Welcome back!', - style: AppTypography.subtitleMedium.copyWith( - color: Colors.white70, + gap16, + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Multichoice', + style: AppTypography.titleLarge.copyWith( + color: Colors.white, + ), + ), + gap4, + Text( + asyncSnapshot.data ?? 'Welcome back!', + style: AppTypography.subtitleMedium.copyWith( + color: Colors.white70, + ), + ), + ], + ), + ), + IconButton( + onPressed: () { + Navigator.of(context).pop(); + }, + tooltip: TooltipEnums.close.tooltip, + icon: const Icon( + Icons.close_outlined, + size: 28, ), ), ], ), - ), - IconButton( - onPressed: () { - Navigator.of(context).pop(); - }, - tooltip: TooltipEnums.close.tooltip, - icon: const Icon( - Icons.close_outlined, - size: 28, - ), - ), - ], - ), + ); + } + + return const SizedBox.shrink(); + }, ); } } diff --git a/apps/multichoice/lib/presentation/drawer/widgets/export.dart b/apps/multichoice/lib/presentation/drawer/widgets/export.dart index d557bdd5..b133c29a 100644 --- a/apps/multichoice/lib/presentation/drawer/widgets/export.dart +++ b/apps/multichoice/lib/presentation/drawer/widgets/export.dart @@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:models/models.dart'; import 'package:multichoice/app/export.dart'; import 'package:multichoice/app/view/theme/app_theme.dart'; import 'package:multichoice/app/view/theme/app_typography.dart'; diff --git a/packages/core/lib/src/services/implementations/firebase_service.dart b/packages/core/lib/src/services/implementations/firebase_service.dart index d023de1e..f255fabf 100644 --- a/packages/core/lib/src/services/implementations/firebase_service.dart +++ b/packages/core/lib/src/services/implementations/firebase_service.dart @@ -100,7 +100,9 @@ class FirebaseService implements IFirebaseService { } try { - return _remoteConfig.getString(key.key); + final value = _remoteConfig.getString(key.key); + + return value; } catch (e) { log('Error getting string for key "${key.key}": $e'); return null; From 393908065ab061914af07b4bdc94d1fd999dfabe Mon Sep 17 00:00:00 2001 From: Zander Kotze Date: Fri, 6 Feb 2026 20:04:23 +0200 Subject: [PATCH 13/13] chore: linting issue --- apps/multichoice/lib/bootstrap.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/multichoice/lib/bootstrap.dart b/apps/multichoice/lib/bootstrap.dart index 8fc16f73..ef2a62ea 100644 --- a/apps/multichoice/lib/bootstrap.dart +++ b/apps/multichoice/lib/bootstrap.dart @@ -1,3 +1,5 @@ +// ignore_for_file: avoid_catches_without_on_clauses, document_ignores + import 'dart:async'; import 'dart:developer';