├── .editorconfig ├── .env.example ├── .github └── dependabot.yml ├── .gitignore ├── .kiro ├── settings │ └── mcp.json └── steering │ ├── dev_workflow.md │ ├── kiro_rules.md │ ├── self_improve.md │ └── taskmaster.md ├── .metadata ├── .taskmaster ├── config.json ├── docs │ └── prd.txt ├── state.json ├── tasks │ ├── task_001.txt │ ├── task_002.txt │ ├── task_003.txt │ └── tasks.json └── templates │ └── example_prd.txt ├── AGENTS.md ├── GEMINI.md ├── README.md ├── analysis_options.yaml ├── android ├── .gitignore ├── app │ ├── build.gradle │ ├── build.gradle.kts │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── kotlin │ │ │ └── com │ │ │ │ └── example │ │ │ │ ├── firebase_auth_flutter_ddd │ │ │ │ └── MainActivity.kt │ │ │ │ └── firebase_authentication_flutter_ddd │ │ │ │ └── MainActivity.kt │ │ └── res │ │ │ ├── drawable-v21 │ │ │ └── launch_background.xml │ │ │ ├── drawable │ │ │ └── launch_background.xml │ │ │ ├── mipmap-hdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── values-night │ │ │ └── styles.xml │ │ │ └── values │ │ │ └── styles.xml │ │ └── profile │ │ └── AndroidManifest.xml ├── build.gradle ├── build.gradle.kts ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties ├── settings.gradle └── settings.gradle.kts ├── guides ├── architecture.md └── setup.md ├── ios ├── .gitignore ├── Flutter │ ├── AppFrameworkInfo.plist │ ├── Debug.xcconfig │ └── Release.xcconfig ├── Podfile ├── Podfile.lock ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings ├── Runner │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ └── Icon-App-83.5x83.5@2x.png │ │ └── LaunchImage.imageset │ │ │ ├── Contents.json │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ └── README.md │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── Info.plist │ └── Runner-Bridging-Header.h └── RunnerTests │ └── RunnerTests.swift ├── lib ├── app.dart ├── application │ └── authentication │ │ ├── auth_events.dart │ │ ├── auth_events.freezed.dart │ │ ├── auth_state_controller.dart │ │ ├── auth_states.dart │ │ └── auth_states.freezed.dart ├── core │ └── theme │ │ ├── animated_widgets.dart │ │ └── app_theme.dart ├── domain │ ├── authentication │ │ ├── auth_failures.dart │ │ ├── auth_failures.freezed.dart │ │ ├── auth_value_failures.dart │ │ ├── auth_value_failures.freezed.dart │ │ ├── auth_value_objects.dart │ │ ├── auth_value_validators.dart │ │ └── i_auth_facade.dart │ └── core │ │ ├── errors.dart │ │ └── value_object.dart ├── firebase_options.dart ├── main.dart ├── screens │ ├── home_page.dart │ ├── login_page.dart │ ├── registration_page.dart │ └── utils │ │ └── custom_snackbar.dart └── services │ └── authentication │ └── firebase_auth_facade.dart ├── opencode.json ├── pubspec.lock ├── pubspec.yaml ├── scripts └── delete └── web ├── favicon.png ├── icons ├── Icon-192.png ├── Icon-512.png ├── Icon-maskable-192.png └── Icon-maskable-512.png ├── index.html └── manifest.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | tab_width = 4 6 | indent_size = 4 7 | end_of_line = lf 8 | max_line_length = 120 9 | ij_visual_guides = 120 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [*.{js,py,html}] 14 | charset = utf-8 15 | 16 | [*.md] 17 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | OLLAMA_API_KEY="ollama" 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "pub" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | **/ios/Flutter/.last_build_id 26 | .dart_tool/ 27 | .flutter-plugins 28 | .flutter-plugins-dependencies 29 | .packages 30 | .pub-cache/ 31 | .pub/ 32 | /build/ 33 | 34 | # Web related 35 | lib/generated_plugin_registrant.dart 36 | 37 | # Symbolication related 38 | app.*.symbols 39 | 40 | # Obfuscation related 41 | app.*.map.json 42 | 43 | # Android Studio will place build artifacts here 44 | /android/app/debug 45 | /android/app/profile 46 | /android/app/release 47 | /android/app/google-services.json 48 | 49 | .idea 50 | .dart_tool 51 | 52 | ios/firebase_app_id_file.json 53 | 54 | # Logs 55 | logs 56 | npm-debug.log* 57 | yarn-debug.log* 58 | yarn-error.log* 59 | dev-debug.log 60 | # Dependency directories 61 | node_modules/ 62 | # Environment variables 63 | .env 64 | # Editor directories and files 65 | .vscode 66 | *.suo 67 | *.ntvs* 68 | *.njsproj 69 | *.sln 70 | *.sw? 71 | # OS specific 72 | 73 | # Task files 74 | # tasks.json 75 | # tasks/ 76 | 77 | .gemini -------------------------------------------------------------------------------- /.kiro/settings/mcp.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": { 3 | "task-master-ai": { 4 | "command": "npx", 5 | "args": ["-y", "--package=task-master-ai", "task-master-ai"], 6 | "env": { 7 | "ANTHROPIC_API_KEY": "YOUR_ANTHROPIC_API_KEY_HERE", 8 | "PERPLEXITY_API_KEY": "YOUR_PERPLEXITY_API_KEY_HERE", 9 | "OPENAI_API_KEY": "YOUR_OPENAI_KEY_HERE", 10 | "GOOGLE_API_KEY": "YOUR_GOOGLE_KEY_HERE", 11 | "XAI_API_KEY": "YOUR_XAI_KEY_HERE", 12 | "OPENROUTER_API_KEY": "YOUR_OPENROUTER_KEY_HERE", 13 | "MISTRAL_API_KEY": "YOUR_MISTRAL_KEY_HERE", 14 | "AZURE_OPENAI_API_KEY": "YOUR_AZURE_KEY_HERE", 15 | "OLLAMA_API_KEY": "YOUR_OLLAMA_API_KEY_HERE" 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.kiro/steering/kiro_rules.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: Guidelines for creating and maintaining Kiro rules to ensure consistency and effectiveness. 3 | globs: .kiro/steering/*.md 4 | alwaysApply: true 5 | --- 6 | 7 | - **Required Rule Structure:** 8 | ```markdown 9 | --- 10 | description: Clear, one-line description of what the rule enforces 11 | globs: path/to/files/*.ext, other/path/**/* 12 | alwaysApply: boolean 13 | --- 14 | 15 | - **Main Points in Bold** 16 | - Sub-points with details 17 | - Examples and explanations 18 | ``` 19 | 20 | - **File References:** 21 | - Use `[filename](mdc:path/to/file)` ([filename](mdc:filename)) to reference files 22 | - Example: [prisma.md](.kiro/steering/prisma.md) for rule references 23 | - Example: [schema.prisma](mdc:prisma/schema.prisma) for code references 24 | 25 | - **Code Examples:** 26 | - Use language-specific code blocks 27 | ```typescript 28 | // ✅ DO: Show good examples 29 | const goodExample = true; 30 | 31 | // ❌ DON'T: Show anti-patterns 32 | const badExample = false; 33 | ``` 34 | 35 | - **Rule Content Guidelines:** 36 | - Start with high-level overview 37 | - Include specific, actionable requirements 38 | - Show examples of correct implementation 39 | - Reference existing code when possible 40 | - Keep rules DRY by referencing other rules 41 | 42 | - **Rule Maintenance:** 43 | - Update rules when new patterns emerge 44 | - Add examples from actual codebase 45 | - Remove outdated patterns 46 | - Cross-reference related rules 47 | 48 | - **Best Practices:** 49 | - Use bullet points for clarity 50 | - Keep descriptions concise 51 | - Include both DO and DON'T examples 52 | - Reference actual code over theoretical examples 53 | - Use consistent formatting across rules -------------------------------------------------------------------------------- /.kiro/steering/self_improve.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: Guidelines for continuously improving Kiro rules based on emerging code patterns and best practices. 3 | globs: **/* 4 | alwaysApply: true 5 | --- 6 | 7 | - **Rule Improvement Triggers:** 8 | - New code patterns not covered by existing rules 9 | - Repeated similar implementations across files 10 | - Common error patterns that could be prevented 11 | - New libraries or tools being used consistently 12 | - Emerging best practices in the codebase 13 | 14 | - **Analysis Process:** 15 | - Compare new code with existing rules 16 | - Identify patterns that should be standardized 17 | - Look for references to external documentation 18 | - Check for consistent error handling patterns 19 | - Monitor test patterns and coverage 20 | 21 | - **Rule Updates:** 22 | - **Add New Rules When:** 23 | - A new technology/pattern is used in 3+ files 24 | - Common bugs could be prevented by a rule 25 | - Code reviews repeatedly mention the same feedback 26 | - New security or performance patterns emerge 27 | 28 | - **Modify Existing Rules When:** 29 | - Better examples exist in the codebase 30 | - Additional edge cases are discovered 31 | - Related rules have been updated 32 | - Implementation details have changed 33 | 34 | - **Example Pattern Recognition:** 35 | ```typescript 36 | // If you see repeated patterns like: 37 | const data = await prisma.user.findMany({ 38 | select: { id: true, email: true }, 39 | where: { status: 'ACTIVE' } 40 | }); 41 | 42 | // Consider adding to [prisma.md](.kiro/steering/prisma.md): 43 | // - Standard select fields 44 | // - Common where conditions 45 | // - Performance optimization patterns 46 | ``` 47 | 48 | - **Rule Quality Checks:** 49 | - Rules should be actionable and specific 50 | - Examples should come from actual code 51 | - References should be up to date 52 | - Patterns should be consistently enforced 53 | 54 | - **Continuous Improvement:** 55 | - Monitor code review comments 56 | - Track common development questions 57 | - Update rules after major refactors 58 | - Add links to relevant documentation 59 | - Cross-reference related rules 60 | 61 | - **Rule Deprecation:** 62 | - Mark outdated patterns as deprecated 63 | - Remove rules that no longer apply 64 | - Update references to deprecated rules 65 | - Document migration paths for old patterns 66 | 67 | - **Documentation Updates:** 68 | - Keep examples synchronized with code 69 | - Update references to external docs 70 | - Maintain links between related rules 71 | - Document breaking changes 72 | Follow [kiro_rules.md](.kiro/steering/kiro_rules.md) for proper rule formatting and structure. 73 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: "077b4a4ce10a07b82caa6897f0c626f9c0a3ac90" 8 | channel: "stable" 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: 077b4a4ce10a07b82caa6897f0c626f9c0a3ac90 17 | base_revision: 077b4a4ce10a07b82caa6897f0c626f9c0a3ac90 18 | - platform: android 19 | create_revision: 077b4a4ce10a07b82caa6897f0c626f9c0a3ac90 20 | base_revision: 077b4a4ce10a07b82caa6897f0c626f9c0a3ac90 21 | - platform: ios 22 | create_revision: 077b4a4ce10a07b82caa6897f0c626f9c0a3ac90 23 | base_revision: 077b4a4ce10a07b82caa6897f0c626f9c0a3ac90 24 | - platform: linux 25 | create_revision: 077b4a4ce10a07b82caa6897f0c626f9c0a3ac90 26 | base_revision: 077b4a4ce10a07b82caa6897f0c626f9c0a3ac90 27 | - platform: macos 28 | create_revision: 077b4a4ce10a07b82caa6897f0c626f9c0a3ac90 29 | base_revision: 077b4a4ce10a07b82caa6897f0c626f9c0a3ac90 30 | - platform: web 31 | create_revision: 077b4a4ce10a07b82caa6897f0c626f9c0a3ac90 32 | base_revision: 077b4a4ce10a07b82caa6897f0c626f9c0a3ac90 33 | - platform: windows 34 | create_revision: 077b4a4ce10a07b82caa6897f0c626f9c0a3ac90 35 | base_revision: 077b4a4ce10a07b82caa6897f0c626f9c0a3ac90 36 | 37 | # User provided section 38 | 39 | # List of Local paths (relative to this file) that should be 40 | # ignored by the migrate tool. 41 | # 42 | # Files that are not part of the templates will be ignored by default. 43 | unmanaged_files: 44 | - 'lib/main.dart' 45 | - 'ios/Runner.xcodeproj/project.pbxproj' 46 | -------------------------------------------------------------------------------- /.taskmaster/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "models": { 3 | "main": { 4 | "provider": "ollama", 5 | "modelId": "devstral:24b", 6 | "maxTokens": 64000, 7 | "temperature": 0.2 8 | }, 9 | "research": { 10 | "provider": "ollama", 11 | "modelId": "gemma3:12b", 12 | "maxTokens": 8700, 13 | "temperature": 0.1 14 | }, 15 | "fallback": { 16 | "provider": "ollama", 17 | "modelId": "phi4-mini:latest", 18 | "maxTokens": 8192, 19 | "temperature": 0.2 20 | } 21 | }, 22 | "global": { 23 | "logLevel": "info", 24 | "debug": false, 25 | "defaultNumTasks": 10, 26 | "defaultSubtasks": 5, 27 | "defaultPriority": "medium", 28 | "projectName": "Task Master", 29 | "ollamaBaseURL": "http://127.0.0.1:11434/api", 30 | "bedrockBaseURL": "https://bedrock.us-east-1.amazonaws.com", 31 | "responseLanguage": "English", 32 | "userId": "1234567890", 33 | "defaultTag": "master" 34 | }, 35 | "claudeCode": {} 36 | } 37 | -------------------------------------------------------------------------------- /.taskmaster/docs/prd.txt: -------------------------------------------------------------------------------- 1 | 2 | # Product Requirements Document: Firebase Authentication Flutter DDD App 3 | 4 | ## 1. Overview 5 | 6 | This document outlines the requirements for updating and improving the Firebase Authentication Flutter DDD application. The primary goals are to migrate to the latest stable version of Riverpod, modernize the user interface, and improve the overall quality and maintainability of the codebase. 7 | 8 | ## 2. Key Features and Improvements 9 | 10 | ### 2.1. Riverpod 3.0 Migration 11 | 12 | The application currently uses a pre-release version of Riverpod 3.0. This should be updated to the latest stable release of Riverpod 3.0. This migration will involve: 13 | 14 | - Updating the `hooks_riverpod` dependency in `pubspec.yaml`. 15 | - Refactoring the existing providers and widgets to align with the latest Riverpod 3.0 APIs. 16 | - Ensuring that the state management is robust and efficient. 17 | 18 | ### 2.2. UI Modernization 19 | 20 | The current user interface is basic and can be significantly improved. The new UI should be: 21 | 22 | - **Modern and visually appealing:** Adopt a clean and modern design language (e.g., Material You). 23 | - **User-friendly:** The user experience should be intuitive and seamless. 24 | - **Responsive:** The UI should adapt to different screen sizes and orientations. 25 | 26 | This will involve: 27 | 28 | - Redesigning the login, registration, and home pages. 29 | - Creating a consistent theme and style guide. 30 | - Adding animations and transitions to enhance the user experience. 31 | 32 | ### 2.3. Code Refactoring and Cleanup 33 | 34 | The codebase should be refactored to improve its quality, readability, and maintainability. This includes: 35 | 36 | - **Updating dependencies:** All dependencies should be updated to their latest stable versions. 37 | - **Improving error handling:** Implement a more robust error handling mechanism to gracefully handle exceptions and provide clear feedback to the user. 38 | - **Enhancing validation:** Strengthen the input validation for the login and registration forms. 39 | - **Code organization:** Ensure that the code is well-organized and follows the principles of Domain-Driven Design (DDD). 40 | 41 | ### 2.4. Enhanced Authentication Flow 42 | 43 | The authentication flow can be improved by adding the following features: 44 | 45 | - **Password reset:** Allow users to reset their passwords if they forget them. 46 | - **Email verification:** Send a verification email to new users to ensure that they have provided a valid email address. 47 | - **Social logins:** Add support for social logins (e.g., Google, Apple) to provide users with more options for signing in. 48 | 49 | ## 3. Non-Functional Requirements 50 | 51 | - **Performance:** The application should be performant and responsive, with minimal jank or lag. 52 | - **Security:** The application should be secure and protect user data. 53 | - **Scalability:** The architecture should be scalable to accommodate future growth and new features. 54 | 55 | ## 4. Future Enhancements 56 | 57 | - **Profile page:** A user profile page where users can view and edit their information. 58 | - **Two-factor authentication:** Add an extra layer of security with two-factor authentication. 59 | - **Offline support:** Cache data locally to provide a seamless experience even when the user is offline. 60 | -------------------------------------------------------------------------------- /.taskmaster/state.json: -------------------------------------------------------------------------------- 1 | { 2 | "currentTag": "master", 3 | "lastSwitched": "2025-07-21T07:37:41.540Z", 4 | "branchTagMapping": {}, 5 | "migrationNoticeShown": true 6 | } -------------------------------------------------------------------------------- /.taskmaster/tasks/task_001.txt: -------------------------------------------------------------------------------- 1 | # Task ID: 1 2 | # Title: Migrate to Riverpod 3.0 3 | # Status: done 4 | # Dependencies: None 5 | # Priority: high 6 | # Description: Update the application to use the latest stable version of Riverpod 3.0. This includes updating the `hooks_riverpod` dependency, refactoring providers and widgets, and ensuring the state management is robust. 7 | # Details: 8 | 9 | 10 | # Test Strategy: 11 | 12 | -------------------------------------------------------------------------------- /.taskmaster/tasks/task_002.txt: -------------------------------------------------------------------------------- 1 | # Task ID: 2 2 | # Title: Modernize the User Interface 3 | # Status: done 4 | # Dependencies: 1 5 | # Priority: high 6 | # Description: Redesign the login, registration, and home pages to be more modern and visually appealing. Adopt a clean design language like Material You, create a consistent theme, and add animations to enhance the user experience. 7 | # Details: 8 | 9 | 10 | # Test Strategy: 11 | 12 | -------------------------------------------------------------------------------- /.taskmaster/tasks/task_003.txt: -------------------------------------------------------------------------------- 1 | # Task ID: 3 2 | # Title: Refactor and Clean Up Codebase 3 | # Status: pending 4 | # Dependencies: 1 5 | # Priority: medium 6 | # Description: Improve the quality, readability, and maintainability of the codebase. This includes updating all dependencies to their latest stable versions, improving error handling, enhancing input validation, and ensuring the code is well-organized. 7 | # Details: 8 | 9 | 10 | # Test Strategy: 11 | 12 | -------------------------------------------------------------------------------- /.taskmaster/tasks/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "master": { 3 | "tasks": [ 4 | { 5 | "id": 1, 6 | "title": "Migrate to Riverpod 3.0", 7 | "description": "Update the application to use the latest stable version of Riverpod 3.0. This includes updating the `hooks_riverpod` dependency, refactoring providers and widgets, and ensuring the state management is robust.", 8 | "status": "done", 9 | "priority": "high", 10 | "dependencies": [] 11 | }, 12 | { 13 | "id": 2, 14 | "title": "Modernize the User Interface", 15 | "description": "Redesign the login, registration, and home pages to be more modern and visually appealing. Adopt a clean design language like Material You, create a consistent theme, and add animations to enhance the user experience.", 16 | "status": "done", 17 | "priority": "high", 18 | "dependencies": [ 19 | 1 20 | ] 21 | }, 22 | { 23 | "id": 3, 24 | "title": "Refactor and Clean Up Codebase", 25 | "description": "Improve the quality, readability, and maintainability of the codebase. This includes updating all dependencies to their latest stable versions, improving error handling, enhancing input validation, and ensuring the code is well-organized.", 26 | "status": "pending", 27 | "dependencies": [ 28 | 1 29 | ], 30 | "priority": "medium", 31 | "details": "", 32 | "testStrategy": "", 33 | "subtasks": [ 34 | { 35 | "id": 1, 36 | "title": "Refactor Domain Layer", 37 | "description": "Fix typos, enhance email validation with industry-standard regex, strengthen password security (8+ chars)", 38 | "status": "done", 39 | "dependencies": [], 40 | "details": "", 41 | "testStrategy": "" 42 | }, 43 | { 44 | "id": 2, 45 | "title": "Refactor Application Layer", 46 | "description": "Streamline AuthStateController, improve error handling, cleaner code structure", 47 | "status": "done", 48 | "dependencies": [], 49 | "details": "", 50 | "testStrategy": "" 51 | }, 52 | { 53 | "id": 3, 54 | "title": "Refactor Services Layer", 55 | "description": "Enhance Firebase Auth Facade with centralized error mapping and better null safety", 56 | "status": "done", 57 | "dependencies": [], 58 | "details": "", 59 | "testStrategy": "" 60 | }, 61 | { 62 | "id": 4, 63 | "title": "Refactor UI Components", 64 | "description": "Refactor Login/Registration pages with modular structure, extract helper methods, enhance validation", 65 | "status": "done", 66 | "dependencies": [], 67 | "details": "", 68 | "testStrategy": "" 69 | }, 70 | { 71 | "id": 5, 72 | "title": "App Level Improvements", 73 | "description": "Simplify routing, add global page transitions, performance improvements", 74 | "status": "done", 75 | "dependencies": [], 76 | "details": "", 77 | "testStrategy": "" 78 | }, 79 | { 80 | "id": 6, 81 | "title": "Update Dependencies", 82 | "description": "Update all dependencies to their latest stable versions", 83 | "status": "pending", 84 | "dependencies": [], 85 | "details": "", 86 | "testStrategy": "" 87 | } 88 | ] 89 | }, 90 | { 91 | "id": 4, 92 | "title": "Implement Password Reset", 93 | "description": "Allow users to reset their passwords if they forget them. This involves creating a 'Forgot Password' screen and integrating with Firebase Authentication's password reset functionality.", 94 | "status": "pending", 95 | "priority": "medium", 96 | "dependencies": [ 97 | 1 98 | ] 99 | }, 100 | { 101 | "id": 5, 102 | "title": "Implement Email Verification", 103 | "description": "Send a verification email to new users to ensure that they have provided a valid email address. This will help to reduce spam and ensure that users can be reached.", 104 | "status": "pending", 105 | "priority": "medium", 106 | "dependencies": [ 107 | 1 108 | ] 109 | }, 110 | { 111 | "id": 6, 112 | "title": "Add Social Logins", 113 | "description": "Add support for social logins (e.g., Google, Apple) to provide users with more options for signing in. This will improve the user experience and make it easier for users to get started with the app.", 114 | "status": "pending", 115 | "priority": "low", 116 | "dependencies": [ 117 | 1 118 | ] 119 | }, 120 | { 121 | "id": 7, 122 | "title": "Create User Profile Page", 123 | "description": "Create a user profile page where users can view and edit their information. This will give users more control over their data and allow them to personalize their experience.", 124 | "status": "pending", 125 | "priority": "low", 126 | "dependencies": [ 127 | 1 128 | ] 129 | }, 130 | { 131 | "id": 8, 132 | "title": "Implement Two-Factor Authentication", 133 | "description": "Add an extra layer of security with two-factor authentication. This will help to protect user accounts from unauthorized access.", 134 | "status": "pending", 135 | "priority": "low", 136 | "dependencies": [ 137 | 1 138 | ] 139 | }, 140 | { 141 | "id": 9, 142 | "title": "Implement Offline Support", 143 | "description": "Cache data locally to provide a seamless experience even when the user is offline. This will improve the user experience and make the app more resilient to network interruptions.", 144 | "status": "pending", 145 | "priority": "low", 146 | "dependencies": [ 147 | 1 148 | ] 149 | } 150 | ], 151 | "metadata": { 152 | "created": "2025-07-21T07:47:36.467Z", 153 | "updated": "2025-07-21T13:00:48.148Z", 154 | "description": "Tasks for master context" 155 | } 156 | } 157 | } -------------------------------------------------------------------------------- /.taskmaster/templates/example_prd.txt: -------------------------------------------------------------------------------- 1 | 2 | # Overview 3 | [Provide a high-level overview of your product here. Explain what problem it solves, who it's for, and why it's valuable.] 4 | 5 | # Core Features 6 | [List and describe the main features of your product. For each feature, include: 7 | - What it does 8 | - Why it's important 9 | - How it works at a high level] 10 | 11 | # User Experience 12 | [Describe the user journey and experience. Include: 13 | - User personas 14 | - Key user flows 15 | - UI/UX considerations] 16 | 17 | 18 | # Technical Architecture 19 | [Outline the technical implementation details: 20 | - System components 21 | - Data models 22 | - APIs and integrations 23 | - Infrastructure requirements] 24 | 25 | # Development Roadmap 26 | [Break down the development process into phases: 27 | - MVP requirements 28 | - Future enhancements 29 | - Do not think about timelines whatsoever -- all that matters is scope and detailing exactly what needs to be build in each phase so it can later be cut up into tasks] 30 | 31 | # Logical Dependency Chain 32 | [Define the logical order of development: 33 | - Which features need to be built first (foundation) 34 | - Getting as quickly as possible to something usable/visible front end that works 35 | - Properly pacing and scoping each feature so it is atomic but can also be built upon and improved as development approaches] 36 | 37 | # Risks and Mitigations 38 | [Identify potential risks and how they'll be addressed: 39 | - Technical challenges 40 | - Figuring out the MVP that we can build upon 41 | - Resource constraints] 42 | 43 | # Appendix 44 | [Include any additional information: 45 | - Research findings 46 | - Technical specifications] 47 | -------------------------------------------------------------------------------- /AGENTS.md: -------------------------------------------------------------------------------- 1 | # Task Master AI - Agent Integration Guide 2 | 3 | ## Essential Commands 4 | 5 | ### Core Workflow Commands 6 | 7 | ```bash 8 | # Project Setup 9 | task-master init # Initialize Task Master in current project 10 | task-master parse-prd .taskmaster/docs/prd.txt # Generate tasks from PRD document 11 | task-master models --setup # Configure AI models interactively 12 | 13 | # Daily Development Workflow 14 | task-master list # Show all tasks with status 15 | task-master next # Get next available task to work on 16 | task-master show # View detailed task information (e.g., task-master show 1.2) 17 | task-master set-status --id= --status=done # Mark task complete 18 | 19 | # Task Management 20 | task-master add-task --prompt="description" --research # Add new task with AI assistance 21 | task-master expand --id= --research --force # Break task into subtasks 22 | task-master update-task --id= --prompt="changes" # Update specific task 23 | task-master update --from= --prompt="changes" # Update multiple tasks from ID onwards 24 | task-master update-subtask --id= --prompt="notes" # Add implementation notes to subtask 25 | 26 | # Analysis & Planning 27 | task-master analyze-complexity --research # Analyze task complexity 28 | task-master complexity-report # View complexity analysis 29 | task-master expand --all --research # Expand all eligible tasks 30 | 31 | # Dependencies & Organization 32 | task-master add-dependency --id= --depends-on= # Add task dependency 33 | task-master move --from= --to= # Reorganize task hierarchy 34 | task-master validate-dependencies # Check for dependency issues 35 | task-master generate # Update task markdown files (usually auto-called) 36 | ``` 37 | 38 | ## Key Files & Project Structure 39 | 40 | ### Core Files 41 | 42 | - `.taskmaster/tasks/tasks.json` - Main task data file (auto-managed) 43 | - `.taskmaster/config.json` - AI model configuration (use `task-master models`to 44 | modify) 45 | - `.taskmaster/docs/prd.txt` - Product Requirements Document for parsing 46 | - `.taskmaster/tasks/*.txt` - Individual task files (auto-generated from 47 | tasks.json) 48 | - `.env` - API keys for CLI usage 49 | 50 | ### Claude Code Integration Files 51 | 52 | - `CLAUDE.md` - Auto-loaded context for Claude Code (this file) 53 | - `.claude/settings.json` - Claude Code tool allowlist and preferences 54 | - `.claude/commands/` - Custom slash commands for repeated workflows 55 | - `.mcp.json` - MCP server configuration (project-specific) 56 | 57 | ### Directory Structure 58 | 59 | ``` 60 | project/ 61 | ├── .taskmaster/ 62 | │ ├── tasks/ # Task files directory 63 | │ │ ├── tasks.json # Main task database 64 | │ │ ├── task-1.md # Individual task files 65 | │ │ └── task-2.md 66 | │ ├── docs/ # Documentation directory 67 | │ │ ├── prd.txt # Product requirements 68 | │ ├── reports/ # Analysis reports directory 69 | │ │ └── task-complexity-report.json 70 | │ ├── templates/ # Template files 71 | │ │ └── example_prd.txt # Example PRD template 72 | │ └── config.json # AI models & settings 73 | ├── .claude/ 74 | │ ├── settings.json # Claude Code configuration 75 | │ └── commands/ # Custom slash commands 76 | ├── .env # API keys 77 | ├── .mcp.json # MCP configuration 78 | └── CLAUDE.md # This file - auto-loaded by Claude Code 79 | ``` 80 | 81 | ## MCP Integration 82 | 83 | Task Master provides an MCP server that Claude Code can connect to. Configure in 84 | `.mcp.json`: 85 | 86 | ```json 87 | { 88 | "mcpServers": { 89 | "task-master-ai": { 90 | "command": "npx", 91 | "args": [ 92 | "-y", 93 | "--package=task-master-ai", 94 | "task-master-ai" 95 | ], 96 | "env": { 97 | "ANTHROPIC_API_KEY": "your_key_here", 98 | "PERPLEXITY_API_KEY": "your_key_here", 99 | "OPENAI_API_KEY": "OPENAI_API_KEY_HERE", 100 | "GOOGLE_API_KEY": "GOOGLE_API_KEY_HERE", 101 | "XAI_API_KEY": "XAI_API_KEY_HERE", 102 | "OPENROUTER_API_KEY": "OPENROUTER_API_KEY_HERE", 103 | "MISTRAL_API_KEY": "MISTRAL_API_KEY_HERE", 104 | "AZURE_OPENAI_API_KEY": "AZURE_OPENAI_API_KEY_HERE", 105 | "OLLAMA_API_KEY": "OLLAMA_API_KEY_HERE" 106 | } 107 | } 108 | } 109 | } 110 | ``` 111 | 112 | ### Essential MCP Tools 113 | 114 | ```javascript 115 | help; // = shows available taskmaster commands 116 | // Project setup 117 | initialize_project; // = task-master init 118 | parse_prd; // = task-master parse-prd 119 | 120 | // Daily workflow 121 | get_tasks; // = task-master list 122 | next_task; // = task-master next 123 | get_task; // = task-master show 124 | set_task_status; // = task-master set-status 125 | 126 | // Task management 127 | add_task; // = task-master add-task 128 | expand_task; // = task-master expand 129 | update_task; // = task-master update-task 130 | update_subtask; // = task-master update-subtask 131 | update; // = task-master update 132 | 133 | // Analysis 134 | analyze_project_complexity; // = task-master analyze-complexity 135 | complexity_report; // = task-master complexity-report 136 | ``` 137 | 138 | ## Claude Code Workflow Integration 139 | 140 | ### Standard Development Workflow 141 | 142 | #### 1. Project Initialization 143 | 144 | ```bash 145 | # Initialize Task Master 146 | task-master init 147 | 148 | # Create or obtain PRD, then parse it 149 | task-master parse-prd .taskmaster/docs/prd.txt 150 | 151 | # Analyze complexity and expand tasks 152 | task-master analyze-complexity --research 153 | task-master expand --all --research 154 | ``` 155 | 156 | If tasks already exist, another PRD can be parsed (with new information only!) 157 | using parse-prd with --append flag. This will add the generated tasks to the 158 | existing list of tasks.. 159 | 160 | #### 2. Daily Development Loop 161 | 162 | ```bash 163 | # Start each session 164 | task-master next # Find next available task 165 | task-master show # Review task details 166 | 167 | # During implementation, check in code context into the tasks and subtasks 168 | task-master update-subtask --id= --prompt="implementation notes..." 169 | 170 | # Complete tasks 171 | task-master set-status --id= --status=done 172 | ``` 173 | 174 | #### 3. Multi-Claude Workflows 175 | 176 | For complex projects, use multiple Claude Code sessions: 177 | 178 | ```bash 179 | # Terminal 1: Main implementation 180 | cd project && claude 181 | 182 | # Terminal 2: Testing and validation 183 | cd project-test-worktree && claude 184 | 185 | # Terminal 3: Documentation updates 186 | cd project-docs-worktree && claude 187 | ``` 188 | 189 | ### Custom Slash Commands 190 | 191 | Create `.claude/commands/taskmaster-next.md`: 192 | 193 | ```markdown 194 | Find the next available Task Master task and show its details. 195 | 196 | Steps: 197 | 198 | 1. Run `task-master next` to get the next task 199 | 2. If a task is available, run `task-master show ` for full details 200 | 3. Provide a summary of what needs to be implemented 201 | 4. Suggest the first implementation step 202 | ``` 203 | 204 | Create `.claude/commands/taskmaster-complete.md`: 205 | 206 | ```markdown 207 | Complete a Task Master task: $ARGUMENTS 208 | 209 | Steps: 210 | 211 | 1. Review the current task with `task-master show $ARGUMENTS` 212 | 2. Verify all implementation is complete 213 | 3. Run any tests related to this task 214 | 4. Mark as complete: `task-master set-status --id=$ARGUMENTS --status=done` 215 | 5. Show the next available task with `task-master next` 216 | ``` 217 | 218 | ## Tool Allowlist Recommendations 219 | 220 | Add to `.claude/settings.json`: 221 | 222 | ```json 223 | { 224 | "allowedTools": [ 225 | "Edit", 226 | "Bash(task-master *)", 227 | "Bash(git commit:*)", 228 | "Bash(git add:*)", 229 | "Bash(npm run *)", 230 | "mcp__task_master_ai__*" 231 | ] 232 | } 233 | ``` 234 | 235 | ## Configuration & Setup 236 | 237 | ### API Keys Required 238 | 239 | At least **one** of these API keys must be configured: 240 | 241 | - `ANTHROPIC_API_KEY` (Claude models) - **Recommended** 242 | - `PERPLEXITY_API_KEY` (Research features) - **Highly recommended** 243 | - `OPENAI_API_KEY` (GPT models) 244 | - `GOOGLE_API_KEY` (Gemini models) 245 | - `MISTRAL_API_KEY` (Mistral models) 246 | - `OPENROUTER_API_KEY` (Multiple models) 247 | - `XAI_API_KEY` (Grok models) 248 | 249 | An API key is required for any provider used across any of the 3 roles defined 250 | in the `models` command. 251 | 252 | ### Model Configuration 253 | 254 | ```bash 255 | # Interactive setup (recommended) 256 | task-master models --setup 257 | 258 | # Set specific models 259 | task-master models --set-main claude-3-5-sonnet-20241022 260 | task-master models --set-research perplexity-llama-3.1-sonar-large-128k-online 261 | task-master models --set-fallback gpt-4o-mini 262 | ``` 263 | 264 | ## Task Structure & IDs 265 | 266 | ### Task ID Format 267 | 268 | - Main tasks: `1`, `2`, `3`, etc. 269 | - Subtasks: `1.1`, `1.2`, `2.1`, etc. 270 | - Sub-subtasks: `1.1.1`, `1.1.2`, etc. 271 | 272 | ### Task Status Values 273 | 274 | - `pending` - Ready to work on 275 | - `in-progress` - Currently being worked on 276 | - `done` - Completed and verified 277 | - `deferred` - Postponed 278 | - `cancelled` - No longer needed 279 | - `blocked` - Waiting on external factors 280 | 281 | ### Task Fields 282 | 283 | ```json 284 | { 285 | "id": "1.2", 286 | "title": "Implement user authentication", 287 | "description": "Set up JWT-based auth system", 288 | "status": "pending", 289 | "priority": "high", 290 | "dependencies": [ 291 | "1.1" 292 | ], 293 | "details": "Use bcrypt for hashing, JWT for tokens...", 294 | "testStrategy": "Unit tests for auth functions, integration tests for login flow", 295 | "subtasks": [] 296 | } 297 | ``` 298 | 299 | ## Claude Code Best Practices with Task Master 300 | 301 | ### Context Management 302 | 303 | - Use `/clear` between different tasks to maintain focus 304 | - This CLAUDE.md file is automatically loaded for context 305 | - Use `task-master show ` to pull specific task context when needed 306 | 307 | ### Iterative Implementation 308 | 309 | 1. `task-master show ` - Understand requirements 310 | 2. Explore codebase and plan implementation 311 | 3. `task-master update-subtask --id= --prompt="detailed plan"` - Log plan 312 | 4. `task-master set-status --id= --status=in-progress` - Start work 313 | 5. Implement code following logged plan 314 | 6. `task-master update-subtask --id= --prompt="what worked/didn't work"` - 315 | Log progress 316 | 7. `task-master set-status --id= --status=done` - Complete task 317 | 318 | ### Complex Workflows with Checklists 319 | 320 | For large migrations or multi-step processes: 321 | 322 | 1. Create a markdown PRD file describing the new changes: 323 | `touch task-migration-checklist.md` (prds can be .txt or .md) 324 | 2. Use Taskmaster to parse the new prd with `task-master parse-prd --append` ( 325 | also available in MCP) 326 | 3. Use Taskmaster to expand the newly generated tasks into subtasks. Consdier 327 | using `analyze-complexity` with the correct --to and --from IDs (the new ids) 328 | to identify the ideal subtask amounts for each task. Then expand them. 329 | 4. Work through items systematically, checking them off as completed 330 | 5. Use `task-master update-subtask` to log progress on each task/subtask and/or 331 | updating/researching them before/during implementation if getting stuck 332 | 333 | ### Git Integration 334 | 335 | Task Master works well with `gh` CLI: 336 | 337 | ```bash 338 | # Create PR for completed task 339 | gh pr create --title "Complete task 1.2: User authentication" --body "Implements JWT auth system as specified in task 1.2" 340 | 341 | # Reference task in commits 342 | git commit -m "feat: implement JWT auth (task 1.2)" 343 | ``` 344 | 345 | ### Parallel Development with Git Worktrees 346 | 347 | ```bash 348 | # Create worktrees for parallel task development 349 | git worktree add ../project-auth feature/auth-system 350 | git worktree add ../project-api feature/api-refactor 351 | 352 | # Run Claude Code in each worktree 353 | cd ../project-auth && claude # Terminal 1: Auth work 354 | cd ../project-api && claude # Terminal 2: API work 355 | ``` 356 | 357 | ## Troubleshooting 358 | 359 | ### AI Commands Failing 360 | 361 | ```bash 362 | # Check API keys are configured 363 | cat .env # For CLI usage 364 | 365 | # Verify model configuration 366 | task-master models 367 | 368 | # Test with different model 369 | task-master models --set-fallback gpt-4o-mini 370 | ``` 371 | 372 | ### MCP Connection Issues 373 | 374 | - Check `.mcp.json` configuration 375 | - Verify Node.js installation 376 | - Use `--mcp-debug` flag when starting Claude Code 377 | - Use CLI as fallback if MCP unavailable 378 | 379 | ### Task File Sync Issues 380 | 381 | ```bash 382 | # Regenerate task files from tasks.json 383 | task-master generate 384 | 385 | # Fix dependency issues 386 | task-master fix-dependencies 387 | ``` 388 | 389 | DO NOT RE-INITIALIZE. That will not do anything beyond re-adding the same 390 | Taskmaster core files. 391 | 392 | ## Important Notes 393 | 394 | ### AI-Powered Operations 395 | 396 | These commands make AI calls and may take up to a minute: 397 | 398 | - `parse_prd` / `task-master parse-prd` 399 | - `analyze_project_complexity` / `task-master analyze-complexity` 400 | - `expand_task` / `task-master expand` 401 | - `expand_all` / `task-master expand --all` 402 | - `add_task` / `task-master add-task` 403 | - `update` / `task-master update` 404 | - `update_task` / `task-master update-task` 405 | - `update_subtask` / `task-master update-subtask` 406 | 407 | ### File Management 408 | 409 | - Never manually edit `tasks.json` - use commands instead 410 | - Never manually edit `.taskmaster/config.json` - use `task-master models` 411 | - Task markdown files in `tasks/` are auto-generated 412 | - Run `task-master generate` after manual changes to tasks.json 413 | 414 | ### Claude Code Session Management 415 | 416 | - Use `/clear` frequently to maintain focused context 417 | - Create custom slash commands for repeated Task Master workflows 418 | - Configure tool allowlist to streamline permissions 419 | - Use headless mode for automation: `claude -p "task-master next"` 420 | 421 | ### Multi-Task Updates 422 | 423 | - Use `update --from=` to update multiple future tasks 424 | - Use `update-task --id=` for single task updates 425 | - Use `update-subtask --id=` for implementation logging 426 | 427 | ### Research Mode 428 | 429 | - Add `--research` flag for research-based AI enhancement 430 | - Requires a research model API key like Perplexity (`PERPLEXITY_API_KEY`) in 431 | environment 432 | - Provides more informed task creation and updates 433 | - Recommended for complex technical tasks 434 | 435 | --- 436 | 437 | _This guide ensures Claude Code has immediate access to Task Master's essential 438 | functionality for agentic development workflows._ 439 | -------------------------------------------------------------------------------- /GEMINI.md: -------------------------------------------------------------------------------- 1 | # Task Master AI - Agent Integration Guide 2 | 3 | ## Essential Commands 4 | 5 | ### Core Workflow Commands 6 | 7 | ```bash 8 | # Project Setup 9 | task-master init # Initialize Task Master in current project 10 | task-master parse-prd .taskmaster/docs/prd.txt # Generate tasks from PRD document 11 | task-master models --setup # Configure AI models interactively 12 | 13 | # Daily Development Workflow 14 | task-master list # Show all tasks with status 15 | task-master next # Get next available task to work on 16 | task-master show # View detailed task information (e.g., task-master show 1.2) 17 | task-master set-status --id= --status=done # Mark task complete 18 | 19 | # Task Management 20 | task-master add-task --prompt="description" --research # Add new task with AI assistance 21 | task-master expand --id= --research --force # Break task into subtasks 22 | task-master update-task --id= --prompt="changes" # Update specific task 23 | task-master update --from= --prompt="changes" # Update multiple tasks from ID onwards 24 | task-master update-subtask --id= --prompt="notes" # Add implementation notes to subtask 25 | 26 | # Analysis & Planning 27 | task-master analyze-complexity --research # Analyze task complexity 28 | task-master complexity-report # View complexity analysis 29 | task-master expand --all --research # Expand all eligible tasks 30 | 31 | # Dependencies & Organization 32 | task-master add-dependency --id= --depends-on= # Add task dependency 33 | task-master move --from= --to= # Reorganize task hierarchy 34 | task-master validate-dependencies # Check for dependency issues 35 | task-master generate # Update task markdown files (usually auto-called) 36 | ``` 37 | 38 | ## Key Files & Project Structure 39 | 40 | ### Core Files 41 | 42 | - `.taskmaster/tasks/tasks.json` - Main task data file (auto-managed) 43 | - `.taskmaster/config.json` - AI model configuration (use `task-master models`to 44 | modify) 45 | - `.taskmaster/docs/prd.txt` - Product Requirements Document for parsing 46 | - `.taskmaster/tasks/*.txt` - Individual task files (auto-generated from 47 | tasks.json) 48 | - `.env` - API keys for CLI usage 49 | 50 | ### Claude Code Integration Files 51 | 52 | - `CLAUDE.md` - Auto-loaded context for Claude Code (this file) 53 | - `.claude/settings.json` - Claude Code tool allowlist and preferences 54 | - `.claude/commands/` - Custom slash commands for repeated workflows 55 | - `.mcp.json` - MCP server configuration (project-specific) 56 | 57 | ### Directory Structure 58 | 59 | ``` 60 | project/ 61 | ├── .taskmaster/ 62 | │ ├── tasks/ # Task files directory 63 | │ │ ├── tasks.json # Main task database 64 | │ │ ├── task-1.md # Individual task files 65 | │ │ └── task-2.md 66 | │ ├── docs/ # Documentation directory 67 | │ │ ├── prd.txt # Product requirements 68 | │ ├── reports/ # Analysis reports directory 69 | │ │ └── task-complexity-report.json 70 | │ ├── templates/ # Template files 71 | │ │ └── example_prd.txt # Example PRD template 72 | │ └── config.json # AI models & settings 73 | ├── .claude/ 74 | │ ├── settings.json # Claude Code configuration 75 | │ └── commands/ # Custom slash commands 76 | ├── .env # API keys 77 | ├── .mcp.json # MCP configuration 78 | └── CLAUDE.md # This file - auto-loaded by Claude Code 79 | ``` 80 | 81 | ## MCP Integration 82 | 83 | Task Master provides an MCP server that Claude Code can connect to. Configure in 84 | `.mcp.json`: 85 | 86 | ```json 87 | { 88 | "mcpServers": { 89 | "task-master-ai": { 90 | "command": "npx", 91 | "args": [ 92 | "-y", 93 | "--package=task-master-ai", 94 | "task-master-ai" 95 | ], 96 | "env": { 97 | "ANTHROPIC_API_KEY": "your_key_here", 98 | "PERPLEXITY_API_KEY": "your_key_here", 99 | "OPENAI_API_KEY": "OPENAI_API_KEY_HERE", 100 | "GOOGLE_API_KEY": "GOOGLE_API_KEY_HERE", 101 | "XAI_API_KEY": "XAI_API_KEY_HERE", 102 | "OPENROUTER_API_KEY": "OPENROUTER_API_KEY_HERE", 103 | "MISTRAL_API_KEY": "MISTRAL_API_KEY_HERE", 104 | "AZURE_OPENAI_API_KEY": "AZURE_OPENAI_API_KEY_HERE", 105 | "OLLAMA_API_KEY": "OLLAMA_API_KEY_HERE" 106 | } 107 | } 108 | } 109 | } 110 | ``` 111 | 112 | ### Essential MCP Tools 113 | 114 | ```javascript 115 | help; // = shows available taskmaster commands 116 | // Project setup 117 | initialize_project; // = task-master init 118 | parse_prd; // = task-master parse-prd 119 | 120 | // Daily workflow 121 | get_tasks; // = task-master list 122 | next_task; // = task-master next 123 | get_task; // = task-master show 124 | set_task_status; // = task-master set-status 125 | 126 | // Task management 127 | add_task; // = task-master add-task 128 | expand_task; // = task-master expand 129 | update_task; // = task-master update-task 130 | update_subtask; // = task-master update-subtask 131 | update; // = task-master update 132 | 133 | // Analysis 134 | analyze_project_complexity; // = task-master analyze-complexity 135 | complexity_report; // = task-master complexity-report 136 | ``` 137 | 138 | ## Claude Code Workflow Integration 139 | 140 | ### Standard Development Workflow 141 | 142 | #### 1. Project Initialization 143 | 144 | ```bash 145 | # Initialize Task Master 146 | task-master init 147 | 148 | # Create or obtain PRD, then parse it 149 | task-master parse-prd .taskmaster/docs/prd.txt 150 | 151 | # Analyze complexity and expand tasks 152 | task-master analyze-complexity --research 153 | task-master expand --all --research 154 | ``` 155 | 156 | If tasks already exist, another PRD can be parsed (with new information only!) 157 | using parse-prd with --append flag. This will add the generated tasks to the 158 | existing list of tasks.. 159 | 160 | #### 2. Daily Development Loop 161 | 162 | ```bash 163 | # Start each session 164 | task-master next # Find next available task 165 | task-master show # Review task details 166 | 167 | # During implementation, check in code context into the tasks and subtasks 168 | task-master update-subtask --id= --prompt="implementation notes..." 169 | 170 | # Complete tasks 171 | task-master set-status --id= --status=done 172 | ``` 173 | 174 | #### 3. Multi-Claude Workflows 175 | 176 | For complex projects, use multiple Claude Code sessions: 177 | 178 | ```bash 179 | # Terminal 1: Main implementation 180 | cd project && claude 181 | 182 | # Terminal 2: Testing and validation 183 | cd project-test-worktree && claude 184 | 185 | # Terminal 3: Documentation updates 186 | cd project-docs-worktree && claude 187 | ``` 188 | 189 | ### Custom Slash Commands 190 | 191 | Create `.claude/commands/taskmaster-next.md`: 192 | 193 | ```markdown 194 | Find the next available Task Master task and show its details. 195 | 196 | Steps: 197 | 198 | 1. Run `task-master next` to get the next task 199 | 2. If a task is available, run `task-master show ` for full details 200 | 3. Provide a summary of what needs to be implemented 201 | 4. Suggest the first implementation step 202 | ``` 203 | 204 | Create `.claude/commands/taskmaster-complete.md`: 205 | 206 | ```markdown 207 | Complete a Task Master task: $ARGUMENTS 208 | 209 | Steps: 210 | 211 | 1. Review the current task with `task-master show $ARGUMENTS` 212 | 2. Verify all implementation is complete 213 | 3. Run any tests related to this task 214 | 4. Mark as complete: `task-master set-status --id=$ARGUMENTS --status=done` 215 | 5. Show the next available task with `task-master next` 216 | ``` 217 | 218 | ## Tool Allowlist Recommendations 219 | 220 | Add to `.claude/settings.json`: 221 | 222 | ```json 223 | { 224 | "allowedTools": [ 225 | "Edit", 226 | "Bash(task-master *)", 227 | "Bash(git commit:*)", 228 | "Bash(git add:*)", 229 | "Bash(npm run *)", 230 | "mcp__task_master_ai__*" 231 | ] 232 | } 233 | ``` 234 | 235 | ## Configuration & Setup 236 | 237 | ### API Keys Required 238 | 239 | At least **one** of these API keys must be configured: 240 | 241 | - `ANTHROPIC_API_KEY` (Claude models) - **Recommended** 242 | - `PERPLEXITY_API_KEY` (Research features) - **Highly recommended** 243 | - `OPENAI_API_KEY` (GPT models) 244 | - `GOOGLE_API_KEY` (Gemini models) 245 | - `MISTRAL_API_KEY` (Mistral models) 246 | - `OPENROUTER_API_KEY` (Multiple models) 247 | - `XAI_API_KEY` (Grok models) 248 | 249 | An API key is required for any provider used across any of the 3 roles defined 250 | in the `models` command. 251 | 252 | ### Model Configuration 253 | 254 | ```bash 255 | # Interactive setup (recommended) 256 | task-master models --setup 257 | 258 | # Set specific models 259 | task-master models --set-main claude-3-5-sonnet-20241022 260 | task-master models --set-research perplexity-llama-3.1-sonar-large-128k-online 261 | task-master models --set-fallback gpt-4o-mini 262 | ``` 263 | 264 | ## Task Structure & IDs 265 | 266 | ### Task ID Format 267 | 268 | - Main tasks: `1`, `2`, `3`, etc. 269 | - Subtasks: `1.1`, `1.2`, `2.1`, etc. 270 | - Sub-subtasks: `1.1.1`, `1.1.2`, etc. 271 | 272 | ### Task Status Values 273 | 274 | - `pending` - Ready to work on 275 | - `in-progress` - Currently being worked on 276 | - `done` - Completed and verified 277 | - `deferred` - Postponed 278 | - `cancelled` - No longer needed 279 | - `blocked` - Waiting on external factors 280 | 281 | ### Task Fields 282 | 283 | ```json 284 | { 285 | "id": "1.2", 286 | "title": "Implement user authentication", 287 | "description": "Set up JWT-based auth system", 288 | "status": "pending", 289 | "priority": "high", 290 | "dependencies": [ 291 | "1.1" 292 | ], 293 | "details": "Use bcrypt for hashing, JWT for tokens...", 294 | "testStrategy": "Unit tests for auth functions, integration tests for login flow", 295 | "subtasks": [] 296 | } 297 | ``` 298 | 299 | ## Claude Code Best Practices with Task Master 300 | 301 | ### Context Management 302 | 303 | - Use `/clear` between different tasks to maintain focus 304 | - This CLAUDE.md file is automatically loaded for context 305 | - Use `task-master show ` to pull specific task context when needed 306 | 307 | ### Iterative Implementation 308 | 309 | 1. `task-master show ` - Understand requirements 310 | 2. Explore codebase and plan implementation 311 | 3. `task-master update-subtask --id= --prompt="detailed plan"` - Log plan 312 | 4. `task-master set-status --id= --status=in-progress` - Start work 313 | 5. Implement code following logged plan 314 | 6. `task-master update-subtask --id= --prompt="what worked/didn't work"` - 315 | Log progress 316 | 7. `task-master set-status --id= --status=done` - Complete task 317 | 318 | ### Complex Workflows with Checklists 319 | 320 | For large migrations or multi-step processes: 321 | 322 | 1. Create a markdown PRD file describing the new changes: 323 | `touch task-migration-checklist.md` (prds can be .txt or .md) 324 | 2. Use Taskmaster to parse the new prd with `task-master parse-prd --append` ( 325 | also available in MCP) 326 | 3. Use Taskmaster to expand the newly generated tasks into subtasks. Consdier 327 | using `analyze-complexity` with the correct --to and --from IDs (the new ids) 328 | to identify the ideal subtask amounts for each task. Then expand them. 329 | 4. Work through items systematically, checking them off as completed 330 | 5. Use `task-master update-subtask` to log progress on each task/subtask and/or 331 | updating/researching them before/during implementation if getting stuck 332 | 333 | ### Git Integration 334 | 335 | Task Master works well with `gh` CLI: 336 | 337 | ```bash 338 | # Create PR for completed task 339 | gh pr create --title "Complete task 1.2: User authentication" --body "Implements JWT auth system as specified in task 1.2" 340 | 341 | # Reference task in commits 342 | git commit -m "feat: implement JWT auth (task 1.2)" 343 | ``` 344 | 345 | ### Parallel Development with Git Worktrees 346 | 347 | ```bash 348 | # Create worktrees for parallel task development 349 | git worktree add ../project-auth feature/auth-system 350 | git worktree add ../project-api feature/api-refactor 351 | 352 | # Run Claude Code in each worktree 353 | cd ../project-auth && claude # Terminal 1: Auth work 354 | cd ../project-api && claude # Terminal 2: API work 355 | ``` 356 | 357 | ## Troubleshooting 358 | 359 | ### AI Commands Failing 360 | 361 | ```bash 362 | # Check API keys are configured 363 | cat .env # For CLI usage 364 | 365 | # Verify model configuration 366 | task-master models 367 | 368 | # Test with different model 369 | task-master models --set-fallback gpt-4o-mini 370 | ``` 371 | 372 | ### MCP Connection Issues 373 | 374 | - Check `.mcp.json` configuration 375 | - Verify Node.js installation 376 | - Use `--mcp-debug` flag when starting Claude Code 377 | - Use CLI as fallback if MCP unavailable 378 | 379 | ### Task File Sync Issues 380 | 381 | ```bash 382 | # Regenerate task files from tasks.json 383 | task-master generate 384 | 385 | # Fix dependency issues 386 | task-master fix-dependencies 387 | ``` 388 | 389 | DO NOT RE-INITIALIZE. That will not do anything beyond re-adding the same 390 | Taskmaster core files. 391 | 392 | ## Important Notes 393 | 394 | ### AI-Powered Operations 395 | 396 | These commands make AI calls and may take up to a minute: 397 | 398 | - `parse_prd` / `task-master parse-prd` 399 | - `analyze_project_complexity` / `task-master analyze-complexity` 400 | - `expand_task` / `task-master expand` 401 | - `expand_all` / `task-master expand --all` 402 | - `add_task` / `task-master add-task` 403 | - `update` / `task-master update` 404 | - `update_task` / `task-master update-task` 405 | - `update_subtask` / `task-master update-subtask` 406 | 407 | ### File Management 408 | 409 | - Never manually edit `tasks.json` - use commands instead 410 | - Never manually edit `.taskmaster/config.json` - use `task-master models` 411 | - Task markdown files in `tasks/` are auto-generated 412 | - Run `task-master generate` after manual changes to tasks.json 413 | 414 | ### Claude Code Session Management 415 | 416 | - Use `/clear` frequently to maintain focused context 417 | - Create custom slash commands for repeated Task Master workflows 418 | - Configure tool allowlist to streamline permissions 419 | - Use headless mode for automation: `claude -p "task-master next"` 420 | 421 | ### Multi-Task Updates 422 | 423 | - Use `update --from=` to update multiple future tasks 424 | - Use `update-task --id=` for single task updates 425 | - Use `update-subtask --id=` for implementation logging 426 | 427 | ### Research Mode 428 | 429 | - Add `--research` flag for research-based AI enhancement 430 | - Requires a research model API key like Perplexity (`PERPLEXITY_API_KEY`) in 431 | environment 432 | - Provides more informed task creation and updates 433 | - Recommended for complex technical tasks 434 | 435 | --- 436 | 437 | _This guide ensures Claude Code has immediate access to Task Master's essential 438 | functionality for agentic development workflows._ 439 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Firebase Authentication with Flutter & DDD 2 | 3 | This repository provides a comprehensive example of a Flutter application that implements Firebase Authentication using a Domain-Driven Design (DDD) architecture. It leverages modern Flutter technologies like Hooks Riverpod for state management and Freezed for immutable data classes. 4 | 5 | This project serves as a robust starting point for building scalable and maintainable Flutter applications. 6 | 7 | ## Features 8 | 9 | * **Firebase Authentication:** Email & Password sign-in. 10 | * **Domain-Driven Design (DDD):** A clean and separated architecture to promote maintainability and testability. 11 | * **Riverpod:** State management for a reactive and predictable state. 12 | * **Freezed:** Code generation for immutable data classes and unions. 13 | * **Error Handling:** Clear separation of domain and application failures. 14 | 15 | ## Project Structure 16 | 17 | The project follows a layered architecture inspired by Domain-Driven Design. For a detailed explanation, please see the [Architecture Guide](./guides/architecture.md). 18 | 19 | ``` 20 | lib/ 21 | ├── application/ # Application Layer (Use Cases/BLoCs) 22 | ├── domain/ # Domain Layer (Entities, Value Objects, Interfaces) 23 | ├── services/ # Infrastructure Layer (Firebase Implementation) 24 | └── screens/ # Presentation Layer (UI) 25 | ``` 26 | 27 | ## Getting Started 28 | 29 | To get a local copy up and running, please follow the detailed instructions in the [Setup Guide](./guides/setup.md). 30 | 31 | ## Architecture 32 | 33 | This project is built with a strong emphasis on clean architecture and Domain-Driven Design principles. To understand the structure, layers, and design decisions, please refer to the [Architecture Guide](./guides/architecture.md). 34 | 35 | ## Contributing 36 | 37 | Contributions are what make the open-source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. 38 | 39 | If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement". 40 | 41 | 1. Fork the Project 42 | 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) 43 | 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`) 44 | 4. Push to the Branch (`git push origin feature/AmazingFeature`) 45 | 5. Open a Pull Request 46 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:flutter_lints/flutter.yaml 2 | 3 | analyzer: 4 | exclude: 5 | - lib/generated_plugin_registrant.dart 6 | - lib/**.freezed.dart 7 | - lib/**.g.dart 8 | - lib/**.config.dart 9 | - lib/**.gen.dart 10 | errors: 11 | prefer_const_constructors: error 12 | annotate_overrides: error 13 | prefer_double_quotes: error 14 | sort_pub_dependencies: error 15 | invalid_annotation_target: ignore 16 | 17 | 18 | 19 | linter: 20 | rules: 21 | prefer_double_quotes: true 22 | sort_pub_dependencies: true 23 | annotate_overrides: true 24 | file_names: false 25 | overridden_fields: false 26 | prefer_single_quotes: false 27 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "com.android.application" 3 | id "kotlin-android" 4 | id "dev.flutter.flutter-gradle-plugin" 5 | } 6 | 7 | def localProperties = new Properties() 8 | def localPropertiesFile = rootProject.file('local.properties') 9 | if (localPropertiesFile.exists()) { 10 | localPropertiesFile.withReader('UTF-8') { reader -> 11 | localProperties.load(reader) 12 | } 13 | } 14 | 15 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 16 | if (flutterVersionCode == null) { 17 | flutterVersionCode = '1' 18 | } 19 | 20 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 21 | if (flutterVersionName == null) { 22 | flutterVersionName = '1.0' 23 | } 24 | 25 | android { 26 | namespace "com.example.firebase_authentication_flutter_ddd" 27 | compileSdkVersion flutter.compileSdkVersion 28 | ndkVersion flutter.ndkVersion 29 | 30 | compileOptions { 31 | sourceCompatibility JavaVersion.VERSION_1_8 32 | targetCompatibility JavaVersion.VERSION_1_8 33 | } 34 | 35 | kotlinOptions { 36 | jvmTarget = '1.8' 37 | } 38 | 39 | sourceSets { 40 | main.java.srcDirs += 'src/main/kotlin' 41 | } 42 | 43 | defaultConfig { 44 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 45 | applicationId "com.example.firebase_authentication_flutter_ddd" 46 | // You can update the following values to match your application needs. 47 | // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. 48 | minSdkVersion flutter.minSdkVersion 49 | targetSdkVersion flutter.targetSdkVersion 50 | versionCode flutterVersionCode.toInteger() 51 | versionName flutterVersionName 52 | } 53 | 54 | buildTypes { 55 | release { 56 | // TODO: Add your own signing config for the release build. 57 | // Signing with the debug keys for now, so `flutter run --release` works. 58 | signingConfig signingConfigs.debug 59 | } 60 | } 61 | } 62 | 63 | flutter { 64 | source '../..' 65 | } 66 | 67 | dependencies {} 68 | -------------------------------------------------------------------------------- /android/app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.application") 3 | id("kotlin-android") 4 | // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. 5 | id("dev.flutter.flutter-gradle-plugin") 6 | } 7 | 8 | android { 9 | namespace = "com.example.firebase_auth_flutter_ddd" 10 | compileSdk = flutter.compileSdkVersion 11 | ndkVersion = flutter.ndkVersion 12 | 13 | compileOptions { 14 | sourceCompatibility = JavaVersion.VERSION_11 15 | targetCompatibility = JavaVersion.VERSION_11 16 | } 17 | 18 | kotlinOptions { 19 | jvmTarget = JavaVersion.VERSION_11.toString() 20 | } 21 | 22 | defaultConfig { 23 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 24 | applicationId = "com.example.firebase_auth_flutter_ddd" 25 | // You can update the following values to match your application needs. 26 | // For more information, see: https://flutter.dev/to/review-gradle-config. 27 | minSdk = flutter.minSdkVersion 28 | targetSdk = flutter.targetSdkVersion 29 | versionCode = flutter.versionCode 30 | versionName = flutter.versionName 31 | } 32 | 33 | buildTypes { 34 | release { 35 | // TODO: Add your own signing config for the release build. 36 | // Signing with the debug keys for now, so `flutter run --release` works. 37 | signingConfig = signingConfigs.getByName("debug") 38 | } 39 | } 40 | } 41 | 42 | flutter { 43 | source = "../.." 44 | } 45 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 14 | 18 | 22 | 23 | 24 | 25 | 26 | 27 | 29 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/example/firebase_auth_flutter_ddd/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.firebase_auth_flutter_ddd 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity : FlutterActivity() 6 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/example/firebase_authentication_flutter_ddd/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.firebase_authentication_flutter_ddd 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pythonhubdev/flutter_ddd_firebase_auth/10b311606657916f4397ad1618aae9812a67f24d/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pythonhubdev/flutter_ddd_firebase_auth/10b311606657916f4397ad1618aae9812a67f24d/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pythonhubdev/flutter_ddd_firebase_auth/10b311606657916f4397ad1618aae9812a67f24d/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pythonhubdev/flutter_ddd_firebase_auth/10b311606657916f4397ad1618aae9812a67f24d/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pythonhubdev/flutter_ddd_firebase_auth/10b311606657916f4397ad1618aae9812a67f24d/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.7.10' 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 10 | } 11 | } 12 | 13 | allprojects { 14 | repositories { 15 | google() 16 | mavenCentral() 17 | } 18 | } 19 | 20 | rootProject.buildDir = '../build' 21 | subprojects { 22 | project.buildDir = "${rootProject.buildDir}/${project.name}" 23 | } 24 | subprojects { 25 | project.evaluationDependsOn(':app') 26 | } 27 | 28 | tasks.register("clean", Delete) { 29 | delete rootProject.buildDir 30 | } 31 | -------------------------------------------------------------------------------- /android/build.gradle.kts: -------------------------------------------------------------------------------- 1 | allprojects { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | } 6 | } 7 | 8 | val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get() 9 | rootProject.layout.buildDirectory.value(newBuildDir) 10 | 11 | subprojects { 12 | val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) 13 | project.layout.buildDirectory.value(newSubprojectBuildDir) 14 | } 15 | subprojects { 16 | project.evaluationDependsOn(":app") 17 | } 18 | 19 | tasks.register("clean") { 20 | delete(rootProject.layout.buildDirectory) 21 | } 22 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx4G 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip 6 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | def flutterSdkPath = { 3 | def properties = new Properties() 4 | file("local.properties").withInputStream { properties.load(it) } 5 | def flutterSdkPath = properties.getProperty("flutter.sdk") 6 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 7 | return flutterSdkPath 8 | } 9 | settings.ext.flutterSdkPath = flutterSdkPath() 10 | 11 | includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle") 12 | 13 | repositories { 14 | google() 15 | mavenCentral() 16 | gradlePluginPortal() 17 | } 18 | 19 | plugins { 20 | id "dev.flutter.flutter-gradle-plugin" version "1.0.0" apply false 21 | } 22 | } 23 | 24 | plugins { 25 | id "dev.flutter.flutter-plugin-loader" version "1.0.0" 26 | id "com.android.application" version "7.3.0" apply false 27 | } 28 | 29 | include ":app" 30 | -------------------------------------------------------------------------------- /android/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | val flutterSdkPath = run { 3 | val properties = java.util.Properties() 4 | file("local.properties").inputStream().use { properties.load(it) } 5 | val flutterSdkPath = properties.getProperty("flutter.sdk") 6 | require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } 7 | flutterSdkPath 8 | } 9 | 10 | includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") 11 | 12 | repositories { 13 | google() 14 | mavenCentral() 15 | gradlePluginPortal() 16 | } 17 | } 18 | 19 | plugins { 20 | id("dev.flutter.flutter-plugin-loader") version "1.0.0" 21 | id("com.android.application") version "8.7.3" apply false 22 | id("org.jetbrains.kotlin.android") version "2.1.0" apply false 23 | } 24 | 25 | include(":app") 26 | -------------------------------------------------------------------------------- /guides/architecture.md: -------------------------------------------------------------------------------- 1 | # Architecture Overview 2 | 3 | This project follows the principles of Domain-Driven Design (DDD) to create a scalable and maintainable Flutter application. The architecture is divided into several layers, each with its own distinct responsibilities. 4 | 5 | ## Layers 6 | 7 | The project is structured into the following layers: 8 | 9 | * **Domain:** This is the core of the application and is independent of any other layer. It contains the business logic, entities, and value objects. 10 | * **Application:** This layer orchestrates the domain layer and is responsible for handling application-specific use cases. It contains the application services and data transfer objects (DTOs). 11 | * **Services (Infrastructure):** This layer is responsible for implementing the interfaces defined in the domain layer. It contains the implementation of the repositories and other external services. 12 | * **Screens (Presentation):** This is the UI layer of the application. It is responsible for displaying the data to the user and handling user input. 13 | 14 | ## Directory Structure 15 | 16 | The project's directory structure reflects the layered architecture: 17 | 18 | ``` 19 | lib 20 | ├── application 21 | │ └── authentication 22 | ├── domain 23 | │ ├── authentication 24 | │ └── core 25 | ├── screens 26 | │ └── utils 27 | └── services 28 | └── authentication 29 | ``` 30 | 31 | ### Domain Layer 32 | 33 | The `domain` directory contains the following: 34 | 35 | * **`authentication`:** This directory contains the core authentication-related business logic, including: 36 | * `auth_failures.dart`: Defines the possible failures that can occur during authentication. 37 | * `i_auth_facade.dart`: Defines the interface for the authentication service. 38 | * `auth_value_objects.dart`: Defines the value objects used in the authentication domain. 39 | * **`core`:** This directory contains the core building blocks of the domain layer, such as the `ValueObject` and `Error` classes. 40 | 41 | ### Application Layer 42 | 43 | The `application` directory contains the following: 44 | 45 | * **`authentication`:** This directory contains the application-level logic for authentication, including: 46 | * `auth_events.dart`: Defines the events that can be triggered by the UI. 47 | * `auth_state_controller.dart`: The BLoC (or Riverpod equivalent) that manages the state of the authentication flow. 48 | * `auth_states.dart`: Defines the different states of the authentication flow. 49 | 50 | ### Services (Infrastructure) Layer 51 | 52 | The `services` directory contains the following: 53 | 54 | * **`authentication`:** This directory contains the implementation of the `IAuthFacade` interface, which interacts with Firebase Authentication. 55 | 56 | ### Screens (Presentation) Layer 57 | 58 | The `screens` directory contains the following: 59 | 60 | * **`login_page.dart`:** The UI for the login screen. 61 | * **`home_page.dart`:** The UI for the home screen, which is displayed after the user has successfully authenticated. 62 | * **`utils`:** This directory contains utility widgets and functions used across the UI. 63 | 64 | ## State Management 65 | 66 | This project uses **Riverpod** for state management. The `auth_state_controller.dart` file in the `application` layer is responsible for managing the state of the authentication flow. 67 | 68 | ## Code Generation 69 | 70 | This project uses the **Freezed** package to generate immutable classes and unions. The `build_runner` package is used to run the code generation. 71 | -------------------------------------------------------------------------------- /guides/setup.md: -------------------------------------------------------------------------------- 1 | # Project Setup Guide 2 | 3 | This guide will walk you through setting up the project on your local machine. 4 | 5 | ## Prerequisites 6 | 7 | Before you begin, ensure you have the following installed: 8 | 9 | * **Flutter SDK:** Make sure you have the Flutter SDK installed and configured correctly. You can find instructions on the [official Flutter website](https://flutter.dev/docs/get-started/install). 10 | * **Firebase Account:** You will need a Firebase account to connect the application to a Firebase project. 11 | 12 | ## Setup Steps 13 | 14 | 1. **Clone the Repository:** 15 | 16 | ```bash 17 | git clone https://github.com/your-username/firebase_authentication_flutter_ddd.git 18 | cd firebase_authentication_flutter_ddd 19 | ``` 20 | 21 | 2. **Configure Firebase:** 22 | 23 | * Create a new Firebase project on the [Firebase Console](https://console.firebase.google.com/). 24 | * Follow the instructions to add a new Flutter application to your project. 25 | * Download the `google-services.json` file for Android and the `GoogleService-Info.plist` file for iOS. 26 | * Place the `google-services.json` file in the `android/app` directory. 27 | * Place the `GoogleService-Info.plist` file in the `ios/Runner` directory. 28 | 29 | 3. **Set Up Environment Variables:** 30 | 31 | * Create a `.env` file in the root of the project by copying the `.env.example` file: 32 | 33 | ```bash 34 | cp .env.example .env 35 | ``` 36 | 37 | * Open the `.env` file and add the necessary API keys. For this project, you will need to add your `OLLAMA_API_KEY`. 38 | 39 | 4. **Install Dependencies:** 40 | 41 | * Run the following command to install the project's dependencies: 42 | 43 | ```bash 44 | flutter pub get 45 | ``` 46 | 47 | 5. **Run the Application:** 48 | 49 | * You can now run the application on an emulator or a physical device: 50 | 51 | ```bash 52 | flutter run 53 | ``` 54 | 55 | ## Build Runner 56 | 57 | This project uses the `build_runner` package to generate code. If you make any changes to the models or other generated files, you will need to run the following command: 58 | 59 | ```bash 60 | flutter pub run build_runner build 61 | ``` 62 | 63 | This will regenerate the necessary files. 64 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 12.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | platform :ios, '14.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | target 'RunnerTests' do 36 | inherit! :search_paths 37 | end 38 | end 39 | 40 | post_install do |installer| 41 | installer.pods_project.targets.each do |target| 42 | flutter_additional_ios_build_settings(target) 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /ios/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Firebase/Auth (11.15.0): 3 | - Firebase/CoreOnly 4 | - FirebaseAuth (~> 11.15.0) 5 | - Firebase/CoreOnly (11.15.0): 6 | - FirebaseCore (~> 11.15.0) 7 | - firebase_auth (5.7.0): 8 | - Firebase/Auth (= 11.15.0) 9 | - firebase_core 10 | - Flutter 11 | - firebase_core (3.15.2): 12 | - Firebase/CoreOnly (= 11.15.0) 13 | - Flutter 14 | - FirebaseAppCheckInterop (11.15.0) 15 | - FirebaseAuth (11.15.0): 16 | - FirebaseAppCheckInterop (~> 11.0) 17 | - FirebaseAuthInterop (~> 11.0) 18 | - FirebaseCore (~> 11.15.0) 19 | - FirebaseCoreExtension (~> 11.15.0) 20 | - GoogleUtilities/AppDelegateSwizzler (~> 8.1) 21 | - GoogleUtilities/Environment (~> 8.1) 22 | - GTMSessionFetcher/Core (< 5.0, >= 3.4) 23 | - RecaptchaInterop (~> 101.0) 24 | - FirebaseAuthInterop (11.15.0) 25 | - FirebaseCore (11.15.0): 26 | - FirebaseCoreInternal (~> 11.15.0) 27 | - GoogleUtilities/Environment (~> 8.1) 28 | - GoogleUtilities/Logger (~> 8.1) 29 | - FirebaseCoreExtension (11.15.0): 30 | - FirebaseCore (~> 11.15.0) 31 | - FirebaseCoreInternal (11.15.0): 32 | - "GoogleUtilities/NSData+zlib (~> 8.1)" 33 | - Flutter (1.0.0) 34 | - GoogleUtilities/AppDelegateSwizzler (8.1.0): 35 | - GoogleUtilities/Environment 36 | - GoogleUtilities/Logger 37 | - GoogleUtilities/Network 38 | - GoogleUtilities/Privacy 39 | - GoogleUtilities/Environment (8.1.0): 40 | - GoogleUtilities/Privacy 41 | - GoogleUtilities/Logger (8.1.0): 42 | - GoogleUtilities/Environment 43 | - GoogleUtilities/Privacy 44 | - GoogleUtilities/Network (8.1.0): 45 | - GoogleUtilities/Logger 46 | - "GoogleUtilities/NSData+zlib" 47 | - GoogleUtilities/Privacy 48 | - GoogleUtilities/Reachability 49 | - "GoogleUtilities/NSData+zlib (8.1.0)": 50 | - GoogleUtilities/Privacy 51 | - GoogleUtilities/Privacy (8.1.0) 52 | - GoogleUtilities/Reachability (8.1.0): 53 | - GoogleUtilities/Logger 54 | - GoogleUtilities/Privacy 55 | - GTMSessionFetcher/Core (4.5.0) 56 | - RecaptchaInterop (101.0.0) 57 | 58 | DEPENDENCIES: 59 | - firebase_auth (from `.symlinks/plugins/firebase_auth/ios`) 60 | - firebase_core (from `.symlinks/plugins/firebase_core/ios`) 61 | - Flutter (from `Flutter`) 62 | 63 | SPEC REPOS: 64 | trunk: 65 | - Firebase 66 | - FirebaseAppCheckInterop 67 | - FirebaseAuth 68 | - FirebaseAuthInterop 69 | - FirebaseCore 70 | - FirebaseCoreExtension 71 | - FirebaseCoreInternal 72 | - GoogleUtilities 73 | - GTMSessionFetcher 74 | - RecaptchaInterop 75 | 76 | EXTERNAL SOURCES: 77 | firebase_auth: 78 | :path: ".symlinks/plugins/firebase_auth/ios" 79 | firebase_core: 80 | :path: ".symlinks/plugins/firebase_core/ios" 81 | Flutter: 82 | :path: Flutter 83 | 84 | SPEC CHECKSUMS: 85 | Firebase: d99ac19b909cd2c548339c2241ecd0d1599ab02e 86 | firebase_auth: 5342db41af2ba5ed32a6177d9e326eecbebda912 87 | firebase_core: 99a37263b3c27536063a7b601d9e2a49400a433c 88 | FirebaseAppCheckInterop: 06fe5a3799278ae4667e6c432edd86b1030fa3df 89 | FirebaseAuth: a6575e5fbf46b046c58dc211a28a5fbdd8d4c83b 90 | FirebaseAuthInterop: 7087d7a4ee4bc4de019b2d0c240974ed5d89e2fd 91 | FirebaseCore: efb3893e5b94f32b86e331e3bd6dadf18b66568e 92 | FirebaseCoreExtension: edbd30474b5ccf04e5f001470bdf6ea616af2435 93 | FirebaseCoreInternal: 9afa45b1159304c963da48addb78275ef701c6b4 94 | Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 95 | GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 96 | GTMSessionFetcher: fc75fc972958dceedee61cb662ae1da7a83a91cf 97 | RecaptchaInterop: 11e0b637842dfb48308d242afc3f448062325aba 98 | 99 | PODFILE CHECKSUM: 1959d098c91d8a792531a723c4a9d7e9f6a01e38 100 | 101 | COCOAPODS: 1.16.2 102 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 38 | 39 | 40 | 41 | 44 | 50 | 51 | 52 | 53 | 54 | 66 | 68 | 74 | 75 | 76 | 77 | 83 | 85 | 91 | 92 | 93 | 94 | 96 | 97 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @main 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pythonhubdev/flutter_ddd_firebase_auth/10b311606657916f4397ad1618aae9812a67f24d/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pythonhubdev/flutter_ddd_firebase_auth/10b311606657916f4397ad1618aae9812a67f24d/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pythonhubdev/flutter_ddd_firebase_auth/10b311606657916f4397ad1618aae9812a67f24d/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pythonhubdev/flutter_ddd_firebase_auth/10b311606657916f4397ad1618aae9812a67f24d/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pythonhubdev/flutter_ddd_firebase_auth/10b311606657916f4397ad1618aae9812a67f24d/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pythonhubdev/flutter_ddd_firebase_auth/10b311606657916f4397ad1618aae9812a67f24d/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pythonhubdev/flutter_ddd_firebase_auth/10b311606657916f4397ad1618aae9812a67f24d/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pythonhubdev/flutter_ddd_firebase_auth/10b311606657916f4397ad1618aae9812a67f24d/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pythonhubdev/flutter_ddd_firebase_auth/10b311606657916f4397ad1618aae9812a67f24d/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pythonhubdev/flutter_ddd_firebase_auth/10b311606657916f4397ad1618aae9812a67f24d/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pythonhubdev/flutter_ddd_firebase_auth/10b311606657916f4397ad1618aae9812a67f24d/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pythonhubdev/flutter_ddd_firebase_auth/10b311606657916f4397ad1618aae9812a67f24d/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pythonhubdev/flutter_ddd_firebase_auth/10b311606657916f4397ad1618aae9812a67f24d/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pythonhubdev/flutter_ddd_firebase_auth/10b311606657916f4397ad1618aae9812a67f24d/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pythonhubdev/flutter_ddd_firebase_auth/10b311606657916f4397ad1618aae9812a67f24d/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pythonhubdev/flutter_ddd_firebase_auth/10b311606657916f4397ad1618aae9812a67f24d/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pythonhubdev/flutter_ddd_firebase_auth/10b311606657916f4397ad1618aae9812a67f24d/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pythonhubdev/flutter_ddd_firebase_auth/10b311606657916f4397ad1618aae9812a67f24d/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | Firebase Authentication Flutter Ddd 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | firebase_authentication_flutter_ddd 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(FLUTTER_BUILD_NUMBER) 25 | LSRequiresIPhoneOS 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | CADisableMinimumFrameDurationOnPhone 45 | 46 | UIApplicationSupportsIndirectInputEvents 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /ios/RunnerTests/RunnerTests.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 3 | import XCTest 4 | 5 | class RunnerTests: XCTestCase { 6 | 7 | func testExample() { 8 | // If you add code to the Runner application, consider adding tests here. 9 | // See https://developer.apple.com/documentation/xctest for more information about using XCTest. 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /lib/app.dart: -------------------------------------------------------------------------------- 1 | import "package:flutter/foundation.dart"; 2 | import "package:flutter/material.dart"; 3 | import "package:flutter/services.dart"; 4 | 5 | import "core/theme/app_theme.dart"; 6 | import "screens/login_page.dart"; 7 | 8 | class FirebaseAuthenticationDDD extends StatelessWidget { 9 | const FirebaseAuthenticationDDD({super.key}); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | SystemChrome.setSystemUIOverlayStyle( 14 | const SystemUiOverlayStyle( 15 | statusBarColor: Colors.transparent, 16 | statusBarIconBrightness: Brightness.dark, 17 | statusBarBrightness: Brightness.light, 18 | systemNavigationBarColor: Colors.transparent, 19 | systemNavigationBarDividerColor: Colors.transparent, 20 | systemNavigationBarIconBrightness: Brightness.dark, 21 | ), 22 | ); 23 | 24 | return MaterialApp( 25 | title: "Firebase Auth DDD", 26 | debugShowCheckedModeBanner: kDebugMode, 27 | 28 | theme: AppTheme.lightTheme, 29 | darkTheme: AppTheme.darkTheme, 30 | themeMode: ThemeMode.system, 31 | 32 | home: const LoginPage(), 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/application/authentication/auth_events.dart: -------------------------------------------------------------------------------- 1 | import "package:freezed_annotation/freezed_annotation.dart"; 2 | 3 | part "auth_events.freezed.dart"; 4 | 5 | @freezed 6 | class AuthEvents with _$AuthEvents { 7 | const factory AuthEvents.emailChanged({required String? email}) = EmailChanged; 8 | 9 | const factory AuthEvents.passwordChanged({required String? password}) = PasswordChanged; 10 | 11 | const factory AuthEvents.signUpWithEmailAndPasswordPressed() = SignUPWithEmailAndPasswordPressed; 12 | 13 | const factory AuthEvents.signInWithEmailAndPasswordPressed() = SignInWithEmailAndPasswordPressed; 14 | } 15 | -------------------------------------------------------------------------------- /lib/application/authentication/auth_events.freezed.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | // coverage:ignore-file 3 | // ignore_for_file: type=lint 4 | // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark 5 | 6 | part of 'auth_events.dart'; 7 | 8 | // ************************************************************************** 9 | // FreezedGenerator 10 | // ************************************************************************** 11 | 12 | // dart format off 13 | T _$identity(T value) => value; 14 | 15 | /// @nodoc 16 | mixin _$AuthEvents { 17 | @override 18 | bool operator ==(Object other) { 19 | return identical(this, other) || (other.runtimeType == runtimeType && other is AuthEvents); 20 | } 21 | 22 | @override 23 | int get hashCode => runtimeType.hashCode; 24 | 25 | @override 26 | String toString() { 27 | return 'AuthEvents()'; 28 | } 29 | } 30 | 31 | /// @nodoc 32 | class $AuthEventsCopyWith<$Res> { 33 | $AuthEventsCopyWith(AuthEvents _, $Res Function(AuthEvents) __); 34 | } 35 | 36 | /// Adds pattern-matching-related methods to [AuthEvents]. 37 | extension AuthEventsPatterns on AuthEvents { 38 | /// A variant of `map` that fallback to returning `orElse`. 39 | /// 40 | /// It is equivalent to doing: 41 | /// ```dart 42 | /// switch (sealedClass) { 43 | /// case final Subclass value: 44 | /// return ...; 45 | /// case _: 46 | /// return orElse(); 47 | /// } 48 | /// ``` 49 | 50 | @optionalTypeArgs 51 | TResult maybeMap({ 52 | TResult Function(EmailChanged value)? emailChanged, 53 | TResult Function(PasswordChanged value)? passwordChanged, 54 | TResult Function(SignUPWithEmailAndPasswordPressed value)? signUpWithEmailAndPasswordPressed, 55 | TResult Function(SignInWithEmailAndPasswordPressed value)? signInWithEmailAndPasswordPressed, 56 | required TResult orElse(), 57 | }) { 58 | final _that = this; 59 | switch (_that) { 60 | case EmailChanged() when emailChanged != null: 61 | return emailChanged(_that); 62 | case PasswordChanged() when passwordChanged != null: 63 | return passwordChanged(_that); 64 | case SignUPWithEmailAndPasswordPressed() when signUpWithEmailAndPasswordPressed != null: 65 | return signUpWithEmailAndPasswordPressed(_that); 66 | case SignInWithEmailAndPasswordPressed() when signInWithEmailAndPasswordPressed != null: 67 | return signInWithEmailAndPasswordPressed(_that); 68 | case _: 69 | return orElse(); 70 | } 71 | } 72 | 73 | /// A `switch`-like method, using callbacks. 74 | /// 75 | /// Callbacks receives the raw object, upcasted. 76 | /// It is equivalent to doing: 77 | /// ```dart 78 | /// switch (sealedClass) { 79 | /// case final Subclass value: 80 | /// return ...; 81 | /// case final Subclass2 value: 82 | /// return ...; 83 | /// } 84 | /// ``` 85 | 86 | @optionalTypeArgs 87 | TResult map({ 88 | required TResult Function(EmailChanged value) emailChanged, 89 | required TResult Function(PasswordChanged value) passwordChanged, 90 | required TResult Function(SignUPWithEmailAndPasswordPressed value) signUpWithEmailAndPasswordPressed, 91 | required TResult Function(SignInWithEmailAndPasswordPressed value) signInWithEmailAndPasswordPressed, 92 | }) { 93 | final _that = this; 94 | switch (_that) { 95 | case EmailChanged(): 96 | return emailChanged(_that); 97 | case PasswordChanged(): 98 | return passwordChanged(_that); 99 | case SignUPWithEmailAndPasswordPressed(): 100 | return signUpWithEmailAndPasswordPressed(_that); 101 | case SignInWithEmailAndPasswordPressed(): 102 | return signInWithEmailAndPasswordPressed(_that); 103 | case _: 104 | throw StateError('Unexpected subclass'); 105 | } 106 | } 107 | 108 | /// A variant of `map` that fallback to returning `null`. 109 | /// 110 | /// It is equivalent to doing: 111 | /// ```dart 112 | /// switch (sealedClass) { 113 | /// case final Subclass value: 114 | /// return ...; 115 | /// case _: 116 | /// return null; 117 | /// } 118 | /// ``` 119 | 120 | @optionalTypeArgs 121 | TResult? mapOrNull({ 122 | TResult? Function(EmailChanged value)? emailChanged, 123 | TResult? Function(PasswordChanged value)? passwordChanged, 124 | TResult? Function(SignUPWithEmailAndPasswordPressed value)? signUpWithEmailAndPasswordPressed, 125 | TResult? Function(SignInWithEmailAndPasswordPressed value)? signInWithEmailAndPasswordPressed, 126 | }) { 127 | final _that = this; 128 | switch (_that) { 129 | case EmailChanged() when emailChanged != null: 130 | return emailChanged(_that); 131 | case PasswordChanged() when passwordChanged != null: 132 | return passwordChanged(_that); 133 | case SignUPWithEmailAndPasswordPressed() when signUpWithEmailAndPasswordPressed != null: 134 | return signUpWithEmailAndPasswordPressed(_that); 135 | case SignInWithEmailAndPasswordPressed() when signInWithEmailAndPasswordPressed != null: 136 | return signInWithEmailAndPasswordPressed(_that); 137 | case _: 138 | return null; 139 | } 140 | } 141 | 142 | /// A variant of `when` that fallback to an `orElse` callback. 143 | /// 144 | /// It is equivalent to doing: 145 | /// ```dart 146 | /// switch (sealedClass) { 147 | /// case Subclass(:final field): 148 | /// return ...; 149 | /// case _: 150 | /// return orElse(); 151 | /// } 152 | /// ``` 153 | 154 | @optionalTypeArgs 155 | TResult maybeWhen({ 156 | TResult Function(String? email)? emailChanged, 157 | TResult Function(String? password)? passwordChanged, 158 | TResult Function()? signUpWithEmailAndPasswordPressed, 159 | TResult Function()? signInWithEmailAndPasswordPressed, 160 | required TResult orElse(), 161 | }) { 162 | final _that = this; 163 | switch (_that) { 164 | case EmailChanged() when emailChanged != null: 165 | return emailChanged(_that.email); 166 | case PasswordChanged() when passwordChanged != null: 167 | return passwordChanged(_that.password); 168 | case SignUPWithEmailAndPasswordPressed() when signUpWithEmailAndPasswordPressed != null: 169 | return signUpWithEmailAndPasswordPressed(); 170 | case SignInWithEmailAndPasswordPressed() when signInWithEmailAndPasswordPressed != null: 171 | return signInWithEmailAndPasswordPressed(); 172 | case _: 173 | return orElse(); 174 | } 175 | } 176 | 177 | /// A `switch`-like method, using callbacks. 178 | /// 179 | /// As opposed to `map`, this offers destructuring. 180 | /// It is equivalent to doing: 181 | /// ```dart 182 | /// switch (sealedClass) { 183 | /// case Subclass(:final field): 184 | /// return ...; 185 | /// case Subclass2(:final field2): 186 | /// return ...; 187 | /// } 188 | /// ``` 189 | 190 | @optionalTypeArgs 191 | TResult when({ 192 | required TResult Function(String? email) emailChanged, 193 | required TResult Function(String? password) passwordChanged, 194 | required TResult Function() signUpWithEmailAndPasswordPressed, 195 | required TResult Function() signInWithEmailAndPasswordPressed, 196 | }) { 197 | final _that = this; 198 | switch (_that) { 199 | case EmailChanged(): 200 | return emailChanged(_that.email); 201 | case PasswordChanged(): 202 | return passwordChanged(_that.password); 203 | case SignUPWithEmailAndPasswordPressed(): 204 | return signUpWithEmailAndPasswordPressed(); 205 | case SignInWithEmailAndPasswordPressed(): 206 | return signInWithEmailAndPasswordPressed(); 207 | case _: 208 | throw StateError('Unexpected subclass'); 209 | } 210 | } 211 | 212 | /// A variant of `when` that fallback to returning `null` 213 | /// 214 | /// It is equivalent to doing: 215 | /// ```dart 216 | /// switch (sealedClass) { 217 | /// case Subclass(:final field): 218 | /// return ...; 219 | /// case _: 220 | /// return null; 221 | /// } 222 | /// ``` 223 | 224 | @optionalTypeArgs 225 | TResult? whenOrNull({ 226 | TResult? Function(String? email)? emailChanged, 227 | TResult? Function(String? password)? passwordChanged, 228 | TResult? Function()? signUpWithEmailAndPasswordPressed, 229 | TResult? Function()? signInWithEmailAndPasswordPressed, 230 | }) { 231 | final _that = this; 232 | switch (_that) { 233 | case EmailChanged() when emailChanged != null: 234 | return emailChanged(_that.email); 235 | case PasswordChanged() when passwordChanged != null: 236 | return passwordChanged(_that.password); 237 | case SignUPWithEmailAndPasswordPressed() when signUpWithEmailAndPasswordPressed != null: 238 | return signUpWithEmailAndPasswordPressed(); 239 | case SignInWithEmailAndPasswordPressed() when signInWithEmailAndPasswordPressed != null: 240 | return signInWithEmailAndPasswordPressed(); 241 | case _: 242 | return null; 243 | } 244 | } 245 | } 246 | 247 | /// @nodoc 248 | 249 | class EmailChanged implements AuthEvents { 250 | const EmailChanged({required this.email}); 251 | 252 | final String? email; 253 | 254 | /// Create a copy of AuthEvents 255 | /// with the given fields replaced by the non-null parameter values. 256 | @JsonKey(includeFromJson: false, includeToJson: false) 257 | @pragma('vm:prefer-inline') 258 | $EmailChangedCopyWith get copyWith => _$EmailChangedCopyWithImpl(this, _$identity); 259 | 260 | @override 261 | bool operator ==(Object other) { 262 | return identical(this, other) || 263 | (other.runtimeType == runtimeType && 264 | other is EmailChanged && 265 | (identical(other.email, email) || other.email == email)); 266 | } 267 | 268 | @override 269 | int get hashCode => Object.hash(runtimeType, email); 270 | 271 | @override 272 | String toString() { 273 | return 'AuthEvents.emailChanged(email: $email)'; 274 | } 275 | } 276 | 277 | /// @nodoc 278 | abstract mixin class $EmailChangedCopyWith<$Res> implements $AuthEventsCopyWith<$Res> { 279 | factory $EmailChangedCopyWith(EmailChanged value, $Res Function(EmailChanged) _then) = _$EmailChangedCopyWithImpl; 280 | @useResult 281 | $Res call({String? email}); 282 | } 283 | 284 | /// @nodoc 285 | class _$EmailChangedCopyWithImpl<$Res> implements $EmailChangedCopyWith<$Res> { 286 | _$EmailChangedCopyWithImpl(this._self, this._then); 287 | 288 | final EmailChanged _self; 289 | final $Res Function(EmailChanged) _then; 290 | 291 | /// Create a copy of AuthEvents 292 | /// with the given fields replaced by the non-null parameter values. 293 | @pragma('vm:prefer-inline') 294 | $Res call({ 295 | Object? email = freezed, 296 | }) { 297 | return _then(EmailChanged( 298 | email: freezed == email 299 | ? _self.email 300 | : email // ignore: cast_nullable_to_non_nullable 301 | as String?, 302 | )); 303 | } 304 | } 305 | 306 | /// @nodoc 307 | 308 | class PasswordChanged implements AuthEvents { 309 | const PasswordChanged({required this.password}); 310 | 311 | final String? password; 312 | 313 | /// Create a copy of AuthEvents 314 | /// with the given fields replaced by the non-null parameter values. 315 | @JsonKey(includeFromJson: false, includeToJson: false) 316 | @pragma('vm:prefer-inline') 317 | $PasswordChangedCopyWith get copyWith => 318 | _$PasswordChangedCopyWithImpl(this, _$identity); 319 | 320 | @override 321 | bool operator ==(Object other) { 322 | return identical(this, other) || 323 | (other.runtimeType == runtimeType && 324 | other is PasswordChanged && 325 | (identical(other.password, password) || other.password == password)); 326 | } 327 | 328 | @override 329 | int get hashCode => Object.hash(runtimeType, password); 330 | 331 | @override 332 | String toString() { 333 | return 'AuthEvents.passwordChanged(password: $password)'; 334 | } 335 | } 336 | 337 | /// @nodoc 338 | abstract mixin class $PasswordChangedCopyWith<$Res> implements $AuthEventsCopyWith<$Res> { 339 | factory $PasswordChangedCopyWith(PasswordChanged value, $Res Function(PasswordChanged) _then) = 340 | _$PasswordChangedCopyWithImpl; 341 | @useResult 342 | $Res call({String? password}); 343 | } 344 | 345 | /// @nodoc 346 | class _$PasswordChangedCopyWithImpl<$Res> implements $PasswordChangedCopyWith<$Res> { 347 | _$PasswordChangedCopyWithImpl(this._self, this._then); 348 | 349 | final PasswordChanged _self; 350 | final $Res Function(PasswordChanged) _then; 351 | 352 | /// Create a copy of AuthEvents 353 | /// with the given fields replaced by the non-null parameter values. 354 | @pragma('vm:prefer-inline') 355 | $Res call({ 356 | Object? password = freezed, 357 | }) { 358 | return _then(PasswordChanged( 359 | password: freezed == password 360 | ? _self.password 361 | : password // ignore: cast_nullable_to_non_nullable 362 | as String?, 363 | )); 364 | } 365 | } 366 | 367 | /// @nodoc 368 | 369 | class SignUPWithEmailAndPasswordPressed implements AuthEvents { 370 | const SignUPWithEmailAndPasswordPressed(); 371 | 372 | @override 373 | bool operator ==(Object other) { 374 | return identical(this, other) || (other.runtimeType == runtimeType && other is SignUPWithEmailAndPasswordPressed); 375 | } 376 | 377 | @override 378 | int get hashCode => runtimeType.hashCode; 379 | 380 | @override 381 | String toString() { 382 | return 'AuthEvents.signUpWithEmailAndPasswordPressed()'; 383 | } 384 | } 385 | 386 | /// @nodoc 387 | 388 | class SignInWithEmailAndPasswordPressed implements AuthEvents { 389 | const SignInWithEmailAndPasswordPressed(); 390 | 391 | @override 392 | bool operator ==(Object other) { 393 | return identical(this, other) || (other.runtimeType == runtimeType && other is SignInWithEmailAndPasswordPressed); 394 | } 395 | 396 | @override 397 | int get hashCode => runtimeType.hashCode; 398 | 399 | @override 400 | String toString() { 401 | return 'AuthEvents.signInWithEmailAndPasswordPressed()'; 402 | } 403 | } 404 | 405 | // dart format on 406 | -------------------------------------------------------------------------------- /lib/application/authentication/auth_state_controller.dart: -------------------------------------------------------------------------------- 1 | import "package:firebase_auth_flutter_ddd/domain/authentication/auth_failures.dart"; 2 | import "package:flutter_riverpod/flutter_riverpod.dart"; 3 | import "package:fpdart/fpdart.dart"; 4 | 5 | import "../../domain/authentication/auth_value_objects.dart"; 6 | import "../../domain/authentication/i_auth_facade.dart"; 7 | import "../../services/authentication/firebase_auth_facade.dart"; 8 | import "auth_events.dart"; 9 | import "auth_states.dart"; 10 | 11 | final authStateControllerProvider = NotifierProvider( 12 | AuthStateController.new, 13 | ); 14 | 15 | class AuthStateController extends Notifier { 16 | @override 17 | AuthStates build() { 18 | return AuthStates.initial(); 19 | } 20 | 21 | IAuthFacade get _authFacade => ref.read(firebaseAuthFacadeProvider); 22 | 23 | Future mapEventsToStates(AuthEvents events) async { 24 | await events.map( 25 | emailChanged: (value) async { 26 | _updateEmail(value.email!); 27 | }, 28 | passwordChanged: (value) async { 29 | _updatePassword(value.password!); 30 | }, 31 | signUpWithEmailAndPasswordPressed: (value) async { 32 | await signUpWithEmailAndPassword(); 33 | }, 34 | signInWithEmailAndPasswordPressed: (value) async { 35 | await signInWithEmailAndPassword(); 36 | }, 37 | ); 38 | } 39 | 40 | void emailChanged(String email) { 41 | _updateEmail(email); 42 | } 43 | 44 | void passwordChanged(String password) { 45 | _updatePassword(password); 46 | } 47 | 48 | void _updateEmail(String email) { 49 | state = state.copyWith( 50 | emailAddress: EmailAddress(email: email), 51 | authFailureOrSuccess: none(), 52 | showError: false, 53 | ); 54 | } 55 | 56 | void _updatePassword(String password) { 57 | state = state.copyWith( 58 | password: Password(password: password), 59 | authFailureOrSuccess: none(), 60 | showError: false, 61 | ); 62 | } 63 | 64 | Future signUpWithEmailAndPassword() async { 65 | await _performAuthAction(_authFacade.registerWithEmailAndPassword); 66 | } 67 | 68 | Future signInWithEmailAndPassword() async { 69 | await _performAuthAction(_authFacade.signInWithEmailAndPassword); 70 | } 71 | 72 | Future _performAuthAction( 73 | Future> Function({ 74 | required EmailAddress emailAddress, 75 | required Password password, 76 | }) forwardCall, 77 | ) async { 78 | final isEmailValid = state.emailAddress.isValid(); 79 | final isPasswordValid = state.password.isValid(); 80 | 81 | if (!isEmailValid || !isPasswordValid) { 82 | state = state.copyWith( 83 | showError: true, 84 | authFailureOrSuccess: none(), 85 | ); 86 | return; 87 | } 88 | 89 | state = state.copyWith( 90 | isSubmitting: true, 91 | authFailureOrSuccess: none(), 92 | showError: false, 93 | ); 94 | 95 | final failureOrSuccess = await forwardCall( 96 | emailAddress: state.emailAddress, 97 | password: state.password, 98 | ); 99 | 100 | state = state.copyWith( 101 | isSubmitting: false, 102 | showError: failureOrSuccess.isLeft(), 103 | authFailureOrSuccess: optionOf(failureOrSuccess), 104 | ); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /lib/application/authentication/auth_states.dart: -------------------------------------------------------------------------------- 1 | import "package:fpdart/fpdart.dart"; 2 | import "package:freezed_annotation/freezed_annotation.dart"; 3 | 4 | import "../../domain/authentication/auth_failures.dart"; 5 | import "../../domain/authentication/auth_value_objects.dart"; 6 | 7 | part "auth_states.freezed.dart"; 8 | 9 | @freezed 10 | sealed class AuthStates with _$AuthStates { 11 | const factory AuthStates({ 12 | required EmailAddress emailAddress, 13 | required Password password, 14 | required bool isSubmitting, 15 | required bool showError, 16 | required Option> authFailureOrSuccess, 17 | }) = _AuthStates; 18 | 19 | factory AuthStates.initial() => AuthStates( 20 | emailAddress: EmailAddress(email: ""), 21 | password: Password(password: ""), 22 | showError: false, 23 | isSubmitting: false, 24 | authFailureOrSuccess: none()); 25 | } 26 | -------------------------------------------------------------------------------- /lib/application/authentication/auth_states.freezed.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | // coverage:ignore-file 3 | // ignore_for_file: type=lint 4 | // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark 5 | 6 | part of 'auth_states.dart'; 7 | 8 | // ************************************************************************** 9 | // FreezedGenerator 10 | // ************************************************************************** 11 | 12 | // dart format off 13 | T _$identity(T value) => value; 14 | 15 | /// @nodoc 16 | mixin _$AuthStates { 17 | EmailAddress get emailAddress; 18 | Password get password; 19 | bool get isSubmitting; 20 | bool get showError; 21 | Option> get authFailureOrSuccess; 22 | 23 | /// Create a copy of AuthStates 24 | /// with the given fields replaced by the non-null parameter values. 25 | @JsonKey(includeFromJson: false, includeToJson: false) 26 | @pragma('vm:prefer-inline') 27 | $AuthStatesCopyWith get copyWith => 28 | _$AuthStatesCopyWithImpl(this as AuthStates, _$identity); 29 | 30 | @override 31 | bool operator ==(Object other) { 32 | return identical(this, other) || 33 | (other.runtimeType == runtimeType && 34 | other is AuthStates && 35 | (identical(other.emailAddress, emailAddress) || 36 | other.emailAddress == emailAddress) && 37 | (identical(other.password, password) || 38 | other.password == password) && 39 | (identical(other.isSubmitting, isSubmitting) || 40 | other.isSubmitting == isSubmitting) && 41 | (identical(other.showError, showError) || 42 | other.showError == showError) && 43 | (identical(other.authFailureOrSuccess, authFailureOrSuccess) || 44 | other.authFailureOrSuccess == authFailureOrSuccess)); 45 | } 46 | 47 | @override 48 | int get hashCode => Object.hash(runtimeType, emailAddress, password, 49 | isSubmitting, showError, authFailureOrSuccess); 50 | 51 | @override 52 | String toString() { 53 | return 'AuthStates(emailAddress: $emailAddress, password: $password, isSubmitting: $isSubmitting, showError: $showError, authFailureOrSuccess: $authFailureOrSuccess)'; 54 | } 55 | } 56 | 57 | /// @nodoc 58 | abstract mixin class $AuthStatesCopyWith<$Res> { 59 | factory $AuthStatesCopyWith( 60 | AuthStates value, $Res Function(AuthStates) _then) = 61 | _$AuthStatesCopyWithImpl; 62 | @useResult 63 | $Res call( 64 | {EmailAddress emailAddress, 65 | Password password, 66 | bool isSubmitting, 67 | bool showError, 68 | Option> authFailureOrSuccess}); 69 | } 70 | 71 | /// @nodoc 72 | class _$AuthStatesCopyWithImpl<$Res> implements $AuthStatesCopyWith<$Res> { 73 | _$AuthStatesCopyWithImpl(this._self, this._then); 74 | 75 | final AuthStates _self; 76 | final $Res Function(AuthStates) _then; 77 | 78 | /// Create a copy of AuthStates 79 | /// with the given fields replaced by the non-null parameter values. 80 | @pragma('vm:prefer-inline') 81 | @override 82 | $Res call({ 83 | Object? emailAddress = null, 84 | Object? password = null, 85 | Object? isSubmitting = null, 86 | Object? showError = null, 87 | Object? authFailureOrSuccess = null, 88 | }) { 89 | return _then(_self.copyWith( 90 | emailAddress: null == emailAddress 91 | ? _self.emailAddress 92 | : emailAddress // ignore: cast_nullable_to_non_nullable 93 | as EmailAddress, 94 | password: null == password 95 | ? _self.password 96 | : password // ignore: cast_nullable_to_non_nullable 97 | as Password, 98 | isSubmitting: null == isSubmitting 99 | ? _self.isSubmitting 100 | : isSubmitting // ignore: cast_nullable_to_non_nullable 101 | as bool, 102 | showError: null == showError 103 | ? _self.showError 104 | : showError // ignore: cast_nullable_to_non_nullable 105 | as bool, 106 | authFailureOrSuccess: null == authFailureOrSuccess 107 | ? _self.authFailureOrSuccess 108 | : authFailureOrSuccess // ignore: cast_nullable_to_non_nullable 109 | as Option>, 110 | )); 111 | } 112 | } 113 | 114 | /// Adds pattern-matching-related methods to [AuthStates]. 115 | extension AuthStatesPatterns on AuthStates { 116 | /// A variant of `map` that fallback to returning `orElse`. 117 | /// 118 | /// It is equivalent to doing: 119 | /// ```dart 120 | /// switch (sealedClass) { 121 | /// case final Subclass value: 122 | /// return ...; 123 | /// case _: 124 | /// return orElse(); 125 | /// } 126 | /// ``` 127 | 128 | @optionalTypeArgs 129 | TResult maybeMap( 130 | TResult Function(_AuthStates value)? $default, { 131 | required TResult orElse(), 132 | }) { 133 | final _that = this; 134 | switch (_that) { 135 | case _AuthStates() when $default != null: 136 | return $default(_that); 137 | case _: 138 | return orElse(); 139 | } 140 | } 141 | 142 | /// A `switch`-like method, using callbacks. 143 | /// 144 | /// Callbacks receives the raw object, upcasted. 145 | /// It is equivalent to doing: 146 | /// ```dart 147 | /// switch (sealedClass) { 148 | /// case final Subclass value: 149 | /// return ...; 150 | /// case final Subclass2 value: 151 | /// return ...; 152 | /// } 153 | /// ``` 154 | 155 | @optionalTypeArgs 156 | TResult map( 157 | TResult Function(_AuthStates value) $default, 158 | ) { 159 | final _that = this; 160 | switch (_that) { 161 | case _AuthStates(): 162 | return $default(_that); 163 | } 164 | } 165 | 166 | /// A variant of `map` that fallback to returning `null`. 167 | /// 168 | /// It is equivalent to doing: 169 | /// ```dart 170 | /// switch (sealedClass) { 171 | /// case final Subclass value: 172 | /// return ...; 173 | /// case _: 174 | /// return null; 175 | /// } 176 | /// ``` 177 | 178 | @optionalTypeArgs 179 | TResult? mapOrNull( 180 | TResult? Function(_AuthStates value)? $default, 181 | ) { 182 | final _that = this; 183 | switch (_that) { 184 | case _AuthStates() when $default != null: 185 | return $default(_that); 186 | case _: 187 | return null; 188 | } 189 | } 190 | 191 | /// A variant of `when` that fallback to an `orElse` callback. 192 | /// 193 | /// It is equivalent to doing: 194 | /// ```dart 195 | /// switch (sealedClass) { 196 | /// case Subclass(:final field): 197 | /// return ...; 198 | /// case _: 199 | /// return orElse(); 200 | /// } 201 | /// ``` 202 | 203 | @optionalTypeArgs 204 | TResult maybeWhen( 205 | TResult Function( 206 | EmailAddress emailAddress, 207 | Password password, 208 | bool isSubmitting, 209 | bool showError, 210 | Option> authFailureOrSuccess)? 211 | $default, { 212 | required TResult orElse(), 213 | }) { 214 | final _that = this; 215 | switch (_that) { 216 | case _AuthStates() when $default != null: 217 | return $default(_that.emailAddress, _that.password, _that.isSubmitting, 218 | _that.showError, _that.authFailureOrSuccess); 219 | case _: 220 | return orElse(); 221 | } 222 | } 223 | 224 | /// A `switch`-like method, using callbacks. 225 | /// 226 | /// As opposed to `map`, this offers destructuring. 227 | /// It is equivalent to doing: 228 | /// ```dart 229 | /// switch (sealedClass) { 230 | /// case Subclass(:final field): 231 | /// return ...; 232 | /// case Subclass2(:final field2): 233 | /// return ...; 234 | /// } 235 | /// ``` 236 | 237 | @optionalTypeArgs 238 | TResult when( 239 | TResult Function( 240 | EmailAddress emailAddress, 241 | Password password, 242 | bool isSubmitting, 243 | bool showError, 244 | Option> authFailureOrSuccess) 245 | $default, 246 | ) { 247 | final _that = this; 248 | switch (_that) { 249 | case _AuthStates(): 250 | return $default(_that.emailAddress, _that.password, _that.isSubmitting, 251 | _that.showError, _that.authFailureOrSuccess); 252 | } 253 | } 254 | 255 | /// A variant of `when` that fallback to returning `null` 256 | /// 257 | /// It is equivalent to doing: 258 | /// ```dart 259 | /// switch (sealedClass) { 260 | /// case Subclass(:final field): 261 | /// return ...; 262 | /// case _: 263 | /// return null; 264 | /// } 265 | /// ``` 266 | 267 | @optionalTypeArgs 268 | TResult? whenOrNull( 269 | TResult? Function( 270 | EmailAddress emailAddress, 271 | Password password, 272 | bool isSubmitting, 273 | bool showError, 274 | Option> authFailureOrSuccess)? 275 | $default, 276 | ) { 277 | final _that = this; 278 | switch (_that) { 279 | case _AuthStates() when $default != null: 280 | return $default(_that.emailAddress, _that.password, _that.isSubmitting, 281 | _that.showError, _that.authFailureOrSuccess); 282 | case _: 283 | return null; 284 | } 285 | } 286 | } 287 | 288 | /// @nodoc 289 | 290 | class _AuthStates implements AuthStates { 291 | const _AuthStates( 292 | {required this.emailAddress, 293 | required this.password, 294 | required this.isSubmitting, 295 | required this.showError, 296 | required this.authFailureOrSuccess}); 297 | 298 | @override 299 | final EmailAddress emailAddress; 300 | @override 301 | final Password password; 302 | @override 303 | final bool isSubmitting; 304 | @override 305 | final bool showError; 306 | @override 307 | final Option> authFailureOrSuccess; 308 | 309 | /// Create a copy of AuthStates 310 | /// with the given fields replaced by the non-null parameter values. 311 | @override 312 | @JsonKey(includeFromJson: false, includeToJson: false) 313 | @pragma('vm:prefer-inline') 314 | _$AuthStatesCopyWith<_AuthStates> get copyWith => 315 | __$AuthStatesCopyWithImpl<_AuthStates>(this, _$identity); 316 | 317 | @override 318 | bool operator ==(Object other) { 319 | return identical(this, other) || 320 | (other.runtimeType == runtimeType && 321 | other is _AuthStates && 322 | (identical(other.emailAddress, emailAddress) || 323 | other.emailAddress == emailAddress) && 324 | (identical(other.password, password) || 325 | other.password == password) && 326 | (identical(other.isSubmitting, isSubmitting) || 327 | other.isSubmitting == isSubmitting) && 328 | (identical(other.showError, showError) || 329 | other.showError == showError) && 330 | (identical(other.authFailureOrSuccess, authFailureOrSuccess) || 331 | other.authFailureOrSuccess == authFailureOrSuccess)); 332 | } 333 | 334 | @override 335 | int get hashCode => Object.hash(runtimeType, emailAddress, password, 336 | isSubmitting, showError, authFailureOrSuccess); 337 | 338 | @override 339 | String toString() { 340 | return 'AuthStates(emailAddress: $emailAddress, password: $password, isSubmitting: $isSubmitting, showError: $showError, authFailureOrSuccess: $authFailureOrSuccess)'; 341 | } 342 | } 343 | 344 | /// @nodoc 345 | abstract mixin class _$AuthStatesCopyWith<$Res> 346 | implements $AuthStatesCopyWith<$Res> { 347 | factory _$AuthStatesCopyWith( 348 | _AuthStates value, $Res Function(_AuthStates) _then) = 349 | __$AuthStatesCopyWithImpl; 350 | @override 351 | @useResult 352 | $Res call( 353 | {EmailAddress emailAddress, 354 | Password password, 355 | bool isSubmitting, 356 | bool showError, 357 | Option> authFailureOrSuccess}); 358 | } 359 | 360 | /// @nodoc 361 | class __$AuthStatesCopyWithImpl<$Res> implements _$AuthStatesCopyWith<$Res> { 362 | __$AuthStatesCopyWithImpl(this._self, this._then); 363 | 364 | final _AuthStates _self; 365 | final $Res Function(_AuthStates) _then; 366 | 367 | /// Create a copy of AuthStates 368 | /// with the given fields replaced by the non-null parameter values. 369 | @override 370 | @pragma('vm:prefer-inline') 371 | $Res call({ 372 | Object? emailAddress = null, 373 | Object? password = null, 374 | Object? isSubmitting = null, 375 | Object? showError = null, 376 | Object? authFailureOrSuccess = null, 377 | }) { 378 | return _then(_AuthStates( 379 | emailAddress: null == emailAddress 380 | ? _self.emailAddress 381 | : emailAddress // ignore: cast_nullable_to_non_nullable 382 | as EmailAddress, 383 | password: null == password 384 | ? _self.password 385 | : password // ignore: cast_nullable_to_non_nullable 386 | as Password, 387 | isSubmitting: null == isSubmitting 388 | ? _self.isSubmitting 389 | : isSubmitting // ignore: cast_nullable_to_non_nullable 390 | as bool, 391 | showError: null == showError 392 | ? _self.showError 393 | : showError // ignore: cast_nullable_to_non_nullable 394 | as bool, 395 | authFailureOrSuccess: null == authFailureOrSuccess 396 | ? _self.authFailureOrSuccess 397 | : authFailureOrSuccess // ignore: cast_nullable_to_non_nullable 398 | as Option>, 399 | )); 400 | } 401 | } 402 | 403 | // dart format on 404 | -------------------------------------------------------------------------------- /lib/core/theme/animated_widgets.dart: -------------------------------------------------------------------------------- 1 | import "package:flutter/material.dart"; 2 | 3 | class AnimatedFormField extends StatefulWidget { 4 | final String label; 5 | final String? hint; 6 | final bool obscureText; 7 | final TextInputType keyboardType; 8 | final String? Function(String?)? validator; 9 | final void Function(String)? onChanged; 10 | final IconData? prefixIcon; 11 | final Widget? suffixIcon; 12 | final TextEditingController? controller; 13 | 14 | const AnimatedFormField({ 15 | super.key, 16 | required this.label, 17 | this.hint, 18 | this.obscureText = false, 19 | this.keyboardType = TextInputType.text, 20 | this.validator, 21 | this.onChanged, 22 | this.prefixIcon, 23 | this.suffixIcon, 24 | this.controller, 25 | }); 26 | 27 | @override 28 | State createState() => _AnimatedFormFieldState(); 29 | } 30 | 31 | class _AnimatedFormFieldState extends State with SingleTickerProviderStateMixin { 32 | late AnimationController _animationController; 33 | late Animation _scaleAnimation; 34 | late Animation _opacityAnimation; 35 | bool _isFocused = false; 36 | 37 | @override 38 | void initState() { 39 | super.initState(); 40 | _animationController = AnimationController( 41 | duration: const Duration(milliseconds: 300), 42 | vsync: this, 43 | ); 44 | _scaleAnimation = Tween(begin: 0.95, end: 1.0).animate( 45 | CurvedAnimation(parent: _animationController, curve: Curves.easeOutCubic), 46 | ); 47 | _opacityAnimation = Tween(begin: 0.0, end: 1.0).animate( 48 | CurvedAnimation(parent: _animationController, curve: Curves.easeOut), 49 | ); 50 | _animationController.forward(); 51 | } 52 | 53 | @override 54 | void dispose() { 55 | _animationController.dispose(); 56 | super.dispose(); 57 | } 58 | 59 | @override 60 | Widget build(BuildContext context) { 61 | return AnimatedBuilder( 62 | animation: _animationController, 63 | builder: (context, child) { 64 | return Transform.scale( 65 | scale: _scaleAnimation.value, 66 | child: Opacity( 67 | opacity: _opacityAnimation.value, 68 | child: Focus( 69 | onFocusChange: (focused) { 70 | setState(() { 71 | _isFocused = focused; 72 | }); 73 | }, 74 | child: AnimatedContainer( 75 | duration: const Duration(milliseconds: 200), 76 | transform: Matrix4.identity()..scale(_isFocused ? 1.02 : 1.0), 77 | child: TextFormField( 78 | controller: widget.controller, 79 | obscureText: widget.obscureText, 80 | keyboardType: widget.keyboardType, 81 | validator: widget.validator, 82 | onChanged: widget.onChanged, 83 | decoration: InputDecoration( 84 | labelText: widget.label, 85 | hintText: widget.hint, 86 | prefixIcon: widget.prefixIcon != null ? Icon(widget.prefixIcon) : null, 87 | suffixIcon: widget.suffixIcon, 88 | ), 89 | ), 90 | ), 91 | ), 92 | ), 93 | ); 94 | }, 95 | ); 96 | } 97 | } 98 | 99 | class AnimatedButton extends StatefulWidget { 100 | final String text; 101 | final VoidCallback? onPressed; 102 | final bool isLoading; 103 | final IconData? icon; 104 | final ButtonStyle? style; 105 | 106 | const AnimatedButton({ 107 | super.key, 108 | required this.text, 109 | this.onPressed, 110 | this.isLoading = false, 111 | this.icon, 112 | this.style, 113 | }); 114 | 115 | @override 116 | State createState() => _AnimatedButtonState(); 117 | } 118 | 119 | class _AnimatedButtonState extends State with SingleTickerProviderStateMixin { 120 | late AnimationController _animationController; 121 | late Animation _scaleAnimation; 122 | 123 | @override 124 | void initState() { 125 | super.initState(); 126 | _animationController = AnimationController( 127 | duration: const Duration(milliseconds: 150), 128 | vsync: this, 129 | ); 130 | _scaleAnimation = Tween(begin: 1.0, end: 0.95).animate( 131 | CurvedAnimation(parent: _animationController, curve: Curves.easeInOut), 132 | ); 133 | } 134 | 135 | @override 136 | void dispose() { 137 | _animationController.dispose(); 138 | super.dispose(); 139 | } 140 | 141 | @override 142 | Widget build(BuildContext context) { 143 | return AnimatedBuilder( 144 | animation: _animationController, 145 | builder: (context, child) { 146 | return Transform.scale( 147 | scale: _scaleAnimation.value, 148 | child: GestureDetector( 149 | onTapDown: (_) { 150 | _animationController.forward(); 151 | }, 152 | onTapUp: (_) { 153 | _animationController.reverse(); 154 | }, 155 | onTapCancel: () { 156 | _animationController.reverse(); 157 | }, 158 | child: ElevatedButton( 159 | onPressed: widget.isLoading ? null : widget.onPressed, 160 | style: widget.style, 161 | child: AnimatedSwitcher( 162 | duration: const Duration(milliseconds: 300), 163 | child: widget.isLoading 164 | ? const SizedBox( 165 | height: 20, 166 | width: 20, 167 | child: CircularProgressIndicator(strokeWidth: 2), 168 | ) 169 | : Row( 170 | mainAxisSize: MainAxisSize.min, 171 | children: [ 172 | if (widget.icon != null) ...[ 173 | Icon(widget.icon), 174 | const SizedBox(width: 8), 175 | ], 176 | Text(widget.text), 177 | ], 178 | ), 179 | ), 180 | ), 181 | ), 182 | ); 183 | }, 184 | ); 185 | } 186 | } 187 | 188 | class SlideInWidget extends StatefulWidget { 189 | final Widget child; 190 | final int delay; 191 | final Offset begin; 192 | final Duration duration; 193 | 194 | const SlideInWidget({ 195 | super.key, 196 | required this.child, 197 | this.delay = 0, 198 | this.begin = const Offset(0, 0.3), 199 | this.duration = const Duration(milliseconds: 600), 200 | }); 201 | 202 | @override 203 | State createState() => _SlideInWidgetState(); 204 | } 205 | 206 | class _SlideInWidgetState extends State with SingleTickerProviderStateMixin { 207 | late AnimationController _animationController; 208 | late Animation _slideAnimation; 209 | late Animation _fadeAnimation; 210 | 211 | @override 212 | void initState() { 213 | super.initState(); 214 | _animationController = AnimationController( 215 | duration: widget.duration, 216 | vsync: this, 217 | ); 218 | 219 | _slideAnimation = Tween( 220 | begin: widget.begin, 221 | end: Offset.zero, 222 | ).animate(CurvedAnimation( 223 | parent: _animationController, 224 | curve: Curves.easeOutCubic, 225 | )); 226 | 227 | _fadeAnimation = Tween( 228 | begin: 0.0, 229 | end: 1.0, 230 | ).animate(CurvedAnimation( 231 | parent: _animationController, 232 | curve: Curves.easeOut, 233 | )); 234 | 235 | Future.delayed(Duration(milliseconds: widget.delay), () { 236 | if (mounted) { 237 | _animationController.forward(); 238 | } 239 | }); 240 | } 241 | 242 | @override 243 | void dispose() { 244 | _animationController.dispose(); 245 | super.dispose(); 246 | } 247 | 248 | @override 249 | Widget build(BuildContext context) { 250 | return AnimatedBuilder( 251 | animation: _animationController, 252 | builder: (context, child) { 253 | return SlideTransition( 254 | position: _slideAnimation, 255 | child: FadeTransition( 256 | opacity: _fadeAnimation, 257 | child: widget.child, 258 | ), 259 | ); 260 | }, 261 | ); 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /lib/core/theme/app_theme.dart: -------------------------------------------------------------------------------- 1 | import "package:flutter/material.dart"; 2 | import "package:flutter/services.dart"; 3 | 4 | class AppTheme { 5 | // Color Seed for Material You 6 | static const Color _primarySeed = Color(0xFF6750A4); 7 | 8 | // Light Theme 9 | static ThemeData get lightTheme { 10 | final ColorScheme lightColorScheme = ColorScheme.fromSeed( 11 | seedColor: _primarySeed, 12 | brightness: Brightness.light, 13 | ); 14 | 15 | return ThemeData( 16 | useMaterial3: true, 17 | colorScheme: lightColorScheme, 18 | 19 | // App Bar Theme 20 | appBarTheme: AppBarTheme( 21 | elevation: 0, 22 | scrolledUnderElevation: 1, 23 | backgroundColor: lightColorScheme.surface, 24 | foregroundColor: lightColorScheme.onSurface, 25 | centerTitle: true, 26 | titleTextStyle: TextStyle( 27 | fontSize: 22, 28 | fontWeight: FontWeight.w500, 29 | color: lightColorScheme.onSurface, 30 | ), 31 | systemOverlayStyle: SystemUiOverlayStyle.dark, 32 | ), 33 | 34 | // Input Decoration Theme 35 | inputDecorationTheme: InputDecorationTheme( 36 | filled: true, 37 | fillColor: lightColorScheme.surfaceContainerHigh.withValues(alpha: 0.3), 38 | border: OutlineInputBorder( 39 | borderRadius: BorderRadius.circular(16), 40 | borderSide: BorderSide.none, 41 | ), 42 | enabledBorder: OutlineInputBorder( 43 | borderRadius: BorderRadius.circular(16), 44 | borderSide: BorderSide( 45 | color: lightColorScheme.outline.withValues(alpha: 0.3), 46 | width: 1, 47 | ), 48 | ), 49 | focusedBorder: OutlineInputBorder( 50 | borderRadius: BorderRadius.circular(16), 51 | borderSide: BorderSide( 52 | color: lightColorScheme.primary, 53 | width: 2, 54 | ), 55 | ), 56 | errorBorder: OutlineInputBorder( 57 | borderRadius: BorderRadius.circular(16), 58 | borderSide: BorderSide( 59 | color: lightColorScheme.error, 60 | width: 1, 61 | ), 62 | ), 63 | focusedErrorBorder: OutlineInputBorder( 64 | borderRadius: BorderRadius.circular(16), 65 | borderSide: BorderSide( 66 | color: lightColorScheme.error, 67 | width: 2, 68 | ), 69 | ), 70 | contentPadding: const EdgeInsets.all(20), 71 | labelStyle: TextStyle( 72 | color: lightColorScheme.onSurfaceVariant, 73 | fontSize: 16, 74 | ), 75 | hintStyle: TextStyle( 76 | color: lightColorScheme.onSurfaceVariant.withValues(alpha: 0.6), 77 | fontSize: 16, 78 | ), 79 | ), 80 | 81 | // Elevated Button Theme 82 | elevatedButtonTheme: ElevatedButtonThemeData( 83 | style: ElevatedButton.styleFrom( 84 | elevation: 0, 85 | backgroundColor: lightColorScheme.primary, 86 | foregroundColor: lightColorScheme.onPrimary, 87 | disabledBackgroundColor: lightColorScheme.onSurface.withValues(alpha: 0.12), 88 | disabledForegroundColor: lightColorScheme.onSurface.withValues(alpha: 0.38), 89 | padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), 90 | minimumSize: const Size(0, 56), 91 | shape: RoundedRectangleBorder( 92 | borderRadius: BorderRadius.circular(16), 93 | ), 94 | textStyle: const TextStyle( 95 | fontSize: 16, 96 | fontWeight: FontWeight.w600, 97 | ), 98 | ), 99 | ), 100 | 101 | // Text Button Theme 102 | textButtonTheme: TextButtonThemeData( 103 | style: TextButton.styleFrom( 104 | foregroundColor: lightColorScheme.primary, 105 | padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), 106 | minimumSize: const Size(0, 48), 107 | shape: RoundedRectangleBorder( 108 | borderRadius: BorderRadius.circular(12), 109 | ), 110 | textStyle: const TextStyle( 111 | fontSize: 16, 112 | fontWeight: FontWeight.w600, 113 | ), 114 | ), 115 | ), 116 | 117 | // Card Theme 118 | cardTheme: CardThemeData( 119 | elevation: 0, 120 | color: lightColorScheme.surfaceContainerHigh.withValues(alpha: 0.3), 121 | shape: RoundedRectangleBorder( 122 | borderRadius: BorderRadius.circular(20), 123 | side: BorderSide( 124 | color: lightColorScheme.outline.withValues(alpha: 0.2), 125 | width: 1, 126 | ), 127 | ), 128 | margin: const EdgeInsets.all(8), 129 | ), 130 | 131 | // Scaffold Background 132 | scaffoldBackgroundColor: lightColorScheme.surface, 133 | ); 134 | } 135 | 136 | // Dark Theme 137 | static ThemeData get darkTheme { 138 | final ColorScheme darkColorScheme = ColorScheme.fromSeed( 139 | seedColor: _primarySeed, 140 | brightness: Brightness.dark, 141 | ); 142 | 143 | return ThemeData( 144 | useMaterial3: true, 145 | colorScheme: darkColorScheme, 146 | 147 | // App Bar Theme 148 | appBarTheme: AppBarTheme( 149 | elevation: 0, 150 | scrolledUnderElevation: 1, 151 | backgroundColor: darkColorScheme.surface, 152 | foregroundColor: darkColorScheme.onSurface, 153 | centerTitle: true, 154 | titleTextStyle: TextStyle( 155 | fontSize: 22, 156 | fontWeight: FontWeight.w500, 157 | color: darkColorScheme.onSurface, 158 | ), 159 | systemOverlayStyle: SystemUiOverlayStyle.light, 160 | ), 161 | 162 | // Input Decoration Theme 163 | inputDecorationTheme: InputDecorationTheme( 164 | filled: true, 165 | fillColor: darkColorScheme.surfaceContainerHigh.withValues(alpha: 0.3), 166 | border: OutlineInputBorder( 167 | borderRadius: BorderRadius.circular(16), 168 | borderSide: BorderSide.none, 169 | ), 170 | enabledBorder: OutlineInputBorder( 171 | borderRadius: BorderRadius.circular(16), 172 | borderSide: BorderSide( 173 | color: darkColorScheme.outline.withValues(alpha: 0.3), 174 | width: 1, 175 | ), 176 | ), 177 | focusedBorder: OutlineInputBorder( 178 | borderRadius: BorderRadius.circular(16), 179 | borderSide: BorderSide( 180 | color: darkColorScheme.primary, 181 | width: 2, 182 | ), 183 | ), 184 | errorBorder: OutlineInputBorder( 185 | borderRadius: BorderRadius.circular(16), 186 | borderSide: BorderSide( 187 | color: darkColorScheme.error, 188 | width: 1, 189 | ), 190 | ), 191 | focusedErrorBorder: OutlineInputBorder( 192 | borderRadius: BorderRadius.circular(16), 193 | borderSide: BorderSide( 194 | color: darkColorScheme.error, 195 | width: 2, 196 | ), 197 | ), 198 | contentPadding: const EdgeInsets.all(20), 199 | labelStyle: TextStyle( 200 | color: darkColorScheme.onSurfaceVariant, 201 | fontSize: 16, 202 | ), 203 | hintStyle: TextStyle( 204 | color: darkColorScheme.onSurfaceVariant.withValues(alpha: 0.6), 205 | fontSize: 16, 206 | ), 207 | ), 208 | 209 | // Elevated Button Theme 210 | elevatedButtonTheme: ElevatedButtonThemeData( 211 | style: ElevatedButton.styleFrom( 212 | elevation: 0, 213 | backgroundColor: darkColorScheme.primary, 214 | foregroundColor: darkColorScheme.onPrimary, 215 | disabledBackgroundColor: darkColorScheme.onSurface.withValues(alpha: 0.12), 216 | disabledForegroundColor: darkColorScheme.onSurface.withValues(alpha: 0.38), 217 | padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), 218 | minimumSize: const Size(0, 56), 219 | shape: RoundedRectangleBorder( 220 | borderRadius: BorderRadius.circular(16), 221 | ), 222 | textStyle: const TextStyle( 223 | fontSize: 16, 224 | fontWeight: FontWeight.w600, 225 | ), 226 | ), 227 | ), 228 | 229 | // Text Button Theme 230 | textButtonTheme: TextButtonThemeData( 231 | style: TextButton.styleFrom( 232 | foregroundColor: darkColorScheme.primary, 233 | padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), 234 | minimumSize: const Size(0, 48), 235 | shape: RoundedRectangleBorder( 236 | borderRadius: BorderRadius.circular(12), 237 | ), 238 | textStyle: const TextStyle( 239 | fontSize: 16, 240 | fontWeight: FontWeight.w600, 241 | ), 242 | ), 243 | ), 244 | 245 | // Card Theme 246 | cardTheme: CardThemeData( 247 | elevation: 0, 248 | color: darkColorScheme.surfaceContainerHigh.withValues(alpha: 0.3), 249 | shape: RoundedRectangleBorder( 250 | borderRadius: BorderRadius.circular(20), 251 | side: BorderSide( 252 | color: darkColorScheme.outline.withValues(alpha: 0.2), 253 | width: 1, 254 | ), 255 | ), 256 | margin: const EdgeInsets.all(8), 257 | ), 258 | 259 | // Scaffold Background 260 | scaffoldBackgroundColor: darkColorScheme.surface, 261 | ); 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /lib/domain/authentication/auth_failures.dart: -------------------------------------------------------------------------------- 1 | import "package:freezed_annotation/freezed_annotation.dart"; 2 | 3 | part "auth_failures.freezed.dart"; 4 | 5 | @freezed 6 | class AuthFailures with _$AuthFailures { 7 | const factory AuthFailures.serverError() = ServerError; 8 | 9 | const factory AuthFailures.emailAlreadyInUse() = EmailAlreadyInUse; 10 | 11 | const factory AuthFailures.invalidEmailAndPasswordCombination() = InvalidEmailAndPasswordCombination; 12 | } 13 | -------------------------------------------------------------------------------- /lib/domain/authentication/auth_failures.freezed.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | // coverage:ignore-file 3 | // ignore_for_file: type=lint 4 | // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark 5 | 6 | part of 'auth_failures.dart'; 7 | 8 | // ************************************************************************** 9 | // FreezedGenerator 10 | // ************************************************************************** 11 | 12 | // dart format off 13 | T _$identity(T value) => value; 14 | 15 | /// @nodoc 16 | mixin _$AuthFailures { 17 | @override 18 | bool operator ==(Object other) { 19 | return identical(this, other) || (other.runtimeType == runtimeType && other is AuthFailures); 20 | } 21 | 22 | @override 23 | int get hashCode => runtimeType.hashCode; 24 | 25 | @override 26 | String toString() { 27 | return 'AuthFailures()'; 28 | } 29 | } 30 | 31 | /// @nodoc 32 | class $AuthFailuresCopyWith<$Res> { 33 | $AuthFailuresCopyWith(AuthFailures _, $Res Function(AuthFailures) __); 34 | } 35 | 36 | /// Adds pattern-matching-related methods to [AuthFailures]. 37 | extension AuthFailuresPatterns on AuthFailures { 38 | /// A variant of `map` that fallback to returning `orElse`. 39 | /// 40 | /// It is equivalent to doing: 41 | /// ```dart 42 | /// switch (sealedClass) { 43 | /// case final Subclass value: 44 | /// return ...; 45 | /// case _: 46 | /// return orElse(); 47 | /// } 48 | /// ``` 49 | 50 | @optionalTypeArgs 51 | TResult maybeMap({ 52 | TResult Function(ServerError value)? serverError, 53 | TResult Function(EmailAlreadyInUse value)? emailAlreadyInUse, 54 | TResult Function(InvalidEmailAndPasswordCombination value)? invalidEmailAndPasswordCombination, 55 | required TResult orElse(), 56 | }) { 57 | final _that = this; 58 | switch (_that) { 59 | case ServerError() when serverError != null: 60 | return serverError(_that); 61 | case EmailAlreadyInUse() when emailAlreadyInUse != null: 62 | return emailAlreadyInUse(_that); 63 | case InvalidEmailAndPasswordCombination() when invalidEmailAndPasswordCombination != null: 64 | return invalidEmailAndPasswordCombination(_that); 65 | case _: 66 | return orElse(); 67 | } 68 | } 69 | 70 | /// A `switch`-like method, using callbacks. 71 | /// 72 | /// Callbacks receives the raw object, upcasted. 73 | /// It is equivalent to doing: 74 | /// ```dart 75 | /// switch (sealedClass) { 76 | /// case final Subclass value: 77 | /// return ...; 78 | /// case final Subclass2 value: 79 | /// return ...; 80 | /// } 81 | /// ``` 82 | 83 | @optionalTypeArgs 84 | TResult map({ 85 | required TResult Function(ServerError value) serverError, 86 | required TResult Function(EmailAlreadyInUse value) emailAlreadyInUse, 87 | required TResult Function(InvalidEmailAndPasswordCombination value) invalidEmailAndPasswordCombination, 88 | }) { 89 | final _that = this; 90 | switch (_that) { 91 | case ServerError(): 92 | return serverError(_that); 93 | case EmailAlreadyInUse(): 94 | return emailAlreadyInUse(_that); 95 | case InvalidEmailAndPasswordCombination(): 96 | return invalidEmailAndPasswordCombination(_that); 97 | case _: 98 | throw StateError('Unexpected subclass'); 99 | } 100 | } 101 | 102 | /// A variant of `map` that fallback to returning `null`. 103 | /// 104 | /// It is equivalent to doing: 105 | /// ```dart 106 | /// switch (sealedClass) { 107 | /// case final Subclass value: 108 | /// return ...; 109 | /// case _: 110 | /// return null; 111 | /// } 112 | /// ``` 113 | 114 | @optionalTypeArgs 115 | TResult? mapOrNull({ 116 | TResult? Function(ServerError value)? serverError, 117 | TResult? Function(EmailAlreadyInUse value)? emailAlreadyInUse, 118 | TResult? Function(InvalidEmailAndPasswordCombination value)? invalidEmailAndPasswordCombination, 119 | }) { 120 | final _that = this; 121 | switch (_that) { 122 | case ServerError() when serverError != null: 123 | return serverError(_that); 124 | case EmailAlreadyInUse() when emailAlreadyInUse != null: 125 | return emailAlreadyInUse(_that); 126 | case InvalidEmailAndPasswordCombination() when invalidEmailAndPasswordCombination != null: 127 | return invalidEmailAndPasswordCombination(_that); 128 | case _: 129 | return null; 130 | } 131 | } 132 | 133 | /// A variant of `when` that fallback to an `orElse` callback. 134 | /// 135 | /// It is equivalent to doing: 136 | /// ```dart 137 | /// switch (sealedClass) { 138 | /// case Subclass(:final field): 139 | /// return ...; 140 | /// case _: 141 | /// return orElse(); 142 | /// } 143 | /// ``` 144 | 145 | @optionalTypeArgs 146 | TResult maybeWhen({ 147 | TResult Function()? serverError, 148 | TResult Function()? emailAlreadyInUse, 149 | TResult Function()? invalidEmailAndPasswordCombination, 150 | required TResult orElse(), 151 | }) { 152 | final _that = this; 153 | switch (_that) { 154 | case ServerError() when serverError != null: 155 | return serverError(); 156 | case EmailAlreadyInUse() when emailAlreadyInUse != null: 157 | return emailAlreadyInUse(); 158 | case InvalidEmailAndPasswordCombination() when invalidEmailAndPasswordCombination != null: 159 | return invalidEmailAndPasswordCombination(); 160 | case _: 161 | return orElse(); 162 | } 163 | } 164 | 165 | /// A `switch`-like method, using callbacks. 166 | /// 167 | /// As opposed to `map`, this offers destructuring. 168 | /// It is equivalent to doing: 169 | /// ```dart 170 | /// switch (sealedClass) { 171 | /// case Subclass(:final field): 172 | /// return ...; 173 | /// case Subclass2(:final field2): 174 | /// return ...; 175 | /// } 176 | /// ``` 177 | 178 | @optionalTypeArgs 179 | TResult when({ 180 | required TResult Function() serverError, 181 | required TResult Function() emailAlreadyInUse, 182 | required TResult Function() invalidEmailAndPasswordCombination, 183 | }) { 184 | final _that = this; 185 | switch (_that) { 186 | case ServerError(): 187 | return serverError(); 188 | case EmailAlreadyInUse(): 189 | return emailAlreadyInUse(); 190 | case InvalidEmailAndPasswordCombination(): 191 | return invalidEmailAndPasswordCombination(); 192 | case _: 193 | throw StateError('Unexpected subclass'); 194 | } 195 | } 196 | 197 | /// A variant of `when` that fallback to returning `null` 198 | /// 199 | /// It is equivalent to doing: 200 | /// ```dart 201 | /// switch (sealedClass) { 202 | /// case Subclass(:final field): 203 | /// return ...; 204 | /// case _: 205 | /// return null; 206 | /// } 207 | /// ``` 208 | 209 | @optionalTypeArgs 210 | TResult? whenOrNull({ 211 | TResult? Function()? serverError, 212 | TResult? Function()? emailAlreadyInUse, 213 | TResult? Function()? invalidEmailAndPasswordCombination, 214 | }) { 215 | final _that = this; 216 | switch (_that) { 217 | case ServerError() when serverError != null: 218 | return serverError(); 219 | case EmailAlreadyInUse() when emailAlreadyInUse != null: 220 | return emailAlreadyInUse(); 221 | case InvalidEmailAndPasswordCombination() when invalidEmailAndPasswordCombination != null: 222 | return invalidEmailAndPasswordCombination(); 223 | case _: 224 | return null; 225 | } 226 | } 227 | } 228 | 229 | /// @nodoc 230 | 231 | class ServerError implements AuthFailures { 232 | const ServerError(); 233 | 234 | @override 235 | bool operator ==(Object other) { 236 | return identical(this, other) || (other.runtimeType == runtimeType && other is ServerError); 237 | } 238 | 239 | @override 240 | int get hashCode => runtimeType.hashCode; 241 | 242 | @override 243 | String toString() { 244 | return 'AuthFailures.serverError()'; 245 | } 246 | } 247 | 248 | /// @nodoc 249 | 250 | class EmailAlreadyInUse implements AuthFailures { 251 | const EmailAlreadyInUse(); 252 | 253 | @override 254 | bool operator ==(Object other) { 255 | return identical(this, other) || (other.runtimeType == runtimeType && other is EmailAlreadyInUse); 256 | } 257 | 258 | @override 259 | int get hashCode => runtimeType.hashCode; 260 | 261 | @override 262 | String toString() { 263 | return 'AuthFailures.emailAlreadyInUse()'; 264 | } 265 | } 266 | 267 | /// @nodoc 268 | 269 | class InvalidEmailAndPasswordCombination implements AuthFailures { 270 | const InvalidEmailAndPasswordCombination(); 271 | 272 | @override 273 | bool operator ==(Object other) { 274 | return identical(this, other) || (other.runtimeType == runtimeType && other is InvalidEmailAndPasswordCombination); 275 | } 276 | 277 | @override 278 | int get hashCode => runtimeType.hashCode; 279 | 280 | @override 281 | String toString() { 282 | return 'AuthFailures.invalidEmailAndPasswordCombination()'; 283 | } 284 | } 285 | 286 | // dart format on 287 | -------------------------------------------------------------------------------- /lib/domain/authentication/auth_value_failures.dart: -------------------------------------------------------------------------------- 1 | // Package imports: 2 | import "package:freezed_annotation/freezed_annotation.dart"; 3 | 4 | part "auth_value_failures.freezed.dart"; 5 | 6 | @freezed 7 | sealed class AuthValueFailures with _$AuthValueFailures { 8 | const factory AuthValueFailures.invalidEmail({required String? failedValue}) = InvalidEmail; 9 | 10 | const factory AuthValueFailures.shortPassword({required String? failedValue}) = ShortPassword; 11 | 12 | const factory AuthValueFailures.noSpecialSymbol({required String? failedValue}) = NoSpecialSymbol; 13 | 14 | const factory AuthValueFailures.noUpperCase({required String? failedValue}) = NoUpperCase; 15 | 16 | const factory AuthValueFailures.noNumber({required String? failedValue}) = NoNumber; 17 | } 18 | -------------------------------------------------------------------------------- /lib/domain/authentication/auth_value_objects.dart: -------------------------------------------------------------------------------- 1 | import "package:firebase_auth_flutter_ddd/domain/authentication/auth_value_failures.dart"; 2 | import "package:firebase_auth_flutter_ddd/domain/authentication/auth_value_validators.dart"; 3 | import "package:firebase_auth_flutter_ddd/domain/core/value_object.dart"; 4 | import "package:fpdart/fpdart.dart"; 5 | 6 | class EmailAddress extends ValueObject { 7 | factory EmailAddress({String? email}) { 8 | return EmailAddress._(validateEmailAddress(email: email)); 9 | } 10 | 11 | const EmailAddress._(this.valueObject); 12 | 13 | @override 14 | final Either, String>? valueObject; 15 | } 16 | 17 | class Password extends ValueObject { 18 | factory Password({String? password}) { 19 | return Password._(validatePassword(password: password)); 20 | } 21 | 22 | const Password._(this.valueObject); 23 | 24 | @override 25 | final Either, String>? valueObject; 26 | } 27 | -------------------------------------------------------------------------------- /lib/domain/authentication/auth_value_validators.dart: -------------------------------------------------------------------------------- 1 | import "package:firebase_auth_flutter_ddd/domain/authentication/auth_value_failures.dart"; 2 | import "package:fpdart/fpdart.dart"; 3 | 4 | Either, String> validateEmailAddress({ 5 | required String? email, 6 | }) { 7 | // Improved email regex pattern for better validation 8 | const emailRegex = 9 | r"^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$"; 10 | final emailPattern = RegExp(emailRegex); 11 | 12 | if (email == null || email.trim().isEmpty) { 13 | return left(AuthValueFailures.invalidEmail(failedValue: email ?? "")); 14 | } 15 | 16 | final trimmedEmail = email.trim().toLowerCase(); 17 | if (emailPattern.hasMatch(trimmedEmail)) { 18 | return right(trimmedEmail); 19 | } else { 20 | return left(AuthValueFailures.invalidEmail(failedValue: email)); 21 | } 22 | } 23 | 24 | Either, String> validatePassword({ 25 | required String? password, 26 | }) { 27 | if (password == null || password.isEmpty) { 28 | return left(AuthValueFailures.shortPassword(failedValue: password ?? "")); 29 | } 30 | 31 | final hasMinLength = password.length >= 8; // Changed from > 6 to >= 8 for better security 32 | final hasUppercase = RegExp(r"[A-Z]").hasMatch(password); 33 | final hasDigits = RegExp(r"[0-9]").hasMatch(password); 34 | final hasSpecialCharacters = RegExp(r'[!@#$%^&*(),.?":{}|<>]').hasMatch(password); 35 | 36 | if (!hasMinLength) { 37 | return left(AuthValueFailures.shortPassword(failedValue: password)); 38 | } else if (!hasUppercase) { 39 | return left(AuthValueFailures.noUpperCase(failedValue: password)); 40 | } else if (!hasDigits) { 41 | return left(AuthValueFailures.noNumber(failedValue: password)); 42 | } else if (!hasSpecialCharacters) { 43 | return left(AuthValueFailures.noSpecialSymbol(failedValue: password)); 44 | } else { 45 | return right(password); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/domain/authentication/i_auth_facade.dart: -------------------------------------------------------------------------------- 1 | import "package:firebase_auth_flutter_ddd/domain/authentication/auth_failures.dart"; 2 | import "package:firebase_auth_flutter_ddd/domain/authentication/auth_value_objects.dart"; 3 | import "package:fpdart/fpdart.dart"; 4 | 5 | abstract class IAuthFacade { 6 | Future> registerWithEmailAndPassword( 7 | {required EmailAddress emailAddress, required Password password}); 8 | 9 | Future> signInWithEmailAndPassword( 10 | {required EmailAddress emailAddress, required Password password}); 11 | 12 | Future> getSignedInUser(); 13 | 14 | Future signOut(); 15 | } 16 | -------------------------------------------------------------------------------- /lib/domain/core/errors.dart: -------------------------------------------------------------------------------- 1 | import "package:firebase_auth_flutter_ddd/domain/authentication/auth_value_failures.dart"; 2 | 3 | class UnExpectedValueError extends Error { 4 | UnExpectedValueError(this.authValueFailures); 5 | 6 | final AuthValueFailures? authValueFailures; 7 | 8 | @override 9 | String toString() { 10 | return Error.safeToString("UnExpectedValueError{authValueFailures: $authValueFailures}"); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/domain/core/value_object.dart: -------------------------------------------------------------------------------- 1 | import "package:firebase_auth_flutter_ddd/domain/authentication/auth_value_failures.dart"; 2 | import "package:flutter/cupertino.dart"; 3 | import "package:fpdart/fpdart.dart"; 4 | 5 | @immutable 6 | abstract class ValueObject { 7 | const ValueObject(); 8 | 9 | Either, T>? get valueObject; 10 | 11 | bool isValid() => valueObject!.isRight(); 12 | 13 | @override 14 | bool operator ==(Object other) => 15 | identical(this, other) || other is ValueObject && runtimeType == other.runtimeType; 16 | 17 | @override 18 | int get hashCode => 0; 19 | 20 | @override 21 | String toString() { 22 | return "AuthValueObjects{$valueObject}"; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/firebase_options.dart: -------------------------------------------------------------------------------- 1 | // File generated by FlutterFire CLI. 2 | // ignore_for_file: lines_longer_than_80_chars, avoid_classes_with_only_static_members 3 | import "package:firebase_core/firebase_core.dart" show FirebaseOptions; 4 | import "package:flutter/foundation.dart" show defaultTargetPlatform, kIsWeb, TargetPlatform; 5 | 6 | /// Default [FirebaseOptions] for use with your Firebase apps. 7 | /// 8 | /// Example: 9 | /// ```dart 10 | /// import 'firebase_options.dart'; 11 | /// // ... 12 | /// await Firebase.initializeApp( 13 | /// options: DefaultFirebaseOptions.currentPlatform, 14 | /// ); 15 | /// ``` 16 | class DefaultFirebaseOptions { 17 | static FirebaseOptions get currentPlatform { 18 | if (kIsWeb) { 19 | throw UnsupportedError( 20 | "DefaultFirebaseOptions have not been configured for web - " 21 | "you can reconfigure this by running the FlutterFire CLI again.", 22 | ); 23 | } 24 | switch (defaultTargetPlatform) { 25 | case TargetPlatform.android: 26 | return android; 27 | case TargetPlatform.iOS: 28 | return ios; 29 | case TargetPlatform.macOS: 30 | throw UnsupportedError( 31 | "DefaultFirebaseOptions have not been configured for macos - " 32 | "you can reconfigure this by running the FlutterFire CLI again.", 33 | ); 34 | case TargetPlatform.windows: 35 | throw UnsupportedError( 36 | "DefaultFirebaseOptions have not been configured for windows - " 37 | "you can reconfigure this by running the FlutterFire CLI again.", 38 | ); 39 | case TargetPlatform.linux: 40 | throw UnsupportedError( 41 | "DefaultFirebaseOptions have not been configured for linux - " 42 | "you can reconfigure this by running the FlutterFire CLI again.", 43 | ); 44 | default: 45 | throw UnsupportedError( 46 | "DefaultFirebaseOptions are not supported for this platform.", 47 | ); 48 | } 49 | } 50 | 51 | static const FirebaseOptions android = FirebaseOptions( 52 | apiKey: "AIzaSyBZMVnawoT-EeCyymhzl8DvSOC_xVGXbqY", 53 | appId: "1:846249838909:android:c59123c52149d476345d54", 54 | messagingSenderId: "846249838909", 55 | projectId: "edu-app-4717e", 56 | storageBucket: "edu-app-4717e.appspot.com", 57 | ); 58 | 59 | static const FirebaseOptions ios = FirebaseOptions( 60 | apiKey: "AIzaSyDbWt27RbXb_0zUq3v6hf6Gdr_ghQHn0Vg", 61 | appId: "1:846249838909:ios:8d8b184cfaac3a32345d54", 62 | messagingSenderId: "846249838909", 63 | projectId: "edu-app-4717e", 64 | storageBucket: "edu-app-4717e.appspot.com", 65 | androidClientId: "846249838909-jve30eor0thvaq7jdfl59gdikdiukj3r.apps.googleusercontent.com", 66 | iosClientId: "846249838909-ipe51fptev2i57sunfjn6a0can051t49.apps.googleusercontent.com", 67 | iosBundleId: "dev.pythonhub.firebaseAuthFlutterDdd", 68 | ); 69 | } 70 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import "package:firebase_auth_flutter_ddd/app.dart"; 2 | import "package:firebase_auth_flutter_ddd/firebase_options.dart"; 3 | import "package:firebase_core/firebase_core.dart"; 4 | import "package:flutter/material.dart"; 5 | import "package:hooks_riverpod/hooks_riverpod.dart"; 6 | 7 | Future main() async { 8 | WidgetsFlutterBinding.ensureInitialized(); 9 | await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); 10 | runApp( 11 | const ProviderScope( 12 | child: FirebaseAuthenticationDDD(), 13 | ), 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /lib/screens/login_page.dart: -------------------------------------------------------------------------------- 1 | import "package:firebase_auth_flutter_ddd/core/theme/animated_widgets.dart"; 2 | import "package:firebase_auth_flutter_ddd/domain/authentication/auth_failures.dart"; 3 | import "package:firebase_auth_flutter_ddd/screens/home_page.dart"; 4 | import "package:flutter/cupertino.dart"; 5 | import "package:flutter/material.dart"; 6 | import "package:flutter/services.dart"; 7 | import "package:flutter_hooks/flutter_hooks.dart"; 8 | import "package:hooks_riverpod/hooks_riverpod.dart"; 9 | 10 | import "../application/authentication/auth_state_controller.dart"; 11 | import "../application/authentication/auth_states.dart"; 12 | import "registration_page.dart"; 13 | import "utils/custom_snackbar.dart"; 14 | 15 | class LoginPage extends HookConsumerWidget { 16 | const LoginPage({super.key}); 17 | 18 | @override 19 | Widget build(BuildContext context, WidgetRef ref) { 20 | // Use hooks to create persistent controllers that survive rebuilds 21 | final formKey = useMemoized(() => GlobalKey()); 22 | final emailController = useTextEditingController(); 23 | final passwordController = useTextEditingController(); 24 | 25 | final formStates = ref.watch(authStateControllerProvider); 26 | final formNotifier = ref.read(authStateControllerProvider.notifier); 27 | 28 | ref.listen(authStateControllerProvider, (previous, next) { 29 | _handleAuthStateChanges(context, next); 30 | }); 31 | 32 | return Scaffold( 33 | backgroundColor: Theme.of(context).colorScheme.surface, 34 | body: SafeArea( 35 | child: GestureDetector( 36 | onTap: () => FocusScope.of(context).unfocus(), 37 | child: SingleChildScrollView( 38 | padding: const EdgeInsets.all(24), 39 | child: Form( 40 | key: formKey, 41 | child: Column( 42 | crossAxisAlignment: CrossAxisAlignment.stretch, 43 | children: [ 44 | const SizedBox(height: 40), 45 | _buildHeroSection(context), 46 | const SizedBox(height: 48), 47 | _buildEmailField(emailController, formNotifier), 48 | const SizedBox(height: 20), 49 | _buildPasswordField(passwordController, formNotifier), 50 | const SizedBox(height: 32), 51 | _buildSignInButton(context, formKey, formStates, formNotifier), 52 | const SizedBox(height: 24), 53 | _buildSignUpPrompt(context), 54 | ], 55 | ), 56 | ), 57 | ), 58 | ), 59 | ), 60 | ); 61 | } 62 | 63 | void _handleAuthStateChanges(BuildContext context, AuthStates state) { 64 | state.authFailureOrSuccess.fold( 65 | () {}, 66 | (either) => either.fold( 67 | (failure) => _showErrorSnackBar(context, failure), 68 | (success) => _handleSuccessfulAuth(context), 69 | ), 70 | ); 71 | } 72 | 73 | void _showErrorSnackBar(BuildContext context, AuthFailures failure) { 74 | HapticFeedback.lightImpact(); 75 | final errorMessage = failure.when( 76 | serverError: () => "Server error occurred", 77 | emailAlreadyInUse: () => "User already exists", 78 | invalidEmailAndPasswordCombination: () => "Invalid email or password", 79 | ); 80 | 81 | buildCustomSnackBar( 82 | context: context, 83 | flashBackground: Theme.of(context).colorScheme.error, 84 | icon: Icons.error_outline_rounded, 85 | content: Text( 86 | errorMessage, 87 | style: Theme.of(context).textTheme.bodyLarge?.copyWith(color: Colors.white), 88 | ), 89 | ); 90 | } 91 | 92 | void _handleSuccessfulAuth(BuildContext context) { 93 | HapticFeedback.lightImpact(); 94 | buildCustomSnackBar( 95 | context: context, 96 | flashBackground: Theme.of(context).colorScheme.primary, 97 | icon: CupertinoIcons.check_mark_circled_solid, 98 | content: Text( 99 | "Welcome back! Login successful", 100 | style: Theme.of(context).textTheme.bodyLarge?.copyWith(color: Colors.white), 101 | ), 102 | ); 103 | 104 | Navigator.pushReplacement( 105 | context, 106 | PageRouteBuilder( 107 | pageBuilder: (context, animation, secondaryAnimation) => const HomePage(), 108 | transitionsBuilder: (context, animation, secondaryAnimation, child) { 109 | const begin = Offset(1.0, 0.0); 110 | const end = Offset.zero; 111 | const curve = Curves.easeInOutCubic; 112 | final tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve)); 113 | return SlideTransition(position: animation.drive(tween), child: child); 114 | }, 115 | transitionDuration: const Duration(milliseconds: 300), 116 | ), 117 | ); 118 | } 119 | 120 | Widget _buildHeroSection(BuildContext context) { 121 | return SlideInWidget( 122 | delay: 100, 123 | child: Column( 124 | children: [ 125 | Container( 126 | height: 120, 127 | width: 120, 128 | decoration: BoxDecoration( 129 | color: Theme.of(context).colorScheme.primaryContainer, 130 | shape: BoxShape.circle, 131 | ), 132 | child: Icon( 133 | Icons.login_rounded, 134 | size: 64, 135 | color: Theme.of(context).colorScheme.onPrimaryContainer, 136 | ), 137 | ), 138 | const SizedBox(height: 32), 139 | Text( 140 | "Welcome back", 141 | style: Theme.of(context).textTheme.headlineMedium?.copyWith( 142 | fontWeight: FontWeight.bold, 143 | color: Theme.of(context).colorScheme.onSurface, 144 | ), 145 | ), 146 | const SizedBox(height: 8), 147 | Text( 148 | "Sign in to your account to continue", 149 | style: Theme.of(context).textTheme.bodyLarge?.copyWith( 150 | color: Theme.of(context).colorScheme.onSurfaceVariant, 151 | ), 152 | ), 153 | ], 154 | ), 155 | ); 156 | } 157 | 158 | Widget _buildEmailField(TextEditingController controller, AuthStateController notifier) { 159 | return SlideInWidget( 160 | delay: 200, 161 | child: AnimatedFormField( 162 | label: "Email", 163 | hint: "Enter your email address", 164 | keyboardType: TextInputType.emailAddress, 165 | prefixIcon: Icons.email_outlined, 166 | controller: controller, 167 | validator: _validateEmail, 168 | onChanged: notifier.emailChanged, 169 | ), 170 | ); 171 | } 172 | 173 | Widget _buildPasswordField(TextEditingController controller, AuthStateController notifier) { 174 | return SlideInWidget( 175 | delay: 300, 176 | child: AnimatedFormField( 177 | label: "Password", 178 | hint: "Enter your password", 179 | obscureText: true, 180 | prefixIcon: Icons.lock_outline, 181 | controller: controller, 182 | validator: _validatePassword, 183 | onChanged: notifier.passwordChanged, 184 | ), 185 | ); 186 | } 187 | 188 | Widget _buildSignInButton( 189 | BuildContext context, 190 | GlobalKey formKey, 191 | AuthStates formStates, 192 | AuthStateController formNotifier, 193 | ) { 194 | return SlideInWidget( 195 | delay: 400, 196 | child: AnimatedButton( 197 | text: "Sign In", 198 | icon: Icons.arrow_forward_rounded, 199 | isLoading: formStates.isSubmitting, 200 | onPressed: formStates.isSubmitting ? null : () => _handleSignIn(formKey, formNotifier), 201 | ), 202 | ); 203 | } 204 | 205 | Widget _buildSignUpPrompt(BuildContext context) { 206 | return SlideInWidget( 207 | delay: 500, 208 | child: Row( 209 | mainAxisAlignment: MainAxisAlignment.center, 210 | children: [ 211 | Text( 212 | "Don't have an account? ", 213 | style: Theme.of(context).textTheme.bodyMedium, 214 | ), 215 | TextButton( 216 | onPressed: () => _navigateToRegistration(context), 217 | child: Text( 218 | "Sign Up", 219 | style: TextStyle( 220 | color: Theme.of(context).colorScheme.primary, 221 | fontWeight: FontWeight.w600, 222 | ), 223 | ), 224 | ), 225 | ], 226 | ), 227 | ); 228 | } 229 | 230 | String? _validateEmail(String? value) { 231 | if (value == null || value.trim().isEmpty) { 232 | return "Please enter your email"; 233 | } 234 | if (!value.contains("@")) { 235 | return "Please enter a valid email"; 236 | } 237 | return null; 238 | } 239 | 240 | String? _validatePassword(String? value) { 241 | if (value == null || value.isEmpty) { 242 | return "Please enter your password"; 243 | } 244 | if (value.length < 8) { 245 | return "Password must be at least 8 characters"; 246 | } 247 | return null; 248 | } 249 | 250 | void _handleSignIn(GlobalKey formKey, AuthStateController notifier) { 251 | if (formKey.currentState?.validate() ?? false) { 252 | HapticFeedback.lightImpact(); 253 | notifier.signInWithEmailAndPassword(); 254 | } else { 255 | HapticFeedback.lightImpact(); 256 | } 257 | } 258 | 259 | void _navigateToRegistration(BuildContext context) { 260 | Navigator.push( 261 | context, 262 | PageRouteBuilder( 263 | pageBuilder: (context, animation, secondaryAnimation) => const RegistrationPage(), 264 | transitionsBuilder: (context, animation, secondaryAnimation, child) { 265 | const begin = Offset(1.0, 0.0); 266 | const end = Offset.zero; 267 | const curve = Curves.easeInOutCubic; 268 | final tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve)); 269 | return SlideTransition(position: animation.drive(tween), child: child); 270 | }, 271 | transitionDuration: const Duration(milliseconds: 300), 272 | ), 273 | ); 274 | } 275 | } 276 | -------------------------------------------------------------------------------- /lib/screens/registration_page.dart: -------------------------------------------------------------------------------- 1 | import "package:firebase_auth_flutter_ddd/core/theme/animated_widgets.dart"; 2 | import "package:firebase_auth_flutter_ddd/domain/authentication/auth_failures.dart"; 3 | import "package:flutter/cupertino.dart"; 4 | import "package:flutter/material.dart"; 5 | import "package:flutter/services.dart"; 6 | import "package:flutter_hooks/flutter_hooks.dart"; 7 | import "package:hooks_riverpod/hooks_riverpod.dart"; 8 | 9 | import "../application/authentication/auth_state_controller.dart"; 10 | import "../application/authentication/auth_states.dart"; 11 | import "login_page.dart"; 12 | import "utils/custom_snackbar.dart"; 13 | 14 | class RegistrationPage extends HookConsumerWidget { 15 | const RegistrationPage({super.key}); 16 | 17 | @override 18 | Widget build(BuildContext context, WidgetRef ref) { 19 | // Use hooks to create persistent controllers that survive rebuilds 20 | final formKey = useMemoized(() => GlobalKey()); 21 | final emailController = useTextEditingController(); 22 | final passwordController = useTextEditingController(); 23 | final confirmPasswordController = useTextEditingController(); 24 | 25 | final formStates = ref.watch(authStateControllerProvider); 26 | final formNotifier = ref.read(authStateControllerProvider.notifier); 27 | 28 | ref.listen(authStateControllerProvider, (previous, next) { 29 | _handleAuthStateChanges(context, next); 30 | }); 31 | 32 | return Scaffold( 33 | backgroundColor: Theme.of(context).colorScheme.surface, 34 | body: SafeArea( 35 | child: GestureDetector( 36 | onTap: () => FocusScope.of(context).unfocus(), 37 | child: SingleChildScrollView( 38 | padding: const EdgeInsets.all(24), 39 | child: Form( 40 | key: formKey, 41 | child: Column( 42 | spacing: 20, 43 | crossAxisAlignment: CrossAxisAlignment.stretch, 44 | children: [ 45 | const SizedBox(height: 20), 46 | _buildBackButton(context), 47 | _buildHeroSection(context), 48 | const SizedBox(height: 28), 49 | _buildEmailField(emailController, formNotifier), 50 | _buildPasswordField(passwordController, formNotifier), 51 | _buildConfirmPasswordField(confirmPasswordController, passwordController), 52 | const SizedBox(height: 12), 53 | _buildSignUpButton(context, formKey, formStates, formNotifier), 54 | const SizedBox(height: 4), 55 | _buildSignInPrompt(context), 56 | ], 57 | ), 58 | ), 59 | ), 60 | ), 61 | ), 62 | ); 63 | } 64 | 65 | void _handleAuthStateChanges(BuildContext context, AuthStates state) { 66 | state.authFailureOrSuccess.fold( 67 | () {}, 68 | (either) => either.fold( 69 | (failure) => _showErrorSnackBar(context, failure), 70 | (success) => _handleSuccessfulRegistration(context), 71 | ), 72 | ); 73 | } 74 | 75 | void _showErrorSnackBar(BuildContext context, AuthFailures failure) { 76 | HapticFeedback.lightImpact(); 77 | final errorMessage = failure.when( 78 | serverError: () => "Server error occurred", 79 | emailAlreadyInUse: () => "This email is already registered", 80 | invalidEmailAndPasswordCombination: () => "Invalid email or password format", 81 | ); 82 | 83 | buildCustomSnackBar( 84 | context: context, 85 | flashBackground: Theme.of(context).colorScheme.error, 86 | icon: Icons.error_outline_rounded, 87 | content: Text( 88 | errorMessage, 89 | style: Theme.of(context).textTheme.bodyLarge?.copyWith(color: Colors.white), 90 | ), 91 | ); 92 | } 93 | 94 | void _handleSuccessfulRegistration(BuildContext context) { 95 | HapticFeedback.lightImpact(); 96 | buildCustomSnackBar( 97 | context: context, 98 | flashBackground: Theme.of(context).colorScheme.primary, 99 | icon: CupertinoIcons.check_mark_circled_solid, 100 | content: Text( 101 | "Account created successfully! Welcome aboard!", 102 | style: Theme.of(context).textTheme.bodyLarge?.copyWith(color: Colors.white), 103 | ), 104 | ); 105 | 106 | Navigator.pushReplacement( 107 | context, 108 | PageRouteBuilder( 109 | pageBuilder: (context, animation, secondaryAnimation) => const LoginPage(), 110 | transitionsBuilder: (context, animation, secondaryAnimation, child) { 111 | const begin = Offset(-1.0, 0.0); 112 | const end = Offset.zero; 113 | const curve = Curves.easeInOutCubic; 114 | final tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve)); 115 | return SlideTransition(position: animation.drive(tween), child: child); 116 | }, 117 | transitionDuration: const Duration(milliseconds: 300), 118 | ), 119 | ); 120 | } 121 | 122 | Widget _buildBackButton(BuildContext context) { 123 | return SlideInWidget( 124 | delay: 50, 125 | begin: const Offset(-0.3, 0), 126 | child: Align( 127 | alignment: Alignment.centerLeft, 128 | child: IconButton( 129 | onPressed: () => Navigator.pop(context), 130 | icon: Icon( 131 | Icons.arrow_back_rounded, 132 | color: Theme.of(context).colorScheme.onSurface, 133 | ), 134 | ), 135 | ), 136 | ); 137 | } 138 | 139 | Widget _buildHeroSection(BuildContext context) { 140 | return SlideInWidget( 141 | delay: 100, 142 | child: Column( 143 | children: [ 144 | Container( 145 | height: 120, 146 | width: 120, 147 | decoration: BoxDecoration( 148 | color: Theme.of(context).colorScheme.primaryContainer, 149 | shape: BoxShape.circle, 150 | ), 151 | child: Icon( 152 | Icons.person_add_rounded, 153 | size: 64, 154 | color: Theme.of(context).colorScheme.onPrimaryContainer, 155 | ), 156 | ), 157 | const SizedBox(height: 32), 158 | Text( 159 | "Create Account", 160 | style: Theme.of(context).textTheme.headlineMedium?.copyWith( 161 | fontWeight: FontWeight.bold, 162 | color: Theme.of(context).colorScheme.onSurface, 163 | ), 164 | ), 165 | const SizedBox(height: 8), 166 | Text( 167 | "Join us and start your journey today", 168 | style: Theme.of(context).textTheme.bodyLarge?.copyWith( 169 | color: Theme.of(context).colorScheme.onSurfaceVariant, 170 | ), 171 | ), 172 | ], 173 | ), 174 | ); 175 | } 176 | 177 | Widget _buildEmailField(TextEditingController controller, AuthStateController notifier) { 178 | return SlideInWidget( 179 | delay: 200, 180 | child: AnimatedFormField( 181 | label: "Email", 182 | hint: "Enter your email address", 183 | keyboardType: TextInputType.emailAddress, 184 | prefixIcon: Icons.email_outlined, 185 | controller: controller, 186 | validator: _validateEmail, 187 | onChanged: notifier.emailChanged, 188 | ), 189 | ); 190 | } 191 | 192 | Widget _buildPasswordField(TextEditingController controller, AuthStateController notifier) { 193 | return SlideInWidget( 194 | delay: 300, 195 | child: AnimatedFormField( 196 | label: "Password", 197 | hint: "Create a strong password", 198 | obscureText: true, 199 | prefixIcon: Icons.lock_outline, 200 | controller: controller, 201 | validator: _validatePassword, 202 | onChanged: notifier.passwordChanged, 203 | ), 204 | ); 205 | } 206 | 207 | Widget _buildConfirmPasswordField(TextEditingController controller, TextEditingController passwordController) { 208 | return SlideInWidget( 209 | delay: 350, 210 | child: AnimatedFormField( 211 | label: "Confirm Password", 212 | hint: "Confirm your password", 213 | obscureText: true, 214 | prefixIcon: Icons.lock_outline, 215 | controller: controller, 216 | validator: (value) => _validateConfirmPassword(value, passwordController.text), 217 | ), 218 | ); 219 | } 220 | 221 | Widget _buildSignUpButton( 222 | BuildContext context, 223 | GlobalKey formKey, 224 | AuthStates formStates, 225 | AuthStateController formNotifier, 226 | ) { 227 | return SlideInWidget( 228 | delay: 400, 229 | child: AnimatedButton( 230 | text: "Create Account", 231 | icon: Icons.arrow_forward_rounded, 232 | isLoading: formStates.isSubmitting, 233 | onPressed: formStates.isSubmitting ? null : () => _handleSignUp(formKey, formNotifier), 234 | ), 235 | ); 236 | } 237 | 238 | Widget _buildSignInPrompt(BuildContext context) { 239 | return SlideInWidget( 240 | delay: 500, 241 | child: Row( 242 | mainAxisAlignment: MainAxisAlignment.center, 243 | children: [ 244 | Text( 245 | "Already have an account? ", 246 | style: Theme.of(context).textTheme.bodyMedium, 247 | ), 248 | TextButton( 249 | onPressed: () => Navigator.pop(context), 250 | child: Text( 251 | "Sign In", 252 | style: TextStyle( 253 | color: Theme.of(context).colorScheme.primary, 254 | fontWeight: FontWeight.w600, 255 | ), 256 | ), 257 | ), 258 | ], 259 | ), 260 | ); 261 | } 262 | 263 | String? _validateEmail(String? value) { 264 | if (value == null || value.trim().isEmpty) { 265 | return "Please enter your email"; 266 | } 267 | if (!value.contains("@")) { 268 | return "Please enter a valid email"; 269 | } 270 | return null; 271 | } 272 | 273 | String? _validatePassword(String? value) { 274 | if (value == null || value.isEmpty) { 275 | return "Please enter your password"; 276 | } 277 | if (value.length < 8) { 278 | return "Password must be at least 8 characters"; 279 | } 280 | if (!RegExp(r"[A-Z]").hasMatch(value)) { 281 | return "Password must contain at least one uppercase letter"; 282 | } 283 | if (!RegExp(r"[0-9]").hasMatch(value)) { 284 | return "Password must contain at least one number"; 285 | } 286 | if (!RegExp(r'[!@#$%^&*(),.?":{}|<>]').hasMatch(value)) { 287 | return "Password must contain at least one special character"; 288 | } 289 | return null; 290 | } 291 | 292 | String? _validateConfirmPassword(String? value, String password) { 293 | if (value == null || value.isEmpty) { 294 | return "Please confirm your password"; 295 | } 296 | if (value != password) { 297 | return "Passwords do not match"; 298 | } 299 | return null; 300 | } 301 | 302 | void _handleSignUp(GlobalKey formKey, AuthStateController notifier) { 303 | if (formKey.currentState?.validate() ?? false) { 304 | HapticFeedback.lightImpact(); 305 | notifier.signUpWithEmailAndPassword(); 306 | } else { 307 | HapticFeedback.lightImpact(); 308 | } 309 | } 310 | } 311 | -------------------------------------------------------------------------------- /lib/screens/utils/custom_snackbar.dart: -------------------------------------------------------------------------------- 1 | import "package:flutter/material.dart"; 2 | 3 | ScaffoldFeatureController buildCustomSnackBar({ 4 | required BuildContext context, 5 | required Color flashBackground, 6 | required Text content, 7 | required IconData icon, 8 | }) { 9 | return ScaffoldMessenger.of(context).showSnackBar(SnackBar( 10 | backgroundColor: flashBackground, 11 | padding: const EdgeInsets.all(5), 12 | content: Padding( 13 | padding: const EdgeInsets.only(left: 20), 14 | child: Row( 15 | children: [ 16 | Icon( 17 | icon, 18 | color: Colors.white, 19 | ), 20 | const SizedBox( 21 | width: 20, 22 | ), 23 | content 24 | ], 25 | ), 26 | ))); 27 | } 28 | -------------------------------------------------------------------------------- /lib/services/authentication/firebase_auth_facade.dart: -------------------------------------------------------------------------------- 1 | import "package:firebase_auth/firebase_auth.dart"; 2 | import "package:fpdart/fpdart.dart"; 3 | import "package:hooks_riverpod/hooks_riverpod.dart"; 4 | 5 | import "../../domain/authentication/auth_failures.dart"; 6 | import "../../domain/authentication/auth_value_objects.dart"; 7 | import "../../domain/authentication/i_auth_facade.dart"; 8 | import "../../domain/core/errors.dart"; 9 | 10 | // Optimized providers with better separation of concerns 11 | final firebaseAuthProvider = Provider((ref) => FirebaseAuth.instance); 12 | 13 | final firebaseAuthFacadeProvider = Provider((ref) { 14 | return FirebaseAuthFacade(ref.read(firebaseAuthProvider)); 15 | }); 16 | 17 | class FirebaseAuthFacade implements IAuthFacade { 18 | const FirebaseAuthFacade(this._firebaseAuth); 19 | 20 | final FirebaseAuth _firebaseAuth; 21 | 22 | @override 23 | Future> registerWithEmailAndPassword({ 24 | required EmailAddress emailAddress, 25 | required Password password, 26 | }) async { 27 | return _executeAuthOperation( 28 | () => _firebaseAuth.createUserWithEmailAndPassword( 29 | email: _extractEmailValue(emailAddress), 30 | password: _extractPasswordValue(password), 31 | ), 32 | onEmailAlreadyInUse: () => const AuthFailures.emailAlreadyInUse(), 33 | ); 34 | } 35 | 36 | @override 37 | Future> signInWithEmailAndPassword({ 38 | required EmailAddress emailAddress, 39 | required Password password, 40 | }) async { 41 | return _executeAuthOperation( 42 | () => _firebaseAuth.signInWithEmailAndPassword( 43 | email: _extractEmailValue(emailAddress), 44 | password: _extractPasswordValue(password), 45 | ), 46 | onInvalidCredentials: () => const AuthFailures.invalidEmailAndPasswordCombination(), 47 | ); 48 | } 49 | 50 | @override 51 | Future> getSignedInUser() async { 52 | return optionOf(_firebaseAuth.currentUser?.uid); 53 | } 54 | 55 | @override 56 | Future signOut() async { 57 | await _firebaseAuth.signOut(); 58 | } 59 | 60 | // Helper method to extract email value safely 61 | String _extractEmailValue(EmailAddress emailAddress) { 62 | return emailAddress.valueObject?.fold( 63 | (failure) => throw UnExpectedValueError(failure), 64 | (email) => email, 65 | ) ?? 66 | ""; 67 | } 68 | 69 | // Helper method to extract password value safely 70 | String _extractPasswordValue(Password password) { 71 | return password.valueObject?.fold( 72 | (failure) => throw UnExpectedValueError(failure), 73 | (pwd) => pwd, 74 | ) ?? 75 | ""; 76 | } 77 | 78 | // Generic method to handle Firebase Auth operations with proper error mapping 79 | Future> _executeAuthOperation( 80 | Future Function() operation, { 81 | AuthFailures Function()? onEmailAlreadyInUse, 82 | AuthFailures Function()? onInvalidCredentials, 83 | }) async { 84 | try { 85 | await operation(); 86 | return right(unit); 87 | } on FirebaseAuthException catch (e) { 88 | return left(_mapFirebaseError(e, onEmailAlreadyInUse, onInvalidCredentials)); 89 | } catch (e) { 90 | return left(const AuthFailures.serverError()); 91 | } 92 | } 93 | 94 | // Centralized Firebase error mapping 95 | AuthFailures _mapFirebaseError( 96 | FirebaseAuthException exception, 97 | AuthFailures Function()? onEmailAlreadyInUse, 98 | AuthFailures Function()? onInvalidCredentials, 99 | ) { 100 | switch (exception.code) { 101 | case "email-already-in-use": 102 | return onEmailAlreadyInUse?.call() ?? const AuthFailures.serverError(); 103 | case "wrong-password": 104 | case "user-not-found": 105 | case "invalid-email": 106 | case "user-disabled": 107 | case "invalid-credential": 108 | return onInvalidCredentials?.call() ?? const AuthFailures.serverError(); 109 | case "weak-password": 110 | case "operation-not-allowed": 111 | case "too-many-requests": 112 | case "network-request-failed": 113 | default: 114 | return const AuthFailures.serverError(); 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /opencode.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://opencode.ai/config.json", 3 | "mcp": { 4 | "task-master-ai": { 5 | "type": "local", 6 | "command": [ 7 | "npx", 8 | "-y", 9 | "--package=task-master-ai", 10 | "task-master-ai" 11 | ], 12 | "enabled": true, 13 | "environment": { 14 | "ANTHROPIC_API_KEY": "YOUR_ANTHROPIC_API_KEY_HERE", 15 | "PERPLEXITY_API_KEY": "YOUR_PERPLEXITY_API_KEY_HERE", 16 | "OPENAI_API_KEY": "YOUR_OPENAI_KEY_HERE", 17 | "GOOGLE_API_KEY": "YOUR_GOOGLE_KEY_HERE", 18 | "XAI_API_KEY": "YOUR_XAI_KEY_HERE", 19 | "OPENROUTER_API_KEY": "YOUR_OPENROUTER_KEY_HERE", 20 | "MISTRAL_API_KEY": "YOUR_MISTRAL_KEY_HERE", 21 | "AZURE_OPENAI_API_KEY": "YOUR_AZURE_KEY_HERE", 22 | "OLLAMA_API_KEY": "YOUR_OLLAMA_API_KEY_HERE" 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: firebase_auth_flutter_ddd 2 | description: Firebase authentication example with Hooks Riverpod and Freezed following Flutter DDD architecture 3 | 4 | 5 | 6 | publish_to: 'none' 7 | 8 | version: 1.0.0+1 9 | 10 | environment: 11 | sdk: '>=3.2.3 <4.0.0' 12 | 13 | dependencies: 14 | cupertino_icons: ^1.0.8 15 | firebase_auth: ^5.7.0 16 | firebase_core: ^3.15.2 17 | flutter: 18 | sdk: flutter 19 | flutter_hooks: ^0.21.2 20 | fpdart: ^1.1.1 21 | freezed_annotation: ^3.1.0 22 | hooks_riverpod: ^3.0.0-dev.16 23 | 24 | dev_dependencies: 25 | build_runner: ^2.6.0 26 | flutter_lints: ^6.0.0 27 | flutter_test: 28 | sdk: flutter 29 | freezed: ^3.2.0 30 | 31 | flutter: 32 | uses-material-design: true 33 | -------------------------------------------------------------------------------- /scripts/delete: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Delete all local branches that are merged / deleted branches in remote 4 | 5 | # Fetch and prune remote branches 6 | git fetch --prune 7 | 8 | # Get the default branch name (either main or master) 9 | default_branch=$(git symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null | cut -d '/' -f 2) 10 | 11 | # If default_branch is not found, default to main 12 | if [ -z "$default_branch" ]; then 13 | default_branch="main" 14 | fi 15 | 16 | # Delete local branches that are merged into the default branch 17 | git branch --merged origin/"$default_branch" | grep -v "^\*" | grep -v "^$default_branch$" | xargs -r git branch -d 18 | 19 | # Delete local branches that track deleted remote branches 20 | git branch -vv | grep ': gone]' | awk '{print $1}' | grep -v "^$default_branch$" | xargs -r git branch -D 21 | 22 | -------------------------------------------------------------------------------- /web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pythonhubdev/flutter_ddd_firebase_auth/10b311606657916f4397ad1618aae9812a67f24d/web/favicon.png -------------------------------------------------------------------------------- /web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pythonhubdev/flutter_ddd_firebase_auth/10b311606657916f4397ad1618aae9812a67f24d/web/icons/Icon-192.png -------------------------------------------------------------------------------- /web/icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pythonhubdev/flutter_ddd_firebase_auth/10b311606657916f4397ad1618aae9812a67f24d/web/icons/Icon-512.png -------------------------------------------------------------------------------- /web/icons/Icon-maskable-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pythonhubdev/flutter_ddd_firebase_auth/10b311606657916f4397ad1618aae9812a67f24d/web/icons/Icon-maskable-192.png -------------------------------------------------------------------------------- /web/icons/Icon-maskable-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pythonhubdev/flutter_ddd_firebase_auth/10b311606657916f4397ad1618aae9812a67f24d/web/icons/Icon-maskable-512.png -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | firebase_auth_flutter_ddd 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "firebase_auth_flutter_ddd", 3 | "short_name": "firebase_auth_flutter_ddd", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "background_color": "#0175C2", 7 | "theme_color": "#0175C2", 8 | "description": "A new Flutter project.", 9 | "orientation": "portrait-primary", 10 | "prefer_related_applications": false, 11 | "icons": [ 12 | { 13 | "src": "icons/Icon-192.png", 14 | "sizes": "192x192", 15 | "type": "image/png" 16 | }, 17 | { 18 | "src": "icons/Icon-512.png", 19 | "sizes": "512x512", 20 | "type": "image/png" 21 | }, 22 | { 23 | "src": "icons/Icon-maskable-192.png", 24 | "sizes": "192x192", 25 | "type": "image/png", 26 | "purpose": "maskable" 27 | }, 28 | { 29 | "src": "icons/Icon-maskable-512.png", 30 | "sizes": "512x512", 31 | "type": "image/png", 32 | "purpose": "maskable" 33 | } 34 | ] 35 | } 36 | --------------------------------------------------------------------------------