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/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 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 + }); + }); +} diff --git a/.gitignore b/.gitignore index 8cc7c3e4..22623fa5 100644 --- a/.gitignore +++ b/.gitignore @@ -81,6 +81,7 @@ upload-keystore.* .svn/ migrate_working_dir/ devtools_options.yaml +**/node_modules/ # IntelliJ related *.iml 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 diff --git a/apps/multichoice/lib/bootstrap.dart b/apps/multichoice/lib/bootstrap.dart index 401082f5..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'; @@ -45,4 +47,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 + } } 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/apps/multichoice/pubspec.yaml b/apps/multichoice/pubspec.yaml index 0f3a49d9..293fb959 100644 --- a/apps/multichoice/pubspec.yaml +++ b/apps/multichoice/pubspec.yaml @@ -8,7 +8,7 @@ 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 @@ -16,6 +16,7 @@ dependencies: firebase_core: ^4.4.0 firebase_crashlytics: ^5.0.7 firebase_performance: ^0.11.1+4 + firebase_remote_config: ^6.1.4 flutter: sdk: flutter flutter_bloc: ^9.1.0 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..911ff953 --- /dev/null +++ b/packages/core/lib/src/application/firebase/firebase_event.dart @@ -0,0 +1,6 @@ +part of 'firebase_bloc.dart'; + +@freezed +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 new file mode 100644 index 00000000..80bec2fc --- /dev/null +++ b/packages/core/lib/src/application/firebase/firebase_state.dart @@ -0,0 +1,12 @@ +part of 'firebase_bloc.dart'; + +@freezed +abstract class FirebaseState with _$FirebaseState { + const factory FirebaseState({ + required String color, + }) = _FirebaseState; + + factory FirebaseState.initial() => FirebaseState( + color: '', + ); +} 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..f255fabf --- /dev/null +++ b/packages/core/lib/src/services/implementations/firebase_service.dart @@ -0,0 +1,111 @@ +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 { + final value = _remoteConfig.getString(key.key); + + return value; + } 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); +} diff --git a/packages/core/pubspec.yaml b/packages/core/pubspec.yaml index 29cac2a9..545d62e6 100644 --- a/packages/core/pubspec.yaml +++ b/packages/core/pubspec.yaml @@ -18,6 +18,9 @@ dependencies: sdk: flutter flutter_bloc: ^9.1.0 freezed_annotation: ^3.1.0 + 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 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; +}