diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 00000000..3058dc9d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,146 @@ +name: ๐Ÿ› Bug Report +description: File a bug report to help us improve +title: "[Bug]: " +labels: ["bug", "triage"] +assignees: + - ZanderCowboy + +body: + - type: markdown + attributes: + value: | + ## ๐Ÿ› Bug Report + + Thank you for taking the time to report a bug! Please fill out this form as completely as possible. + + > **Tip**: Before submitting, check if a similar bug report already exists. + + - type: input + id: summary + attributes: + label: ๐Ÿ“‹ Bug Summary + description: A clear and concise description of what the bug is + placeholder: "Example: App crashes when trying to add a new entry with special characters" + validations: + required: true + + - type: textarea + id: reproduction-steps + attributes: + label: ๐Ÿ”„ Steps to Reproduce + description: Step-by-step instructions to reproduce the bug + placeholder: | + 1. Open the app + 2. Navigate to '...' + 3. Click on '...' + 4. See error + validations: + required: true + + - type: textarea + id: expected-behavior + attributes: + label: โœ… Expected Behavior + description: What you expected to happen + placeholder: "The entry should be added successfully without any crashes." + validations: + required: true + + - type: textarea + id: actual-behavior + attributes: + label: โŒ Actual Behavior + description: What actually happened instead + placeholder: "The app crashes and returns to the home screen." + validations: + required: true + + - type: dropdown + id: platform + attributes: + label: ๐Ÿ“ฑ Platform + description: Which platform are you using? + options: + - Android + - iOS + - Web + - Windows + - macOS + - Linux + multiple: true + validations: + required: true + + - type: input + id: app-version + attributes: + label: ๐Ÿ“ฆ App Version + description: What version of the app are you running? + placeholder: "Example: 1.2.3+45" + validations: + required: true + + - type: dropdown + id: severity + attributes: + label: ๐Ÿšจ Severity Level + description: How severe is this bug? + options: + - "๐Ÿ”ฅ Critical - App unusable/crashes" + - "๐Ÿšจ High - Major feature broken" + - "โš ๏ธ Medium - Feature partially works" + - "๐Ÿ”ง Low - Minor issue/cosmetic" + validations: + required: true + + - type: textarea + id: device-info + attributes: + label: ๐Ÿ“ฑ Device Information + description: Information about your device + placeholder: | + - Device: (e.g., Pixel 7, iPhone 14) + - OS Version: (e.g., Android 13, iOS 16.1) + - Flutter Version: (if applicable) + render: markdown + validations: + required: false + + - type: textarea + id: logs + attributes: + label: ๐Ÿ“„ Error Logs/Screenshots + description: Any relevant logs, error messages, or screenshots + placeholder: | + ``` + Paste error logs here + ``` + + Or drag and drop screenshots here + render: shell + validations: + required: false + + - type: textarea + id: additional-context + attributes: + label: ๐Ÿ“ Additional Context + description: Any other context about the problem + placeholder: "This bug started happening after updating to version X.X.X" + validations: + required: false + + - type: checkboxes + id: checklist + attributes: + label: โœ… Pre-submission Checklist + description: Please verify the following before submitting + options: + - label: I have searched for existing issues + required: true + - label: I have provided steps to reproduce + required: true + - label: I have included relevant device/version information + required: true + - label: I am willing to help test the fix + required: false \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..32f7d8eb --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,14 @@ +blank_issues_enabled: true +contact_links: + - name: ๐Ÿ’ฌ Discussions + url: https://github.com/ZanderCowboy/multichoice/discussions + about: Ask questions, share ideas, and discuss the project with the community + - name: ๐Ÿ”’ Security Issues + url: https://github.com/ZanderCowboy/multichoice/security/advisories/new + about: Report security vulnerabilities privately + - name: ๐Ÿ“ฑ Google Play Store + url: https://play.google.com/store/apps/details?id=co.za.zandercowboy.multichoice + about: Rate and review the app on Google Play Store + - name: ๐Ÿ“Š Project Roadmap + url: https://github.com/ZanderCowboy/multichoice/projects + about: View our development roadmap and project milestones \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/documentation.yml b/.github/ISSUE_TEMPLATE/documentation.yml new file mode 100644 index 00000000..7e712fb4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/documentation.yml @@ -0,0 +1,182 @@ +name: ๐Ÿ“š Documentation +description: Improve or request documentation +title: "[Docs]: " +labels: ["documentation", "good first issue"] +assignees: + - ZanderCowboy + +body: + - type: markdown + attributes: + value: | + ## ๐Ÿ“š Documentation Request + + Help us improve our documentation! Whether it's fixing typos, adding examples, or creating new guides. + + > **Perfect for first-time contributors!** + + - type: input + id: doc-title + attributes: + label: ๐Ÿ“ Documentation Title + description: What documentation needs attention? + placeholder: "Example: Add setup instructions for development environment" + validations: + required: true + + - type: dropdown + id: doc-type + attributes: + label: ๐Ÿ“‚ Documentation Type + description: What type of documentation is this? + options: + - "๐Ÿ“– README Updates" + - "๐Ÿš€ Getting Started Guide" + - "๐Ÿ’ป API Documentation" + - "๐Ÿ—๏ธ Architecture Documentation" + - "๐Ÿงช Testing Guide" + - "๐Ÿ”ง Configuration Guide" + - "๐Ÿ“ฑ User Guide" + - "๐ŸŽฏ Contributing Guide" + - "โ“ FAQ" + - "๐Ÿ› Troubleshooting" + - "๐Ÿ“ Code Comments" + - "โœจ Other" + validations: + required: true + + - type: textarea + id: current-state + attributes: + label: ๐Ÿ“„ Current State + description: What's wrong with the current documentation (if any)? + placeholder: | + - Missing information about... + - Outdated instructions for... + - Confusing explanation of... + - No documentation exists for... + validations: + required: true + + - type: textarea + id: proposed-improvement + attributes: + label: โœจ Proposed Improvement + description: What should be added, changed, or improved? + placeholder: | + - Add section about... + - Update instructions to include... + - Clarify the explanation of... + - Create examples for... + validations: + required: true + + - type: dropdown + id: target-audience + attributes: + label: ๐Ÿ‘ฅ Target Audience + description: Who is this documentation for? + options: + - "๐ŸŒฑ New contributors" + - "๐Ÿ’ป Developers" + - "๐Ÿ‘ค End users" + - "๐Ÿ”ง Maintainers" + - "๐Ÿข Enterprise users" + - "๐Ÿ“š Everyone" + validations: + required: true + + - type: textarea + id: affected-files + attributes: + label: ๐Ÿ“ Affected Files/Sections + description: Which files or sections need updates? + placeholder: | + - README.md (section: Getting Started) + - docs/contributing.md + - lib/src/services/README.md + - Code comments in auth_service.dart + validations: + required: false + + - type: textarea + id: content-outline + attributes: + label: ๐Ÿ“‹ Content Outline + description: Provide an outline or draft of the content (if applicable) + placeholder: | + ## Section Title + 1. Introduction + 2. Prerequisites + 3. Step-by-step instructions + 4. Examples + 5. Troubleshooting + validations: + required: false + + - type: textarea + id: examples-needed + attributes: + label: ๐Ÿ’ก Examples Needed + description: What kind of examples would be helpful? + placeholder: | + - Code snippets showing how to... + - Screenshot of the settings page + - Example configuration file + - Sample API response + validations: + required: false + + - type: dropdown + id: priority + attributes: + label: ๐Ÿ“ˆ Priority + description: How important is this documentation update? + options: + - "๐Ÿ”ฅ High - Critical for project usability" + - "โšก Medium - Would significantly help users" + - "๐Ÿ’ก Low - Nice to have improvement" + validations: + required: true + + - type: textarea + id: references + attributes: + label: ๐Ÿ“– References + description: Any relevant links, examples from other projects, or resources + placeholder: | + - Similar documentation: https://example.com/docs + - Related issue: #123 + - Inspiration from: flutter.dev/docs + validations: + required: false + + - type: checkboxes + id: volunteer + attributes: + label: ๐Ÿ™‹โ€โ™€๏ธ Contribution Offer + description: Are you willing to help with this documentation? + options: + - label: I can write this documentation + required: false + - label: I can review the documentation once written + required: false + - label: I can provide examples or screenshots + required: false + - label: I can test the instructions for accuracy + required: false + - label: I need help writing this but can collaborate + required: false + + - type: checkboxes + id: checklist + attributes: + label: โœ… Pre-submission Checklist + description: Please verify the following before submitting + options: + - label: I have searched for existing documentation issues + required: true + - label: I have clearly identified what needs to be documented + required: true + - label: I have specified who this documentation is for + required: true \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 00000000..d4601bf6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,166 @@ +name: โœจ Feature Request +description: Suggest a new feature or enhancement +title: "[Feature]: " +labels: ["enhancement", "feature-request"] +assignees: + - ZanderCowboy + +body: + - type: markdown + attributes: + value: | + ## โœจ Feature Request + + We love hearing about new ideas! Please describe your feature request in detail. + + > **Tip**: Before submitting, check if a similar feature request already exists. + + - type: input + id: feature-title + attributes: + label: ๐ŸŽฏ Feature Title + description: A clear and concise title for your feature request + placeholder: "Example: Add dark mode toggle to settings" + validations: + required: true + + - type: textarea + id: problem-statement + attributes: + label: ๐Ÿค” Problem Statement + description: What problem does this feature solve? + placeholder: "I'm always frustrated when... / Users need a way to..." + validations: + required: true + + - type: textarea + id: proposed-solution + attributes: + label: ๐Ÿ’ก Proposed Solution + description: Describe your ideal solution + placeholder: "I would like to see... / The app should..." + validations: + required: true + + - type: dropdown + id: feature-type + attributes: + label: ๐Ÿท๏ธ Feature Type + description: What type of feature is this? + options: + - "๐ŸŽจ UI/UX Enhancement" + - "โšก Performance Improvement" + - "๐Ÿ”ง New Functionality" + - "๐Ÿ”Œ Integration/API" + - "๐Ÿ“ฑ Platform-specific" + - "๐ŸŒ Accessibility" + - "๐Ÿ”’ Security Enhancement" + - "๐Ÿ“Š Analytics/Tracking" + validations: + required: true + + - type: dropdown + id: priority + attributes: + label: ๐Ÿ“ˆ Priority Level + description: How important is this feature to you? + options: + - "๐Ÿ”ฅ High - Would significantly improve the app" + - "โšก Medium - Nice to have improvement" + - "๐Ÿ’ก Low - Minor enhancement" + validations: + required: true + + - type: textarea + id: user-stories + attributes: + label: ๐Ÿ‘ฅ User Stories + description: Who would benefit from this feature and how? + placeholder: | + - As a [user type], I want [goal] so that [benefit] + - Example: As a student, I want to organize my notes by subject so that I can study more efficiently + validations: + required: true + + - type: textarea + id: acceptance-criteria + attributes: + label: โœ… Acceptance Criteria + description: What conditions must be met for this feature to be considered complete? + placeholder: | + - [ ] User can toggle dark mode in settings + - [ ] Dark mode persists across app restarts + - [ ] All screens support dark mode + - [ ] Follows system dark mode preference + validations: + required: false + + - type: textarea + id: mockups-designs + attributes: + label: ๐ŸŽจ Mockups/Designs + description: Any visual mockups, wireframes, or design ideas + placeholder: "Drag and drop images here or provide links to designs" + validations: + required: false + + - type: textarea + id: technical-considerations + attributes: + label: ๐Ÿ”ง Technical Considerations + description: Any technical requirements or constraints to consider + placeholder: | + - Should work on Android and iOS + - Needs to integrate with existing theme system + - May require new dependencies + validations: + required: false + + - type: textarea + id: alternatives + attributes: + label: ๐Ÿ”„ Alternative Solutions + description: Other solutions you've considered + placeholder: "I also considered... / Another approach could be..." + validations: + required: false + + - type: dropdown + id: complexity + attributes: + label: โš–๏ธ Estimated Complexity + description: How complex do you think this feature might be? + options: + - "๐ŸŸข Simple - Minor changes needed" + - "๐ŸŸก Medium - Moderate development effort" + - "๐ŸŸ  Complex - Significant changes required" + - "๐Ÿ”ด Very Complex - Major architectural changes" + - "โ“ Unknown - Need technical assessment" + validations: + required: false + + - type: textarea + id: additional-context + attributes: + label: ๐Ÿ“ Additional Context + description: Any other information that might be helpful + placeholder: "References to similar features in other apps, relevant links, etc." + validations: + required: false + + - type: checkboxes + id: checklist + attributes: + label: โœ… Pre-submission Checklist + description: Please verify the following before submitting + options: + - label: I have searched for existing feature requests + required: true + - label: I have clearly described the problem this feature solves + required: true + - label: I have provided a detailed proposed solution + required: true + - label: I am willing to help test this feature when implemented + required: false + - label: I am interested in contributing to this feature's development + required: false \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/maintenance.yml b/.github/ISSUE_TEMPLATE/maintenance.yml new file mode 100644 index 00000000..62949e24 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/maintenance.yml @@ -0,0 +1,220 @@ +name: ๐Ÿ”ง Maintenance +description: Technical debt, refactoring, or dependency updates +title: "[Maintenance]: " +labels: ["maintenance", "technical-debt"] +assignees: + - ZanderCowboy + +body: + - type: markdown + attributes: + value: | + ## ๐Ÿ”ง Maintenance Task + + Help keep our codebase healthy! This template is for refactoring, dependency updates, performance improvements, and addressing technical debt. + + - type: input + id: maintenance-title + attributes: + label: ๐ŸŽฏ Maintenance Task Title + description: What needs to be maintained or improved? + placeholder: "Example: Update Flutter dependencies to latest versions" + validations: + required: true + + - type: dropdown + id: maintenance-type + attributes: + label: ๐Ÿท๏ธ Maintenance Type + description: What type of maintenance is this? + options: + - "๐Ÿ“ฆ Dependency Updates" + - "โ™ป๏ธ Code Refactoring" + - "๐Ÿ—‘๏ธ Technical Debt Cleanup" + - "โšก Performance Optimization" + - "๐Ÿ”’ Security Updates" + - "๐Ÿงช Test Infrastructure" + - "๐Ÿ—๏ธ Build System Improvements" + - "๐Ÿ“ File/Folder Restructuring" + - "๐Ÿ”ง Configuration Updates" + - "๐Ÿ“Š Monitoring/Analytics Setup" + - "โœจ Code Quality Improvements" + validations: + required: true + + - type: textarea + id: current-situation + attributes: + label: ๐Ÿ“Š Current Situation + description: What is the current state that needs improvement? + placeholder: | + - Dependencies are outdated (Flutter 3.0.x โ†’ 3.13.x) + - Code duplication in auth modules + - Tests are flaky and need refactoring + - Build times are too slow + validations: + required: true + + - type: textarea + id: proposed-changes + attributes: + label: ๐Ÿ› ๏ธ Proposed Changes + description: What specific changes do you propose? + placeholder: | + - Update pubspec.yaml dependencies + - Extract common auth logic to shared service + - Refactor test setup/teardown methods + - Optimize build configuration + validations: + required: true + + - type: dropdown + id: urgency + attributes: + label: โšก Urgency Level + description: How urgent is this maintenance? + options: + - "๐Ÿ”ฅ Critical - Security/stability issue" + - "โš ๏ธ High - Blocking other development" + - "๐Ÿ“ˆ Medium - Should be done soon" + - "๐Ÿ“… Low - Can be scheduled later" + validations: + required: true + + - type: textarea + id: affected-components + attributes: + label: ๐ŸŽฏ Affected Components + description: What parts of the codebase will be affected? + placeholder: | + - packages/core/ + - Authentication services + - CI/CD workflows + - All Flutter packages + validations: + required: true + + - type: textarea + id: benefits + attributes: + label: โœจ Expected Benefits + description: What improvements will this maintenance provide? + placeholder: | + - Improved performance + - Better code maintainability + - Enhanced security + - Faster build times + - Reduced technical debt + validations: + required: true + + - type: textarea + id: risks + attributes: + label: โš ๏ธ Potential Risks + description: What could go wrong? What are the risks? + placeholder: | + - Breaking changes in new dependencies + - Temporary build instability + - Need to update documentation + - May require extensive testing + validations: + required: false + + - type: textarea + id: testing-strategy + attributes: + label: ๐Ÿงช Testing Strategy + description: How will you ensure the changes don't break anything? + placeholder: | + - Run full test suite + - Manual testing on key flows + - Integration test verification + - Performance benchmarks + validations: + required: false + + - type: textarea + id: rollback-plan + attributes: + label: ๐Ÿ”„ Rollback Plan + description: How can we revert if something goes wrong? + placeholder: | + - Git revert to previous commit + - Keep backup of old dependency versions + - Document rollback steps + validations: + required: false + + - type: dropdown + id: complexity + attributes: + label: ๐Ÿ“Š Complexity Estimate + description: How complex is this maintenance task? + options: + - "๐ŸŸข Simple - Few files, low risk" + - "๐ŸŸก Medium - Multiple components affected" + - "๐ŸŸ  Complex - Significant changes required" + - "๐Ÿ”ด Very Complex - Major architectural impact" + validations: + required: true + + - type: textarea + id: prerequisites + attributes: + label: ๐Ÿ“‹ Prerequisites + description: Any prerequisites or dependencies for this task? + placeholder: | + - Complete issue #123 first + - Need approval from maintainers + - Coordinate with ongoing feature development + - Backup production data + validations: + required: false + + - type: textarea + id: documentation-updates + attributes: + label: ๐Ÿ“š Documentation Updates Needed + description: What documentation will need to be updated? + placeholder: | + - README.md (dependency versions) + - Contributing guide (new setup steps) + - Architecture documentation + - Deployment guide + validations: + required: false + + - type: checkboxes + id: maintenance-checklist + attributes: + label: โœ… Maintenance Checklist + description: Standard items to consider for maintenance tasks + options: + - label: Breaking changes are documented + required: false + - label: Migration guide is provided (if needed) + required: false + - label: All tests pass after changes + required: false + - label: CI/CD pipelines are updated + required: false + - label: Dependencies are audited for security + required: false + - label: Performance impact is measured + required: false + + - type: checkboxes + id: checklist + attributes: + label: โœ… Pre-submission Checklist + description: Please verify the following before submitting + options: + - label: I have clearly described the current problem + required: true + - label: I have outlined the proposed solution + required: true + - label: I have considered the risks and testing strategy + required: true + - label: I am available to implement or help with this maintenance + required: false \ No newline at end of file diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..e2bbcad0 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,165 @@ +# ๐Ÿš€ Pull Request + +## ๐ŸŽซ Related Issue + +Fixes #(issue number) + +## ๐Ÿ“ Description + + +### ๐ŸŽฏ Type of Change + +- [ ] ๐Ÿ› Bug fix (non-breaking change which fixes an issue) +- [ ] โœจ New feature (non-breaking change which adds functionality) +- [ ] ๐Ÿ’ฅ Breaking change (fix or feature that would cause existing functionality to not work as expected) +- [ ] ๐Ÿ“š Documentation update +- [ ] ๐Ÿ”ง Maintenance (refactoring, dependency updates, etc.) +- [ ] ๐Ÿงช Test improvements +- [ ] โšก Performance improvements + +### ๐Ÿท๏ธ Version Bump + +- [ ] `major` - Breaking changes (1.0.0 โ†’ 2.0.0) +- [ ] `minor` - New features (1.0.0 โ†’ 1.1.0) +- [ ] `patch` - Bug fixes (1.0.0 โ†’ 1.0.1) +- [ ] `no-build` - Documentation/non-code changes + +## ๐Ÿ”„ Changes Made + +- [ ] Added/Modified component X +- [ ] Updated service Y +- [ ] Fixed bug in feature Z +- [ ] Added tests for... +- [ ] Updated documentation for... + +## ๐Ÿงช Testing + + +### โœ… Test Types Run +- [ ] Unit tests +- [ ] Widget tests +- [ ] Integration tests +- [ ] Manual testing + +### ๐Ÿ“ฑ Platforms Tested +- [ ] Android +- [ ] iOS +- [ ] Web +- [ ] Windows +- [ ] macOS +- [ ] Linux + +### ๐Ÿ”ง Test Results + +```bash +# Paste test output here +``` + +## ๐Ÿ“ธ Screenshots/Videos + + +### Before + + +### After + + +## ๐Ÿ—๏ธ Architecture Impact + +- [ ] No architectural changes +- [ ] Minor refactoring +- [ ] New service/repository added +- [ ] Breaking changes to existing APIs +- [ ] Database schema changes +- [ ] New dependencies added + +## ๐Ÿ“‹ Checklist + + +### ๐Ÿš€ Development +- [ ] Code follows the project's style guidelines +- [ ] Self-review of code has been performed +- [ ] Code is properly documented +- [ ] No unused imports or dead code +- [ ] All console warnings/errors are resolved + +### ๐Ÿงช Testing +- [ ] Tests have been added/updated for new functionality +- [ ] All tests pass locally +- [ ] Manual testing completed +- [ ] No regression in existing functionality + +### ๐Ÿ“š Documentation +- [ ] Updated relevant documentation +- [ ] Added/updated code comments where necessary +- [ ] Updated README if needed +- [ ] Created/updated issue documentation in `/docs/` + +### ๐Ÿ”ง Build & Deploy +- [ ] PR builds successfully in CI +- [ ] No linting errors +- [ ] No analysis issues +- [ ] Dependencies are properly added to pubspec.yaml + +## โš ๏ธ Breaking Changes + + + +## ๐Ÿ”— Dependencies + +- **New Dependencies**: + - `package_name: ^version` - Purpose/reason +- **Updated Dependencies**: + - `package_name: ^old_version โ†’ ^new_version` - Reason for update + +## ๐Ÿ“‹ Migration Guide + + + +## ๐ŸŽฏ Post-merge Tasks + +- [ ] Update related documentation +- [ ] Create follow-up issues +- [ ] Notify stakeholders +- [ ] Deploy to staging/production + +## ๐Ÿ” Review Notes + +- Please pay special attention to... +- I'm unsure about... +- Alternative approaches considered... + +## ๐Ÿ“ฆ Related PRs + +- Related to #(PR number) +- Depends on #(PR number) +- Blocks #(PR number) + +--- + +## ๐Ÿ“ For Reviewers + +### ๐ŸŽฏ Focus Areas +- [ ] Code quality and architecture +- [ ] Test coverage and quality +- [ ] Documentation completeness +- [ ] Performance implications +- [ ] Security considerations +- [ ] User experience impact + +### โœ… Review Checklist +- [ ] Code is readable and maintainable +- [ ] Tests are comprehensive and meaningful +- [ ] No obvious performance issues +- [ ] Security best practices followed +- [ ] Documentation is clear and helpful +- [ ] CI checks are passing + +--- + +## ๐ŸŽ‰ Final Notes + + +**Thank you for reviewing this PR!** ๐Ÿ™ + +> ๐Ÿ’ก **Tip**: Use GitHub's review tools to leave specific feedback on code lines. \ No newline at end of file diff --git a/.github/workflows/issue_automation.yml b/.github/workflows/issue_automation.yml new file mode 100644 index 00000000..52395fe0 --- /dev/null +++ b/.github/workflows/issue_automation.yml @@ -0,0 +1,279 @@ +name: ๐Ÿค– Issue & PR Automation + +on: + issues: + types: [opened, labeled] + pull_request: + types: [opened, labeled, ready_for_review] + +jobs: + auto-assign: + name: ๐ŸŽฏ Auto Assignment + runs-on: ubuntu-latest + if: github.event.action == 'opened' + + steps: + - name: ๐Ÿ‘‹ Welcome new contributors + if: github.event.issue && github.actor != 'ZanderCowboy' + uses: actions/github-script@v7 + with: + script: | + const welcomeMessage = ` + ## ๐ŸŽ‰ Welcome to Multichoice! + + Thanks for opening your first issue! Here's what happens next: + + - ๐Ÿ” **Triage**: We'll review and label your issue + - ๐Ÿ“‹ **Assignment**: If approved, we'll assign it to you or a maintainer + - ๐Ÿš€ **Development**: Once assigned, you can start working on it + + ### ๐Ÿ’ก Pro Tips: + - Check out our [Contributing Guide](../CONTRIBUTION.md) for development setup + - Join our [Discussions](https://github.com/ZanderCowboy/multichoice/discussions) for questions + - Look for \`good first issue\` labels if you're new to the project + + **Happy coding!** ๐Ÿš€ + `; + + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: welcomeMessage + }); + + - name: ๐Ÿท๏ธ Auto-label by type + uses: actions/github-script@v7 + with: + script: | + const issue = context.payload.issue || context.payload.pull_request; + const title = issue.title.toLowerCase(); + const body = issue.body || ''; + + const labels = []; + + // Auto-label based on title prefixes + if (title.includes('[bug]')) labels.push('bug'); + if (title.includes('[feature]')) labels.push('enhancement'); + if (title.includes('[docs]')) labels.push('documentation'); + if (title.includes('[maintenance]')) labels.push('maintenance'); + + // Auto-label based on content + if (body.includes('flutter') || body.includes('dart')) labels.push('flutter'); + if (body.includes('android')) labels.push('android'); + if (body.includes('ios')) labels.push('ios'); + if (body.includes('web')) labels.push('web'); + if (body.includes('test') || body.includes('testing')) labels.push('testing'); + + // Add priority labels for critical issues + if (title.includes('critical') || title.includes('urgent') || body.includes('๐Ÿ”ฅ Critical')) { + labels.push('priority: high'); + } + + // Add good first issue for documentation + if (labels.includes('documentation') && !issue.pull_request) { + labels.push('good first issue'); + } + + if (labels.length > 0) { + const issueNumber = issue.number; + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + labels: labels + }); + } + + auto-assign-maintainer: + name: ๐Ÿ‘จโ€๐Ÿ’ผ Assign Maintainer + runs-on: ubuntu-latest + if: | + (github.event.action == 'labeled' && + contains(github.event.label.name, 'triage')) || + (github.event.action == 'opened' && + (contains(github.event.issue.title, '[Bug]') || + contains(github.event.issue.title, '[Maintenance]'))) + + steps: + - name: ๐ŸŽฏ Assign to maintainer + uses: actions/github-script@v7 + with: + script: | + const issue = context.payload.issue; + + await github.rest.issues.addAssignees({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + assignees: ['ZanderCowboy'] + }); + + pr-automation: + name: ๐Ÿ”„ PR Automation + runs-on: ubuntu-latest + if: github.event.pull_request + + steps: + - name: ๐Ÿท๏ธ Auto-label PR by version bump + if: github.event.action == 'opened' + uses: actions/github-script@v7 + with: + script: | + const pr = context.payload.pull_request; + const body = pr.body || ''; + const labels = []; + + // Check for version bump indicators in PR body + if (body.includes('- [x] `major`') || body.includes('- [X] `major`')) { + labels.push('major'); + } else if (body.includes('- [x] `minor`') || body.includes('- [X] `minor`')) { + labels.push('minor'); + } else if (body.includes('- [x] `patch`') || body.includes('- [X] `patch`')) { + labels.push('patch'); + } else if (body.includes('- [x] `no-build`') || body.includes('- [X] `no-build`')) { + labels.push('no-build'); + } + + // Auto-detect change type + if (body.includes('- [x] ๐Ÿ› Bug fix') || body.includes('- [X] ๐Ÿ› Bug fix')) { + labels.push('bug fix'); + } + if (body.includes('- [x] โœจ New feature') || body.includes('- [X] โœจ New feature')) { + labels.push('enhancement'); + } + if (body.includes('- [x] ๐Ÿ“š Documentation') || body.includes('- [X] ๐Ÿ“š Documentation')) { + labels.push('documentation'); + } + + if (labels.length > 0) { + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + labels: labels + }); + } + + - name: ๐Ÿ“‹ Validate PR requirements + if: github.event.action == 'ready_for_review' + uses: actions/github-script@v7 + with: + script: | + const pr = context.payload.pull_request; + const body = pr.body || ''; + + const checks = { + 'Related Issue': /Fixes #\d+/i.test(body), + 'Change Type Selected': /- \[x\]/i.test(body), + 'Version Bump Selected': /`(major|minor|patch|no-build)`.*- \[x\]/i.test(body), + 'Testing Completed': /Test Types Run.*- \[x\]/i.test(body) + }; + + const failed = Object.entries(checks) + .filter(([_, passed]) => !passed) + .map(([check]) => check); + + if (failed.length > 0) { + const comment = ` + ## โš ๏ธ PR Template Requirements Missing + + Please complete the following sections in your PR description: + + ${failed.map(item => `- [ ] ${item}`).join('\n')} + + This helps us review your PR more efficiently! ๐Ÿš€ + `; + + await github.rest.issues.createComment({ + issue_number: pr.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: comment + }); + } + + celebrate: + name: ๐ŸŽ‰ Celebrate Contributions + runs-on: ubuntu-latest + if: | + (github.event.action == 'opened' && github.event.pull_request) || + (github.event.action == 'labeled' && github.event.label.name == 'good first issue') + + steps: + - name: ๐ŸŒŸ First-time contributor celebration + if: github.event.pull_request && github.actor != 'ZanderCowboy' + uses: actions/github-script@v7 + with: + script: | + // Check if this is the user's first contribution + const { data: pullRequests } = await github.rest.pulls.list({ + owner: context.repo.owner, + repo: context.repo.repo, + creator: context.actor, + state: 'all' + }); + + if (pullRequests.length === 1) { + const celebrationComment = ` + ## ๐ŸŽ‰ First Contribution! + + Wow! This is your first contribution to **Multichoice**! ๐ŸŽŠ + + Thank you for: + - ๐Ÿค Joining our community + - ๐Ÿ› ๏ธ Improving the project + - ๐ŸŒŸ Making Multichoice better for everyone + + ### What's Next? + - โœ… Your PR will be reviewed by maintainers + - ๐Ÿ”„ You might receive feedback for improvements + - ๐Ÿš€ Once approved, your code will be merged! + + ### Keep Contributing! + - ๐Ÿท๏ธ Look for more \`good first issue\` labels + - ๐Ÿ’ฌ Join our [Discussions](https://github.com/ZanderCowboy/multichoice/discussions) + - ๐Ÿ“– Check out our [roadmap](https://github.com/ZanderCowboy/multichoice/projects) + + **Welcome to the team!** ๐Ÿš€โœจ + `; + + await github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: celebrationComment + }); + } + + - name: ๐ŸŽฏ Good first issue guidance + if: github.event.action == 'labeled' && github.event.label.name == 'good first issue' + uses: actions/github-script@v7 + with: + script: | + const guidanceComment = ` + ## ๐ŸŒฑ Perfect for First-Time Contributors! + + This issue has been marked as a **good first issue** - great for newcomers! + + ### ๐Ÿš€ Getting Started: + 1. **Comment** to get assigned to this issue + 2. **Fork** the repository + 3. **Read** our [Contributing Guide](../CONTRIBUTION.md) + 4. **Set up** your development environment + 5. **Create** a branch: \`${context.payload.issue.number}-${context.payload.issue.title.toLowerCase().replace(/[^a-z0-9]/g, '-')}\` + + ### ๐Ÿ’ก Need Help? + - ๐Ÿ“š Check existing documentation in \`/docs/\` + - ๐Ÿ’ฌ Ask questions in [Discussions](https://github.com/ZanderCowboy/multichoice/discussions) + - ๐Ÿ” Look at similar completed issues for examples + + **We're here to support you!** ๐Ÿค + `; + + await github.rest.issues.createComment({ + issue_number: context.payload.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: guidanceComment + }); \ No newline at end of file diff --git a/CONTRIBUTION.md b/CONTRIBUTION.md new file mode 100644 index 00000000..f8e61bc6 --- /dev/null +++ b/CONTRIBUTION.md @@ -0,0 +1,332 @@ +# ๐Ÿš€ Contributing to Multichoice + +> **Making contribution cool and straightforward!** ๐ŸŽฏ + +Welcome to the **Multichoice** project! We're excited to have you contribute to this awesome Flutter application. This guide will help you understand our workflow, standards, and how to make meaningful contributions. + +## ๐ŸŒŸ Quick Start + +1. **Fork & Clone** + ```bash + git clone https://github.com/ZanderCowboy/multichoice.git + cd multichoice + ``` + +2. **Setup Development Environment** + ```bash + # Install dependencies + melos get + + # Run the app + flutter run + ``` + +3. **Create a Feature Branch** + ```bash + git checkout -b {issue-number}-{feature-description} + # Example: git checkout -b 42-add-dark-mode-toggle + ``` + +## ๐Ÿ“‹ Project Structure & Architecture + +This project follows **Clean Architecture** principles with a monorepo structure: + +``` +multichoice/ +โ”œโ”€โ”€ apps/ +โ”‚ โ””โ”€โ”€ multichoice/ # Main Flutter app +โ”œโ”€โ”€ packages/ +โ”‚ โ”œโ”€โ”€ core/ # Core business logic +โ”‚ โ”œโ”€โ”€ models/ # Data models & DTOs +โ”‚ โ”œโ”€โ”€ theme/ # Theming system +โ”‚ โ””โ”€โ”€ ui_kit/ # Reusable UI components +โ””โ”€โ”€ docs/ # Documentation & tickets +``` + +### ๐Ÿ—๏ธ Architecture Layers +- **Presentation**: UI components, pages, and widgets +- **Application**: Business logic, use cases, and state management +- **Domain**: Core entities and repository interfaces +- **Data**: Repository implementations and data sources + +## ๐ŸŽฏ Contributing Workflow + +### 1. ๐Ÿ“ Issue Creation & Assignment + +- **Browse existing issues**: Check [Issues](https://github.com/ZanderCowboy/multichoice/issues) first +- **Create detailed issues**: Use our issue templates for consistency +- **Get assigned**: Comment on issues you'd like to work on +- **One issue at a time**: Focus on quality over quantity + +### 2. ๐ŸŒฟ Branch Strategy + +We follow **Git Flow** with these branches: + +``` +main # ๐Ÿญ Production releases +โ”œโ”€โ”€ rc # ๐Ÿงช Release candidates (staging) +โ”œโ”€โ”€ develop # ๐Ÿ”„ Integration branch +โ””โ”€โ”€ feature/ # ๐Ÿš€ Feature branches +``` + +**Branch Naming Convention:** +``` +{issue-number}-{kebab-case-description} +``` + +**Examples:** +- `27-setup-workflows` +- `119-implement-dark-mode` +- `42-add-integration-tests` + +### 3. ๐Ÿท๏ธ Version Management + +We use **Semantic Versioning** (MAJOR.MINOR.PATCH+BUILD) with PR labels: + +| Label | Version Bump | Usage | +|-------|--------------|-------| +| `major` | 1.0.0 โ†’ 2.0.0 | Breaking changes | +| `minor` | 1.0.0 โ†’ 1.1.0 | New features | +| `patch` | 1.0.0 โ†’ 1.0.1 | Bug fixes | +| `no-build` | No change | Documentation, etc. | + +### 4. ๐Ÿ”„ Pull Request Process + +1. **Create PR to `develop`** branch +2. **Add appropriate labels** (major/minor/patch) +3. **Fill out PR template** completely +4. **Ensure CI passes** (tests, linting, build) +5. **Request review** from maintainers +6. **Address feedback** promptly + +### 5. ๐Ÿš€ Release Flow + +``` +develop โ†’ rc (Release Candidate) โ†’ main (Production) +``` + +- **RC builds**: Automatic via staging workflow +- **Production**: Manual promotion from RC + +## ๐Ÿ“š Documentation Standards + +### Issue Documentation + +Every completed issue should have documentation in `/docs/` following this structure: + +```markdown +# [Feature Name](https://github.com/ZanderCowboy/multichoice/issues/{number}) + +## Ticket: [{number}](https://github.com/ZanderCowboy/multichoice/issues/{number}) + +### branch: `{branch-name}` + +### Overview +Brief description of what this ticket accomplishes. + +### What was done +- [X] Implemented feature A +- [X] Added tests for component B +- [X] Updated documentation + +### What needs to be done +- [ ] Follow-up task if any + +### Resources +- [Relevant link](https://example.com) +``` + +## ๐Ÿงช Testing Requirements + +### โœ… Required Tests + +- **Unit Tests**: For business logic and utilities +- **Widget Tests**: For UI components +- **Integration Tests**: For complete user flows + +### ๐Ÿƒโ€โ™‚๏ธ Running Tests + +```bash +# Unit tests +flutter test + +# Widget tests (specific package) +cd packages/core && flutter test + +# Integration tests +flutter drive --driver=test_driver/integration_test.dart --target=integration_test/app_test.dart + +# Coverage +flutter test --coverage +``` + +### ๐Ÿ“Š Coverage Requirements + +- **Minimum**: 80% code coverage +- **Core package**: 90% coverage required +- **UI components**: Visual testing preferred + +## ๐ŸŽจ Code Standards + +### ๐Ÿ”ง Linting & Formatting + +```bash +# Analyze code +flutter analyze + +# Format code +dart format . + +# Fix issues +dart fix --apply +``` + +### ๐Ÿ“ Code Style + +- **Follow Dart conventions**: Use `dart format` +- **Meaningful names**: Clear, descriptive variable/function names +- **Documentation**: Document public APIs with `///` +- **Comments**: Explain complex logic, not obvious code + +### ๐Ÿ—๏ธ Architecture Guidelines + +```dart +// โœ… Good: Clear separation of concerns +class UserRepository implements IUserRepository { + @override + Future getUser(String id) async { + // Implementation + } +} + +// โŒ Avoid: Business logic in widgets +class UserWidget extends StatelessWidget { + @override + Widget build(BuildContext context) { + // Don't put business logic here + } +} +``` + +## ๐Ÿ› ๏ธ Development Tools + +### ๐Ÿณ Dev Container + +We provide a complete development environment: + +```bash +# Using Dev Container +code . +# Select "Reopen in Container" when prompted +``` + +### ๐Ÿ”ง Recommended Extensions + +- **Flutter**: Official Flutter extension +- **Dart**: Dart language support +- **GitLens**: Git visualization +- **Error Lens**: Inline error display + +## ๐Ÿšฆ CI/CD Pipeline + +### ๐Ÿ”„ Automated Workflows + +| Workflow | Trigger | Purpose | +|----------|---------|---------| +| **Linting** | Every PR | Code quality checks | +| **Build** | Every PR | Test & build verification | +| **Develop** | PR to `develop` | Deploy to Firebase App Distribution | +| **Staging** | PR to `rc` | Deploy to Google Play Internal | +| **Production** | PR to `main` | Production release | + +### โœจ What Gets Automated + +- ๐Ÿ” **Code Analysis**: Linting and static analysis +- ๐Ÿงช **Testing**: Unit, widget, and integration tests +- ๐Ÿ“ฆ **Building**: Android APK/AAB generation +- ๐Ÿš€ **Deployment**: Automatic app distribution +- ๐Ÿท๏ธ **Versioning**: Automatic version bumping +- ๐Ÿ“Š **Coverage**: Code coverage reporting + +## ๐ŸŽฏ Issue Types & Templates + +### ๐Ÿ› Bug Reports +Use for: Fixing existing functionality +**Template**: Bug report template + +### โœจ Feature Requests +Use for: New functionality +**Template**: Feature request template + +### ๐Ÿ“š Documentation +Use for: Improving docs +**Template**: Documentation template + +### ๐Ÿ”ง Maintenance +Use for: Refactoring, dependencies +**Template**: Maintenance template + +## ๐Ÿ† Recognition + +### ๐ŸŒŸ Hall of Contributors + +Outstanding contributors get: +- **Recognition** in release notes +- **Contributor badge** on profile +- **Priority** on issue assignments +- **Mentorship** opportunities + +### ๐Ÿ“ˆ Contribution Levels + +| Level | Contributions | Benefits | +|-------|--------------|----------| +| **Rookie** | 1-5 PRs | Welcome package | +| **Contributor** | 6-15 PRs | Priority reviews | +| **Champion** | 16+ PRs | Mentor role | + +## ๐Ÿค Community Guidelines + +### โœจ Be Awesome + +- **Be respectful**: Treat everyone with kindness +- **Be constructive**: Provide helpful feedback +- **Be patient**: Reviews take time +- **Be inclusive**: Welcome all skill levels + +### ๐Ÿ’ฌ Communication + +- **Issues**: Technical discussions +- **Discussions**: General questions & ideas +- **Discord**: Real-time chat (coming soon!) + +## ๐Ÿ“ž Need Help? + +### ๐Ÿ†˜ Getting Support + +1. **Check existing issues**: Someone might have asked already +2. **Search documentation**: Look in `/docs/` folder +3. **Create an issue**: Use appropriate template +4. **Join discussions**: Share ideas and get help + +### ๐Ÿ“ง Contact + +- **Project Maintainer**: [@ZanderCowboy](https://github.com/ZanderCowboy) +- **Issues**: [GitHub Issues](https://github.com/ZanderCowboy/multichoice/issues) +- **Discussions**: [GitHub Discussions](https://github.com/ZanderCowboy/multichoice/discussions) + +--- + +## ๐ŸŽ‰ Ready to Contribute? + +1. ๐Ÿด **Fork** the repository +2. ๐Ÿ” **Find** an issue that interests you +3. ๐Ÿ’ฌ **Comment** to get assigned +4. ๐Ÿš€ **Code** your solution +5. ๐Ÿ“ **Document** your changes +6. ๐Ÿ”„ **Submit** a PR + +**Let's build something amazing together!** ๐Ÿš€โœจ + +--- + +> **Pro Tip**: Start with issues labeled `good first issue` or `help wanted` if you're new to the project! \ No newline at end of file diff --git a/docs/design-patterns-analysis.md b/docs/design-patterns-analysis.md new file mode 100644 index 00000000..00940b3b --- /dev/null +++ b/docs/design-patterns-analysis.md @@ -0,0 +1,697 @@ +# Design Patterns in Multichoice Flutter App + +This document provides a comprehensive analysis of the design patterns implemented in the Multichoice Flutter application codebase. The application demonstrates excellent architectural principles with clear separation of concerns and proper implementation of various design patterns. + +## Table of Contents + +1. [Architecture Overview](#architecture-overview) +2. [Creational Patterns](#creational-patterns) +3. [Structural Patterns](#structural-patterns) +4. [Behavioral Patterns](#behavioral-patterns) +5. [Architectural Patterns](#architectural-patterns) +6. [Flutter-Specific Patterns](#flutter-specific-patterns) +7. [Summary](#summary) + +## Architecture Overview + +The codebase follows a **Clean Architecture** pattern with modular package structure: + +``` +packages/ +โ”œโ”€โ”€ core/ # Business logic and use cases +โ”œโ”€โ”€ models/ # Data models and DTOs +โ”œโ”€โ”€ theme/ # UI theming +โ””โ”€โ”€ ui_kit/ # Reusable UI components +``` + +This structure promotes separation of concerns, testability, and maintainability. + +## Creational Patterns + +### 1. Factory Pattern + +The codebase extensively uses factory constructors for object creation, particularly in DTOs and UI components. + +#### DTO Factory Constructors + +```dart +// packages/models/lib/src/dto/tabs/tabs_dto.dart +factory TabsDTO.empty() => TabsDTO( + id: 0, + title: '', + subtitle: '', + timestamp: DateTime.now(), + entries: [], +); + +// packages/models/lib/src/dto/entry/entry_dto.dart +factory EntryDTO.empty() => EntryDTO( + id: 0, + tabId: 0, + title: '', + subtitle: '', + timestamp: DateTime.now(), +); +``` + +#### UI Component Factories + +```dart +// packages/ui_kit/lib/src/widgets/loaders/circular_loader.dart +/// Creates a small loader (32x32 with 4px stroke) +factory CircularLoader.small({ + Key? key, + double? value, + Color? backgroundColor, + Color? color, + // ... other parameters +}) { + return CircularLoader._( + key: key, + size: 32.0, + strokeWidth: 4.0, + // ... other properties + ); +} + +factory CircularLoader.medium({ /* ... */ }) +factory CircularLoader.large({ /* ... */ }) +factory CircularLoader.custom({ /* ... */ }) +``` + +### 2. Singleton Pattern + +The application uses singletons for shared resources and services. + +#### Service Locator Singleton + +```dart +// packages/core/lib/src/get_it_injection.dart +final coreSl = GetIt.instance; +``` + +#### Controller Singleton + +```dart +// packages/core/lib/src/controllers/implementations/product_tour_controller.dart +@Singleton(as: IProductTourController) +class ProductTourController implements IProductTourController { + // Implementation +} +``` + +### 3. Builder Pattern + +Used for creating complex objects and widgets. + +#### Dialog Builder + +```dart +// packages/ui_kit/lib/src/custom_dialog.dart +class CustomDialog { + CustomDialog.show({ + required this.context, + this.title, + this.actions, + this.content, + }) { + showDialog( + context: context, + builder: _buildWidget, + ); + } + + Widget _buildWidget(BuildContext context) { + return AlertDialog( + title: title, + content: content, + actions: actions, + ); + } +} +``` + +#### Widget Test Builder + +```dart +// apps/multichoice/test/helpers/widget_wrapper.dart +Widget widgetWrapper({required Widget child}) { + return MaterialApp( + home: Scaffold( + body: child, + ), + ); +} +``` + +## Structural Patterns + +### 1. Repository Pattern + +The repository pattern abstracts data access logic and provides a clean interface for data operations. + +#### Repository Interface + +```dart +// packages/core/lib/src/repositories/interfaces/tabs/i_tabs_repository.dart +abstract class ITabsRepository { + Future addTab({ + required String? title, + required String? subtitle, + }); + + Future getTab({required int tabId}); + Future> readTabs(); + Future updateTab({ + required int id, + required String title, + required String subtitle, + }); + Future deleteTab({required int? tabId}); + Future deleteTabs(); +} +``` + +#### Repository Implementation + +```dart +// packages/core/lib/src/repositories/implementation/tabs/tabs_repository.dart +@LazySingleton(as: ITabsRepository) +class TabsRepository implements ITabsRepository { + TabsRepository(this.db); + + final isar.Isar db; + + @override + Future addTab({ + required String? title, + required String? subtitle, + }) async { + try { + return await db.writeTxn(() async { + final result = db.tabs.put( + Tabs( + uuid: const Uuid().v4(), + title: title ?? '', + subtitle: subtitle, + timestamp: clock.now(), + entryIds: [], + ), + ); + return result; + }); + } catch (e) { + log(e.toString()); + return 0; + } + } + // ... other implementations +} +``` + +### 2. Adapter Pattern (Mapper Pattern) + +The mapper pattern adapts data between different layers (database models to DTOs). + +```dart +// packages/models/lib/src/mappers/tabs/tabs_dto_mapper.dart +@AutoMappr([ + MapType( + fields: [ + Field('id', custom: TabsMapper.mapUuid), + Field('title'), + Field('subtitle', custom: TabsMapper.mapSubtitle), + Field('timestamp', custom: TabsMapper.mapTimestamp), + Field('entries', custom: TabsMapper.mapEntryIds), + ], + ), +]) +class TabsMapper extends $TabsMapper { + static int mapUuid(Tabs content) => content.id; + static String mapSubtitle(Tabs content) => content.subtitle ?? ''; + static DateTime mapTimestamp(Tabs content) => content.timestamp ?? DateTime.now(); + static List mapEntryIds(Tabs content) { + return (content.entryIds ?? []) + .map((id) => EntryDTO.empty().copyWith(id: id)) + .toList(); + } +} +``` + +### 3. Decorator Pattern + +The decorator pattern is used to extend widget functionality without modifying the original widget. + +#### Tour Widget Wrapper + +```dart +// apps/multichoice/lib/utils/product_tour/tour_widget_wrapper.dart +class TourWidgetWrapper extends StatelessWidget { + const TourWidgetWrapper({ + required this.step, + required this.child, + this.tabId, + super.key, + }); + + final Widget child; + final ProductTourStep step; + final int? tabId; + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state.currentStep != step) { + return child; // Return original widget + } + + final key = getProductTourKey(step, tabId: tabId); + final showcaseData = _getProductTourData(step); + + if (key != null && context.mounted) { + return Showcase( // Decorated widget + key: key, + title: showcaseData.title, + description: showcaseData.description, + // ... other showcase properties + child: child, + ); + } + + return child; + }, + ); + } +} +``` + +#### Custom Scroll Behavior + +```dart +// packages/ui_kit/lib/src/custom_scroll_behaviour.dart +class CustomScrollBehaviour extends MaterialScrollBehavior { + @override + Set get dragDevices => { + PointerDeviceKind.touch, + PointerDeviceKind.mouse, + }; +} +``` + +### 4. Facade Pattern + +The service layer acts as a facade to simplify complex subsystem interactions. + +```dart +// packages/core/lib/src/services/implementations/app_storage_service.dart +@LazySingleton(as: IAppStorageService) +class AppStorageService implements IAppStorageService { + final _sharedPreferences = coreSl(); + + @override + Future get isDarkMode async { + final isDarkMode = _sharedPreferences.getBool(StorageKeys.isDarkMode.key); + return isDarkMode ?? false; + } + + @override + Future setIsDarkMode(bool isDarkMode) async { + await _sharedPreferences.setBool( + StorageKeys.isDarkMode.key, + isDarkMode, + ); + } + // ... other storage operations +} +``` + +## Behavioral Patterns + +### 1. Observer Pattern + +Implemented through the BLoC pattern and Flutter's reactive framework. + +#### BLoC Observer + +```dart +// apps/multichoice/lib/bootstrap.dart +class SimpleBlocObserver extends BlocObserver { + const SimpleBlocObserver(); + + @override + void onEvent(Bloc bloc, Object? event) { + super.onEvent(bloc, event); + log('${bloc.runtimeType} $event'); + } + + @override + void onError(BlocBase bloc, Object error, StackTrace stackTrace) { + log('${bloc.runtimeType} $error'); + super.onError(bloc, error, stackTrace); + } + + @override + void onTransition(Bloc bloc, Transition transition) { + super.onTransition(bloc, transition); + log('$transition'); + } +} +``` + +### 2. Command Pattern + +Events in the BLoC pattern represent commands that encapsulate requests. + +```dart +// packages/core/lib/src/application/home/home_event.dart +@freezed +class HomeEvent with _$HomeEvent { + const factory HomeEvent.onGetTabs() = OnGetTabs; + const factory HomeEvent.onGetTab(int tabId) = OnGetTab; + const factory HomeEvent.onPressedAddTab() = OnPressedAddTab; + const factory HomeEvent.onPressedAddEntry() = OnPressedAddEntry; + const factory HomeEvent.onLongPressedDeleteTab(int tabId) = OnLongPressedDeleteTab; + const factory HomeEvent.onLongPressedDeleteEntry(int tabId, int entryId) = OnLongPressedDeleteEntry; + // ... other commands +} +``` + +### 3. State Pattern + +The BLoC pattern implements state management with clear state transitions. + +```dart +// packages/core/lib/src/application/home/home_state.dart +@freezed +class HomeState with _$HomeState { + factory HomeState({ + required List? tabs, + required bool isLoading, + required bool isAddingTab, + required bool isAddingEntry, + required bool isEditingTab, + required bool isEditingEntry, + String? errorMessage, + // ... other state properties + }) = _HomeState; + + factory HomeState.initial() => HomeState( + tabs: null, + isLoading: true, + isAddingTab: false, + isAddingEntry: false, + isEditingTab: false, + isEditingEntry: false, + ); +} +``` + +### 4. Strategy Pattern + +Different strategies for error handling and data processing. + +#### Error Handling Strategy + +```dart +// packages/core/lib/src/repositories/implementation/feedback/feedback_repository.dart +@override +Future> submitFeedback(FeedbackDTO feedback) async { + try { + final model = _mapper.convert(feedback); + await _firestore.collection(_collection).add(model.toFirestore()); + return const Right(null); // Success strategy + } catch (e) { + return Left(FeedbackException('Failed to submit feedback: $e')); // Error strategy + } +} +``` + +### 5. Template Method Pattern + +Base classes define the structure while subclasses implement specific behavior. + +```dart +// apps/multichoice/lib/presentation/shared/widgets/add_widgets/_base.dart +class _BaseCard extends StatelessWidget { + const _BaseCard({ + required this.semanticLabel, + this.elevation, + this.color, + this.shape, + this.child, + this.icon, + this.padding, + this.margin, + this.iconSize, + this.onPressed, + }) : assert( + (child != null) || (icon != null), + 'Either child or icon must be non-null', + ); + + @override + Widget build(BuildContext context) { + return Semantics( + label: semanticLabel, + child: Card( + elevation: elevation, + margin: margin, + color: color, + shape: shape, + child: child ?? + Padding( + padding: padding ?? allPadding6, + child: IconButton( + icon: icon ?? const Icon(Icons.not_interested_rounded), + iconSize: iconSize ?? 10, + onPressed: onPressed, + ), + ), + ), + ); + } +} +``` + +## Architectural Patterns + +### 1. Dependency Injection Pattern + +The application uses GetIt with Injectable for comprehensive dependency management. + +#### Module Configuration + +```dart +// packages/core/lib/src/injectable_module.dart +@module +abstract class InjectableModule { + @preResolve + Future get isar async { + final directory = await getApplicationDocumentsDirectory(); + final isar = await Isar.open( + [TabsSchema, EntrySchema], + directory: directory.path, + name: 'MultichoiceDB', + ); + return isar; + } + + @preResolve + Future get sharedPref async { + final sharedPref = await SharedPreferences.getInstance(); + return sharedPref; + } + + @lazySingleton + FilePicker get filePicker => FilePicker.platform; + + @lazySingleton + FirebaseFirestore get firestore => FirebaseFirestore.instance; +} +``` + +#### Dependency Registration + +```dart +// packages/core/lib/src/get_it_injection.dart +@InjectableInit( + initializerName: 'init', + preferRelativeImports: true, + asExtension: true, +) +Future configureCoreDependencies() async { + return coreSl.init(); +} +``` + +### 2. Clean Architecture Pattern + +Clear separation between layers with dependency inversion. + +``` +โ”œโ”€โ”€ Application Layer (BLoCs, Use Cases) +โ”œโ”€โ”€ Domain Layer (Entities, Repositories) +โ”œโ”€โ”€ Infrastructure Layer (Database, External APIs) +โ””โ”€โ”€ Presentation Layer (UI, Widgets) +``` + +### 3. MVVM Pattern (with BLoC) + +Model-View-ViewModel pattern implemented through BLoC. + +```dart +// packages/core/lib/src/application/home/home_bloc.dart +@Injectable() +class HomeBloc extends Bloc { + HomeBloc( + this._tabsRepository, + this._entryRepository, + ) : super(HomeState.initial()) { + on(_onEvent); + } + + Future _onEvent(HomeEvent event, Emitter emit) async { + switch (event) { + case OnGetTabs(): + await _handleGetTabs(emit); + case OnGetTab(:final tabId): + await _handleGetTab(tabId, emit); + // ... handle other events + } + } + + final ITabsRepository _tabsRepository; + final IEntryRepository _entryRepository; +} +``` + +## Flutter-Specific Patterns + +### 1. BLoC Pattern + +Business Logic Component pattern for state management. + +```dart +// Usage in UI +BlocBuilder( + builder: (context, state) { + if (state.isLoading) { + return const CircularProgressIndicator(); + } + + return ListView.builder( + itemCount: state.tabs?.length ?? 0, + itemBuilder: (context, index) { + final tab = state.tabs![index]; + return ListTile( + title: Text(tab.title), + subtitle: Text(tab.subtitle), + ); + }, + ); + }, +) +``` + +### 2. Provider Pattern + +Used for dependency injection in widget tree. + +```dart +// apps/multichoice/lib/presentation/home/home_page.dart +return MultiProvider( + providers: [ + ChangeNotifierProvider( + create: (_) => AppLayout(), + ), + BlocProvider( + create: (_) => coreSl() + ..add(const HomeEvent.onGetTabs()), + ), + BlocProvider( + create: (_) => coreSl(), + ), + ], + child: const HomePage(), +); +``` + +### 3. Widget Composition Pattern + +Building complex UIs through widget composition. + +```dart +// apps/multichoice/lib/presentation/shared/widgets/add_widgets/tab.dart +class AddTabCard extends StatelessWidget { + @override + Widget build(BuildContext context) { + return _BaseCard( // Composition with base card + semanticLabel: semanticLabel ?? 'AddTab', + elevation: 5, + color: color ?? context.theme.appColors.primaryLight, + shape: RoundedRectangleBorder( + borderRadius: borderCircular12, + ), + child: Padding( + padding: allPadding6, + child: SizedBox( + key: context.keys.addTabSizedBox, + width: width, + child: IconButton( + iconSize: 36, + onPressed: onPressed, + icon: const Icon(Icons.add_outlined), + ), + ), + ), + ); + } +} +``` + +## Summary + +The Multichoice Flutter application demonstrates excellent implementation of design patterns: + +### **Creational Patterns:** + +- **Factory Pattern**: Extensive use in DTOs and UI components for object creation +- **Singleton Pattern**: Service locator and shared controllers +- **Builder Pattern**: Complex widget and dialog construction + +### **Structural Patterns:** + +- **Repository Pattern**: Clean data access abstraction +- **Adapter/Mapper Pattern**: Data transformation between layers +- **Decorator Pattern**: Widget functionality extension +- **Facade Pattern**: Simplified service interfaces + +### **Behavioral Patterns:** + +- **Observer Pattern**: BLoC-based reactive state management +- **Command Pattern**: Event encapsulation in BLoCs +- **State Pattern**: Clear state management and transitions +- **Strategy Pattern**: Flexible algorithm selection +- **Template Method Pattern**: Shared behavior with customization + +### **Architectural Patterns:** + +- **Clean Architecture**: Clear separation of concerns +- **Dependency Injection**: Loose coupling and testability +- **MVVM with BLoC**: Separation of UI and business logic + +### **Flutter-Specific Patterns:** + +- **BLoC Pattern**: Reactive state management +- **Provider Pattern**: Dependency injection in widget tree +- **Widget Composition**: Flexible UI construction + +This architecture promotes: +- **Maintainability**: Clear structure and separation of concerns +- **Testability**: Dependency injection and interface-based design +- **Scalability**: Modular architecture and loose coupling +- **Reusability**: Shared components and services +- **Code Quality**: Consistent patterns and best practices + +The codebase serves as an excellent example of how to structure a Flutter application using proven design patterns and architectural principles. diff --git a/docs/dev-prod-implementation-checklist.md b/docs/dev-prod-implementation-checklist.md new file mode 100644 index 00000000..38d6ca48 --- /dev/null +++ b/docs/dev-prod-implementation-checklist.md @@ -0,0 +1,268 @@ +# DEV/PROD Environment Implementation Checklist + +Use this checklist to systematically implement the DEV and PROD environment setup. Check off each item as you complete it. + +## Phase 1: Firebase Setup (Critical First Step) + +### [ ] Create Dev Firebase Project + +- [ ] Go to [Firebase Console](https://console.firebase.google.com/) +- [ ] Create new project: `multichoice-dev` +- [ ] Enable Google Analytics (optional) + +### [ ] Configure Services in Dev Project + +- [ ] **Authentication** + - [ ] Enable Google Sign-in + - [ ] Add SHA-1 fingerprints for dev builds +- [ ] **Firestore Database** + - [ ] Create database (start in test mode) + - [ ] Select same region as production +- [ ] **Cloud Functions** + - [ ] Set up functions directory + - [ ] Copy feedback function from production +- [ ] **App Distribution** + - [ ] Enable App Distribution + - [ ] Create tester groups + +### [ ] Add Android App to Dev Project + +- [ ] Add Android app with package name: `com.zander.multichoice.dev` +- [ ] Download `google-services.json` for dev + +## Phase 2: Android Configuration + +### [ ] Update build.gradle + +- [ ] Add flavor dimensions in `apps/multichoice/android/app/build.gradle` +- [ ] Create `dev` and `prod` flavors +- [ ] Set application ID suffixes +- [ ] Configure app names and manifest placeholders + +### [ ] Create Firebase Config Directories + +``` +apps/multichoice/android/app/src/ +โ”œโ”€โ”€ dev/google-services.json # New dev config +โ”œโ”€โ”€ prod/google-services.json # Move existing config here +โ””โ”€โ”€ main/... (existing files) +``` + +### [ ] Create Environment-Specific Icons (Optional) + +- [ ] Create dev app icon with "DEV" badge +- [ ] Place in `apps/multichoice/android/app/src/dev/res/mipmap-*/` +- [ ] Copy production icons to `apps/multichoice/android/app/src/prod/res/mipmap-*/` + +## Phase 3: Flutter App Configuration + +### [ ] Create Config Classes + +- [ ] Create `apps/multichoice/lib/config/app_config.dart` +- [ ] Create `apps/multichoice/lib/config/dev_config.dart` +- [ ] Create `apps/multichoice/lib/config/prod_config.dart` + +### [ ] Create Environment-Specific Main Files + +- [ ] Create `apps/multichoice/lib/main_dev.dart` +- [ ] Create `apps/multichoice/lib/main_prod.dart` +- [ ] Update existing `apps/multichoice/lib/main.dart` + +### [ ] Create Firebase Options Files + +- [ ] Generate `apps/multichoice/lib/firebase_options_dev.dart` using Firebase CLI +- [ ] Update existing `firebase_options.dart` for production + +### [ ] Update Bootstrap + +- [ ] Modify `apps/multichoice/lib/bootstrap.dart` to use environment-specific Firebase options +- [ ] Add AppConfig initialization + +## Phase 4: Local Testing + +### [ ] Test Dev Build Locally + +```bash +cd apps/multichoice +flutter run --flavor dev --target lib/main_dev.dart +``` + +### [ ] Test Prod Build Locally + +```bash +cd apps/multichoice +flutter run --flavor prod --target lib/main_prod.dart +``` + +### [ ] Verify Environment Separation + +- [ ] Check app names are different +- [ ] Verify different Firebase projects are being used +- [ ] Test authentication with different Firebase projects + +## Phase 5: CI/CD Pipeline Updates + +### [ ] Update GitHub Secrets + +Add new secrets for dev environment: +- [ ] `DEV_ANDROID_KEYSTORE_BASE64` +- [ ] `DEV_ANDROID_KEYSTORE_PASSWORD` +- [ ] `DEV_ANDROID_KEY_ALIAS` +- [ ] `DEV_ANDROID_KEY_PASSWORD` +- [ ] `DEV_FIREBASE_TOKEN` +- [ ] `DEV_GOOGLE_SERVICES_JSON` +- [ ] `DEV_FIREBASE_APP_ID` +- [ ] `DEV_FIREBASE_SERVICE_ACCOUNT` + +### [ ] Create Dev Workflow + +- [ ] Create `.github/workflows/dev-build.yml` +- [ ] Configure to trigger on `develop` branch +- [ ] Set up Firebase App Distribution upload + +### [ ] Update Existing Workflows + +- [ ] Update production workflow to use `--flavor prod` +- [ ] Update staging workflow if needed +- [ ] Test workflows with manual triggers + +## Phase 6: Google Play Console Setup + +### [ ] Choose Strategy + +- [ ] **Option A**: Use existing app with different tracks (recommended) + - [ ] Set up Internal Testing track for dev builds + - [ ] Set up Alpha track for staging + - [ ] Keep Production track for live releases +- [ ] **Option B**: Create separate dev app (more isolation) + - [ ] Create new app in Google Play Console + - [ ] Set up separate app configuration + +### [ ] Configure Testing + +- [ ] Add internal testers +- [ ] Set up testing groups +- [ ] Configure release notes templates + +## Phase 7: Testing and Validation + +### [ ] End-to-End Testing + +- [ ] Build and deploy dev version via CI/CD +- [ ] Test Firebase App Distribution +- [ ] Verify data isolation (dev vs prod Firebase) +- [ ] Test authentication flows +- [ ] Verify app icons and names + +### [ ] Production Validation + +- [ ] Ensure production builds still work +- [ ] Verify no impact on existing users +- [ ] Test production CI/CD pipeline + +## Phase 8: Team Onboarding + +### [ ] Documentation + +- [ ] Share environment setup guide with team +- [ ] Document new development workflow +- [ ] Create troubleshooting guide for common issues + +### [ ] Training + +- [ ] Train team on new build commands +- [ ] Explain when to use dev vs prod +- [ ] Set up team access to Firebase projects + +## Phase 9: Monitoring and Optimization + +### [ ] Set Up Monitoring + +- [ ] Monitor Firebase usage for both projects +- [ ] Set up alerts for build failures +- [ ] Track deployment metrics + +### [ ] Optimization + +- [ ] Review and optimize build times +- [ ] Fine-tune testing strategies +- [ ] Gather team feedback and iterate + +## Quick Commands Reference + +### Local Development + +```bash +# Run dev build locally +flutter run --flavor dev --target lib/main_dev.dart + +# Run prod build locally +flutter run --flavor prod --target lib/main_prod.dart + +# Build dev APK +flutter build apk --flavor dev --target lib/main_dev.dart + +# Build prod APK +flutter build apk --flavor prod --target lib/main_prod.dart +``` + +### Firebase CLI + +```bash +# Switch to dev project +firebase use multichoice-dev + +# Switch to prod project +firebase use multichoice-412309 + +# Deploy functions to current project +firebase deploy --only functions +``` + +### Android Gradle + +```bash +# Get SHA-1 for dev builds +cd apps/multichoice/android +./gradlew signingReport +``` + +## Rollback Plan + +If something goes wrong, here's how to quickly rollback: + +### [ ] Emergency Rollback Steps + +1. [ ] Revert any changes to existing `main.dart` +2. [ ] Remove flavor configurations from `build.gradle` +3. [ ] Move `google-services.json` back to original location +4. [ ] Revert CI/CD workflow changes +5. [ ] Test production build works as before + +## Success Criteria + +You'll know the setup is successful when: +- [ ] You can build both dev and prod flavors locally +- [ ] Dev builds connect to dev Firebase project +- [ ] Prod builds connect to prod Firebase project +- [ ] CI/CD deploys dev builds to Firebase App Distribution +- [ ] Team can test features safely in dev environment +- [ ] Production app remains unaffected + +## Common Issues and Solutions + +### Build Errors + +- **Flavor not found**: Check `build.gradle` syntax +- **Firebase config missing**: Verify file locations +- **Keystore issues**: Check secrets and file paths + +### Runtime Issues + +- **Wrong Firebase project**: Verify `AppConfig` setup +- **Authentication fails**: Check SHA-1 fingerprints +- **Data not syncing**: Confirm Firestore rules + +--- + +**Pro Tip**: Start with Phase 1 (Firebase setup) and test each phase thoroughly before moving to the next. This incremental approach reduces risk and makes troubleshooting easier. diff --git a/docs/implementing-login-with-google-signin.md b/docs/implementing-login-with-google-signin.md new file mode 100644 index 00000000..c736215c --- /dev/null +++ b/docs/implementing-login-with-google-signin.md @@ -0,0 +1,842 @@ +# Implementing Login Functionality with Google Sign-In + +This guide provides a comprehensive approach to implementing authentication with Google Sign-In in your multichoice app, following the existing clean architecture patterns. + +## Table of Contents + +1. [Overview](#overview) +2. [Current Architecture Analysis](#current-architecture-analysis) +3. [Prerequisites](#prerequisites) +4. [Implementation Steps](#implementation-steps) +5. [Firebase Configuration](#firebase-configuration) +6. [Testing](#testing) +7. [Security Considerations](#security-considerations) +8. [Troubleshooting](#troubleshooting) +9. [Next Steps](#next-steps) +10. [Resources](#resources) + +## Overview + +Your app already has a solid foundation for authentication with: +- Firebase Core integration +- Clean architecture with dependency injection (GetIt + Injectable) +- BLoC pattern for state management +- Existing Session service for token management +- Firebase project configured (`multichoice-412309`) + +This implementation will extend the existing architecture to support Google Sign-In while maintaining the established patterns. + +Remember to have a deregister option. + +## Current Architecture Analysis + +### Existing Components + +- **Session Service**: Basic token storage in SharedPreferences +- **Dependency Injection**: GetIt with Injectable for service management +- **Firebase Integration**: Core Firebase already configured +- **Clean Architecture**: Separation of concerns with interfaces and implementations + +### What We'll Add + +- Firebase Authentication +- Google Sign-In integration +- Enhanced user management +- Authentication state management with BLoC +- User profile data models + +## Prerequisites + +### 1. Firebase Console Setup + +1. Go to [Firebase Console](https://console.firebase.google.com/) +2. Select your project: `multichoice-412309` +3. Enable Authentication: + - Go to Authentication โ†’ Sign-in method + - Enable Google Sign-in + - Add your app's SHA-1 fingerprint for Android + +### 2. Dependencies to Add + +Add these to `apps/multichoice/pubspec.yaml`: +```yaml +dependencies: + firebase_auth: ^5.3.3 + google_sign_in: ^6.2.1 + # ... existing dependencies +``` + +Add these to `packages/core/pubspec.yaml`: +```yaml +dependencies: + firebase_auth: ^5.3.3 + # ... existing dependencies +``` + +### 3. Android Configuration + +Update `apps/multichoice/android/app/build.gradle`: +```gradle +android { + defaultConfig { + // ... existing config + multiDexEnabled true + } +} + +dependencies { + // ... existing dependencies + implementation 'com.google.android.gms:play-services-auth:20.7.0' +} +``` + +### 4. iOS Configuration + +Update `apps/multichoice/ios/Runner/Info.plist`: +```xml +CFBundleURLTypes + + + CFBundleURLName + REVERSED_CLIENT_ID + CFBundleURLSchemes + + com.googleusercontent.apps.YOUR_CLIENT_ID + + + +``` + +## Implementation Steps + +### Step 1: Create User Models + +Create `packages/models/lib/src/models/user_model.dart`: +```dart +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:isar/isar.dart'; + +part 'user_model.freezed.dart'; +part 'user_model.g.dart'; + +@freezed +@collection +class UserModel with _$UserModel { + const factory UserModel({ + required String id, + required String email, + String? displayName, + String? photoURL, + required DateTime createdAt, + required DateTime updatedAt, + }) = _UserModel; + + factory UserModel.fromJson(Map json) => + _$UserModelFromJson(json); +} +``` + +Create `packages/models/lib/src/dto/user_dto.dart`: +```dart +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'user_dto.freezed.dart'; +part 'user_dto.g.dart'; + +@freezed +class UserDTO with _$UserDTO { + const factory UserDTO({ + required String id, + required String email, + String? displayName, + String? photoURL, + required DateTime createdAt, + required DateTime updatedAt, + }) = _UserDTO; + + factory UserDTO.fromJson(Map json) => + _$UserDTOFromJson(json); +} +``` + +### Step 2: Create Authentication Service Interface + +Create `packages/core/lib/src/services/interfaces/i_auth_service.dart`: +```dart +import 'package:dartz/dartz.dart'; +import 'package:models/models.dart'; + +abstract class AuthException implements Exception { + final String message; + AuthException(this.message); + + @override + String toString() => message; +} + +abstract class IAuthService { + Future> signInWithGoogle(); + Future> signOut(); + Future> getCurrentUser(); + Stream get authStateChanges; +} +``` + +### Step 3: Create Authentication Service Implementation + +Create `packages/core/lib/src/services/implementations/auth_service.dart`: +```dart +import 'package:dartz/dartz.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:google_sign_in/google_sign_in.dart'; +import 'package:injectable/injectable.dart'; +import 'package:models/models.dart'; +import 'package:core/src/services/interfaces/i_auth_service.dart'; + +@LazySingleton(as: IAuthService) +class AuthService implements IAuthService { + final FirebaseAuth _firebaseAuth; + final GoogleSignIn _googleSignIn; + + AuthService(this._firebaseAuth, this._googleSignIn); + + @override + Future> signInWithGoogle() async { + try { + final GoogleSignInAccount? googleUser = await _googleSignIn.signIn(); + if (googleUser == null) { + return Left(AuthException('Google sign-in was cancelled')); + } + + final GoogleSignInAuthentication googleAuth = + await googleUser.authentication; + + final credential = GoogleAuthProvider.credential( + accessToken: googleAuth.accessToken, + idToken: googleAuth.idToken, + ); + + final UserCredential userCredential = + await _firebaseAuth.signInWithCredential(credential); + + final user = userCredential.user; + if (user == null) { + return Left(AuthException('Failed to sign in with Google')); + } + + final userDTO = UserDTO( + id: user.uid, + email: user.email ?? '', + displayName: user.displayName, + photoURL: user.photoURL, + createdAt: user.metadata.creationTime ?? DateTime.now(), + updatedAt: user.metadata.lastSignInTime ?? DateTime.now(), + ); + + return Right(userDTO); + } catch (e) { + return Left(AuthException('Google sign-in failed: $e')); + } + } + + @override + Future> signOut() async { + try { + await Future.wait([ + _firebaseAuth.signOut(), + _googleSignIn.signOut(), + ]); + return const Right(null); + } catch (e) { + return Left(AuthException('Sign out failed: $e')); + } + } + + @override + Future> getCurrentUser() async { + try { + final user = _firebaseAuth.currentUser; + if (user == null) { + return const Right(null); + } + + final userDTO = UserDTO( + id: user.uid, + email: user.email ?? '', + displayName: user.displayName, + photoURL: user.photoURL, + createdAt: user.metadata.creationTime ?? DateTime.now(), + updatedAt: user.metadata.lastSignInTime ?? DateTime.now(), + ); + + return Right(userDTO); + } catch (e) { + return Left(AuthException('Failed to get current user: $e')); + } + } + + @override + Stream get authStateChanges { + return _firebaseAuth.authStateChanges().map((user) { + if (user == null) return null; + + return UserDTO( + id: user.uid, + email: user.email ?? '', + displayName: user.displayName, + photoURL: user.photoURL, + createdAt: user.metadata.creationTime ?? DateTime.now(), + updatedAt: user.metadata.lastSignInTime ?? DateTime.now(), + ); + }); + } +} +``` + +### Step 4: Create Authentication BLoC + +Create `packages/core/lib/src/application/auth/auth_bloc.dart`: +```dart +import 'package:bloc/bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:injectable/injectable.dart'; +import 'package:models/models.dart'; +import 'package:core/src/services/interfaces/i_auth_service.dart'; + +part 'auth_event.dart'; +part 'auth_state.dart'; +part 'auth_bloc.freezed.dart'; + +@injectable +class AuthBloc extends Bloc { + final IAuthService _authService; + + AuthBloc(this._authService) : super(const AuthState.initial()) { + on((event, emit) async { + await event.map( + signInWithGoogle: (e) async => await _onSignInWithGoogle(emit), + signOut: (e) async => await _onSignOut(emit), + checkAuthState: (e) async => await _onCheckAuthState(emit), + authStateChanged: (e) async => await _onAuthStateChanged(e, emit), + ); + }); + + // Listen to auth state changes + _authService.authStateChanges.listen((user) { + add(AuthEvent.authStateChanged(user)); + }); + } + + Future _onSignInWithGoogle(Emitter emit) async { + emit(const AuthState.loading()); + + final result = await _authService.signInWithGoogle(); + + result.fold( + (failure) => emit(AuthState.error(failure.toString())), + (user) => emit(AuthState.authenticated(user)), + ); + } + + Future _onSignOut(Emitter emit) async { + emit(const AuthState.loading()); + + final result = await _authService.signOut(); + + result.fold( + (failure) => emit(AuthState.error(failure.toString())), + (_) => emit(const AuthState.unauthenticated()), + ); + } + + Future _onCheckAuthState(Emitter emit) async { + emit(const AuthState.loading()); + + final result = await _authService.getCurrentUser(); + + result.fold( + (failure) => emit(AuthState.error(failure.toString())), + (user) => user != null + ? emit(AuthState.authenticated(user)) + : emit(const AuthState.unauthenticated()), + ); + } + + Future _onAuthStateChanged( + AuthStateChanged event, + Emitter emit, + ) async { + if (event.user != null) { + emit(AuthState.authenticated(event.user!)); + } else { + emit(const AuthState.unauthenticated()); + } + } +} +``` + +Create `packages/core/lib/src/application/auth/auth_event.dart`: +```dart +part of 'auth_bloc.dart'; + +@freezed +class AuthEvent with _$AuthEvent { + const factory AuthEvent.signInWithGoogle() = SignInWithGoogle; + const factory AuthEvent.signOut() = SignOut; + const factory AuthEvent.checkAuthState() = CheckAuthState; + const factory AuthEvent.authStateChanged(UserDTO? user) = AuthStateChanged; +} +``` + +Create `packages/core/lib/src/application/auth/auth_state.dart`: +```dart +part of 'auth_bloc.dart'; + +@freezed +class AuthState with _$AuthState { + const factory AuthState.initial() = Initial; + const factory AuthState.loading() = Loading; + const factory AuthState.authenticated(UserDTO user) = Authenticated; + const factory AuthState.unauthenticated() = Unauthenticated; + const factory AuthState.error(String message) = Error; +} +``` + +### Step 5: Update Dependency Injection + +Update `packages/core/lib/src/injectable_module.dart`: +```dart +import 'package:file_picker/file_picker.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:google_sign_in/google_sign_in.dart'; +import 'package:injectable/injectable.dart'; +import 'package:isar/isar.dart'; +import 'package:models/models.dart' show TabsSchema, EntrySchema, UserSchema; +import 'package:cloud_firestore/cloud_firestore.dart' show FirebaseFirestore; +import 'package:path_provider/path_provider.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +@module +abstract class InjectableModule { + @preResolve + Future get isar async { + final directory = await getApplicationDocumentsDirectory(); + final isar = await Isar.open( + [ + TabsSchema, + EntrySchema, + UserSchema, // Add UserSchema + ], + directory: directory.path, + name: 'MultichoiceDB', + ); + + return isar; + } + + @preResolve + Future get sharedPref async { + final sharedPref = await SharedPreferences.getInstance(); + return sharedPref; + } + + @lazySingleton + FilePicker get filePicker => FilePicker.platform; + + @lazySingleton + FirebaseFirestore get firestore => FirebaseFirestore.instance; + + @lazySingleton + FirebaseAuth get firebaseAuth => FirebaseAuth.instance; + + @lazySingleton + GoogleSignIn get googleSignIn => GoogleSignIn(); +} +``` + +### Step 6: Create Login UI + +Create `apps/multichoice/lib/presentation/auth/login_page.dart`: +```dart +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:core/core.dart'; +import 'package:theme/theme.dart'; + +class LoginPage extends StatelessWidget { + const LoginPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: BlocListener( + listener: (context, state) { + state.mapOrNull( + error: (error) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(error.message), + backgroundColor: Colors.red, + ), + ); + }, + ); + }, + child: SafeArea( + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // App Logo/Title + const Icon( + Icons.apps, + size: 80, + color: Colors.blue, + ), + const SizedBox(height: 24), + Text( + 'Welcome to Multichoice', + style: context.textTheme.headlineMedium, + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + Text( + 'Sign in to continue', + style: context.textTheme.bodyLarge?.copyWith( + color: Colors.grey[600], + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 48), + + // Google Sign-In Button + BlocBuilder( + builder: (context, state) { + return SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: state.maybeWhen( + loading: () => null, + orElse: () => () { + context.read().add( + const AuthEvent.signInWithGoogle(), + ); + }, + ), + icon: state.maybeWhen( + loading: () => const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ), + orElse: () => const Icon(Icons.login), + ), + label: Text( + state.maybeWhen( + loading: () => 'Signing in...', + orElse: () => 'Sign in with Google', + ), + ), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + backgroundColor: Colors.white, + foregroundColor: Colors.black87, + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + ), + ); + }, + ), + ], + ), + ), + ), + ), + ); + } +} +``` + +### Step 7: Create Authentication Wrapper + +Create `apps/multichoice/lib/presentation/auth/auth_wrapper.dart`: +```dart +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:core/core.dart'; +import 'package:multichoice/presentation/auth/login_page.dart'; +import 'package:multichoice/layouts/home_layout/home_layout.dart'; + +class AuthWrapper extends StatelessWidget { + const AuthWrapper({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return state.map( + initial: (_) => const Scaffold( + body: Center(child: CircularProgressIndicator()), + ), + loading: (_) => const Scaffold( + body: Center(child: CircularProgressIndicator()), + ), + authenticated: (_) => const HomeLayout(), + unauthenticated: (_) => const LoginPage(), + error: (error) => Scaffold( + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('Error: ${error.message}'), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () { + context.read().add( + const AuthEvent.checkAuthState(), + ); + }, + child: const Text('Retry'), + ), + ], + ), + ), + ), + ); + }, + ); + } +} +``` + +### Step 8: Update Main App + +Update `apps/multichoice/lib/main.dart`: +```dart +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:core/core.dart'; +import 'package:multichoice/bootstrap.dart'; +import 'package:multichoice/presentation/auth/auth_wrapper.dart'; + +void main() async { + await bootstrap(); + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => coreSl() + ..add(const AuthEvent.checkAuthState()), + ), + // Add other BLoCs as needed + ], + child: MaterialApp( + title: 'Multichoice', + theme: ThemeData( + primarySwatch: Colors.blue, + useMaterial3: true, + ), + home: const AuthWrapper(), + ), + ); + } +} +``` + +### Step 9: Add Sign Out to Drawer + +Update your existing drawer to include sign out functionality: +```dart +// In your drawer widget +ListTile( + leading: const Icon(Icons.logout), + title: const Text('Sign Out'), + onTap: () { + context.read().add(const AuthEvent.signOut()); + }, +), +``` + +## Firebase Configuration + +### 1. Enable Google Sign-In in Firebase Console + +1. Go to Firebase Console โ†’ Authentication โ†’ Sign-in method +2. Enable Google provider +3. Add your app's SHA-1 fingerprint for Android + +### 2. Get SHA-1 Fingerprint + +```bash +cd apps/multichoice/android +./gradlew signingReport +``` + +### 3. Update google-services.json + +Download the updated `google-services.json` from Firebase Console and replace the existing one in `apps/multichoice/android/app/`. + +## Testing + +### 1. Unit Tests + +Create `packages/core/test/src/services/auth_service_test.dart`: +```dart +import 'package:bloc_test/bloc_test.dart'; +import 'package:core/core.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:models/models.dart'; + +import '../../../mocks.mocks.dart'; + +void main() { + group('AuthBloc', () { + late AuthBloc authBloc; + late MockIAuthService mockAuthService; + + setUp(() { + mockAuthService = MockIAuthService(); + authBloc = AuthBloc(mockAuthService); + }); + + tearDown(() { + authBloc.close(); + }); + + test('initial state is Initial', () { + expect(authBloc.state, const AuthState.initial()); + }); + + blocTest( + 'emits [Loading, Authenticated] when signInWithGoogle succeeds', + build: () { + when(mockAuthService.signInWithGoogle()).thenAnswer( + (_) async => Right(UserDTO( + id: 'test-id', + email: 'test@example.com', + createdAt: DateTime.now(), + updatedAt: DateTime.now(), + )), + ); + return authBloc; + }, + act: (bloc) => bloc.add(const AuthEvent.signInWithGoogle()), + expect: () => [ + const AuthState.loading(), + isA(), + ], + ); + }); +} +``` + +### 2. Integration Tests + +Add authentication tests to your existing integration test suite. + +## Security Considerations + +### 1. Firestore Security Rules + +Update your Firestore rules to include user-based access: +```javascript +rules_version = '2'; +service cloud.firestore { + match /databases/{database}/documents { + // Allow authenticated users to read/write their own data + match /users/{userId} { + allow read, write: if request.auth != null && request.auth.uid == userId; + } + + // Allow authenticated users to read/write their own tabs and entries + match /users/{userId}/tabs/{tabId} { + allow read, write: if request.auth != null && request.auth.uid == userId; + } + + match /users/{userId}/entries/{entryId} { + allow read, write: if request.auth != null && request.auth.uid == userId; + } + + // Keep existing feedback rules + match /feedback/{feedbackId} { + allow create: if true; + allow read: if request.auth != null; + allow update, delete: if false; + } + } +} +``` + +### 2. Token Management + +- The existing Session service already handles token storage +- Consider implementing token refresh logic +- Add token validation on app startup + +### 3. Error Handling + +- Implement proper error handling for network issues +- Add retry logic for failed authentication attempts +- Handle edge cases like account deletion + +## Troubleshooting + +### Common Issues + +1. **Google Sign-In not working on Android** + - Verify SHA-1 fingerprint is correct + - Check that google-services.json is up to date + - Ensure Google Play Services is installed + +2. **iOS Sign-In issues** + - Verify REVERSED_CLIENT_ID in Info.plist + - Check that GoogleService-Info.plist is properly configured + +3. **Build errors** + - Run `melos clean` and `melos get` + - Run `melos build:runner` to generate code + - Check that all dependencies are properly added + +### Debug Commands + +```bash +# Clean and rebuild +melos clean +melos get +melos build:runner + +# Run tests +melos test:core +melos test:multichoice + +# Check Firebase configuration +firebase projects:list +firebase auth:export users.json +``` + +## Next Steps + +1. **User Profile Management**: Add user profile editing capabilities +2. **Data Synchronization**: Sync user data with Firestore +3. **Offline Support**: Implement offline-first data handling +4. **Multi-account Support**: Allow users to switch between accounts +5. **Analytics**: Add authentication events to Firebase Analytics + +## Resources + +- [Firebase Authentication Documentation](https://firebase.google.com/docs/auth) +- [Google Sign-In for Flutter](https://pub.dev/packages/google_sign_in) +- [Firebase Auth Flutter Plugin](https://pub.dev/packages/firebase_auth) +- [Clean Architecture in Flutter](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) diff --git a/docs/login-implementation-guide.md b/docs/login-implementation-guide.md new file mode 100644 index 00000000..0ce7b66a --- /dev/null +++ b/docs/login-implementation-guide.md @@ -0,0 +1,839 @@ +# Implementing Login Functionality with Google Sign-In + +This guide provides a comprehensive approach to implementing authentication with Google Sign-In in your multichoice app, following the existing clean architecture patterns. + +## Table of Contents + +1. [Overview](#overview) +2. [Current Architecture Analysis](#current-architecture-analysis) +3. [Prerequisites](#prerequisites) +4. [Implementation Steps](#implementation-steps) +5. [Firebase Configuration](#firebase-configuration) +6. [Code Implementation](#code-implementation) +7. [Testing](#testing) +8. [Security Considerations](#security-considerations) +9. [Troubleshooting](#troubleshooting) + +## Overview + +Your app already has a solid foundation for authentication with: +- Firebase Core integration +- Clean architecture with dependency injection (GetIt + Injectable) +- BLoC pattern for state management +- Existing Session service for token management +- Firebase project configured (`multichoice-412309`) + +This implementation will extend the existing architecture to support Google Sign-In while maintaining the established patterns. + +## Current Architecture Analysis + +### Existing Components + +- **Session Service**: Basic token storage in SharedPreferences +- **Dependency Injection**: GetIt with Injectable for service management +- **Firebase Integration**: Core Firebase already configured +- **Clean Architecture**: Separation of concerns with interfaces and implementations + +### What We'll Add + +- Firebase Authentication +- Google Sign-In integration +- Enhanced user management +- Authentication state management with BLoC +- User profile data models + +## Prerequisites + +### 1. Firebase Console Setup + +1. Go to [Firebase Console](https://console.firebase.google.com/) +2. Select your project: `multichoice-412309` +3. Enable Authentication: + - Go to Authentication โ†’ Sign-in method + - Enable Google Sign-in + - Add your app's SHA-1 fingerprint for Android + +### 2. Dependencies to Add + +Add these to `apps/multichoice/pubspec.yaml`: +```yaml +dependencies: + firebase_auth: ^5.3.3 + google_sign_in: ^6.2.1 + # ... existing dependencies +``` + +Add these to `packages/core/pubspec.yaml`: +```yaml +dependencies: + firebase_auth: ^5.3.3 + # ... existing dependencies +``` + +### 3. Android Configuration + +Update `apps/multichoice/android/app/build.gradle`: +```gradle +android { + defaultConfig { + // ... existing config + multiDexEnabled true + } +} + +dependencies { + // ... existing dependencies + implementation 'com.google.android.gms:play-services-auth:20.7.0' +} +``` + +### 4. iOS Configuration + +Update `apps/multichoice/ios/Runner/Info.plist`: +```xml +CFBundleURLTypes + + + CFBundleURLName + REVERSED_CLIENT_ID + CFBundleURLSchemes + + com.googleusercontent.apps.YOUR_CLIENT_ID + + + +``` + +## Implementation Steps + +### Step 1: Create User Models + +Create `packages/models/lib/src/models/user_model.dart`: +```dart +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:isar/isar.dart'; + +part 'user_model.freezed.dart'; +part 'user_model.g.dart'; + +@freezed +@collection +class UserModel with _$UserModel { + const factory UserModel({ + required String id, + required String email, + String? displayName, + String? photoURL, + required DateTime createdAt, + required DateTime updatedAt, + }) = _UserModel; + + factory UserModel.fromJson(Map json) => + _$UserModelFromJson(json); +} +``` + +Create `packages/models/lib/src/dto/user_dto.dart`: +```dart +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'user_dto.freezed.dart'; +part 'user_dto.g.dart'; + +@freezed +class UserDTO with _$UserDTO { + const factory UserDTO({ + required String id, + required String email, + String? displayName, + String? photoURL, + required DateTime createdAt, + required DateTime updatedAt, + }) = _UserDTO; + + factory UserDTO.fromJson(Map json) => + _$UserDTOFromJson(json); +} +``` + +### Step 2: Create Authentication Service Interface + +Create `packages/core/lib/src/services/interfaces/i_auth_service.dart`: +```dart +import 'package:dartz/dartz.dart'; +import 'package:models/models.dart'; + +abstract class AuthException implements Exception { + final String message; + AuthException(this.message); + + @override + String toString() => message; +} + +abstract class IAuthService { + Future> signInWithGoogle(); + Future> signOut(); + Future> getCurrentUser(); + Stream get authStateChanges; +} +``` + +### Step 3: Create Authentication Service Implementation + +Create `packages/core/lib/src/services/implementations/auth_service.dart`: +```dart +import 'package:dartz/dartz.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:google_sign_in/google_sign_in.dart'; +import 'package:injectable/injectable.dart'; +import 'package:models/models.dart'; +import 'package:core/src/services/interfaces/i_auth_service.dart'; + +@LazySingleton(as: IAuthService) +class AuthService implements IAuthService { + final FirebaseAuth _firebaseAuth; + final GoogleSignIn _googleSignIn; + + AuthService(this._firebaseAuth, this._googleSignIn); + + @override + Future> signInWithGoogle() async { + try { + final GoogleSignInAccount? googleUser = await _googleSignIn.signIn(); + if (googleUser == null) { + return Left(AuthException('Google sign-in was cancelled')); + } + + final GoogleSignInAuthentication googleAuth = + await googleUser.authentication; + + final credential = GoogleAuthProvider.credential( + accessToken: googleAuth.accessToken, + idToken: googleAuth.idToken, + ); + + final UserCredential userCredential = + await _firebaseAuth.signInWithCredential(credential); + + final user = userCredential.user; + if (user == null) { + return Left(AuthException('Failed to sign in with Google')); + } + + final userDTO = UserDTO( + id: user.uid, + email: user.email ?? '', + displayName: user.displayName, + photoURL: user.photoURL, + createdAt: user.metadata.creationTime ?? DateTime.now(), + updatedAt: user.metadata.lastSignInTime ?? DateTime.now(), + ); + + return Right(userDTO); + } catch (e) { + return Left(AuthException('Google sign-in failed: $e')); + } + } + + @override + Future> signOut() async { + try { + await Future.wait([ + _firebaseAuth.signOut(), + _googleSignIn.signOut(), + ]); + return const Right(null); + } catch (e) { + return Left(AuthException('Sign out failed: $e')); + } + } + + @override + Future> getCurrentUser() async { + try { + final user = _firebaseAuth.currentUser; + if (user == null) { + return const Right(null); + } + + final userDTO = UserDTO( + id: user.uid, + email: user.email ?? '', + displayName: user.displayName, + photoURL: user.photoURL, + createdAt: user.metadata.creationTime ?? DateTime.now(), + updatedAt: user.metadata.lastSignInTime ?? DateTime.now(), + ); + + return Right(userDTO); + } catch (e) { + return Left(AuthException('Failed to get current user: $e')); + } + } + + @override + Stream get authStateChanges { + return _firebaseAuth.authStateChanges().map((user) { + if (user == null) return null; + + return UserDTO( + id: user.uid, + email: user.email ?? '', + displayName: user.displayName, + photoURL: user.photoURL, + createdAt: user.metadata.creationTime ?? DateTime.now(), + updatedAt: user.metadata.lastSignInTime ?? DateTime.now(), + ); + }); + } +} +``` + +### Step 4: Create Authentication BLoC + +Create `packages/core/lib/src/application/auth/auth_bloc.dart`: +```dart +import 'package:bloc/bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:injectable/injectable.dart'; +import 'package:models/models.dart'; +import 'package:core/src/services/interfaces/i_auth_service.dart'; + +part 'auth_event.dart'; +part 'auth_state.dart'; +part 'auth_bloc.freezed.dart'; + +@injectable +class AuthBloc extends Bloc { + final IAuthService _authService; + + AuthBloc(this._authService) : super(const AuthState.initial()) { + on((event, emit) async { + await event.map( + signInWithGoogle: (e) async => await _onSignInWithGoogle(emit), + signOut: (e) async => await _onSignOut(emit), + checkAuthState: (e) async => await _onCheckAuthState(emit), + authStateChanged: (e) async => await _onAuthStateChanged(e, emit), + ); + }); + + // Listen to auth state changes + _authService.authStateChanges.listen((user) { + add(AuthEvent.authStateChanged(user)); + }); + } + + Future _onSignInWithGoogle(Emitter emit) async { + emit(const AuthState.loading()); + + final result = await _authService.signInWithGoogle(); + + result.fold( + (failure) => emit(AuthState.error(failure.toString())), + (user) => emit(AuthState.authenticated(user)), + ); + } + + Future _onSignOut(Emitter emit) async { + emit(const AuthState.loading()); + + final result = await _authService.signOut(); + + result.fold( + (failure) => emit(AuthState.error(failure.toString())), + (_) => emit(const AuthState.unauthenticated()), + ); + } + + Future _onCheckAuthState(Emitter emit) async { + emit(const AuthState.loading()); + + final result = await _authService.getCurrentUser(); + + result.fold( + (failure) => emit(AuthState.error(failure.toString())), + (user) => user != null + ? emit(AuthState.authenticated(user)) + : emit(const AuthState.unauthenticated()), + ); + } + + Future _onAuthStateChanged( + AuthStateChanged event, + Emitter emit, + ) async { + if (event.user != null) { + emit(AuthState.authenticated(event.user!)); + } else { + emit(const AuthState.unauthenticated()); + } + } +} +``` + +Create `packages/core/lib/src/application/auth/auth_event.dart`: +```dart +part of 'auth_bloc.dart'; + +@freezed +class AuthEvent with _$AuthEvent { + const factory AuthEvent.signInWithGoogle() = SignInWithGoogle; + const factory AuthEvent.signOut() = SignOut; + const factory AuthEvent.checkAuthState() = CheckAuthState; + const factory AuthEvent.authStateChanged(UserDTO? user) = AuthStateChanged; +} +``` + +Create `packages/core/lib/src/application/auth/auth_state.dart`: +```dart +part of 'auth_bloc.dart'; + +@freezed +class AuthState with _$AuthState { + const factory AuthState.initial() = Initial; + const factory AuthState.loading() = Loading; + const factory AuthState.authenticated(UserDTO user) = Authenticated; + const factory AuthState.unauthenticated() = Unauthenticated; + const factory AuthState.error(String message) = Error; +} +``` + +### Step 5: Update Dependency Injection + +Update `packages/core/lib/src/injectable_module.dart`: +```dart +import 'package:file_picker/file_picker.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:google_sign_in/google_sign_in.dart'; +import 'package:injectable/injectable.dart'; +import 'package:isar/isar.dart'; +import 'package:models/models.dart' show TabsSchema, EntrySchema, UserSchema; +import 'package:cloud_firestore/cloud_firestore.dart' show FirebaseFirestore; +import 'package:path_provider/path_provider.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +@module +abstract class InjectableModule { + @preResolve + Future get isar async { + final directory = await getApplicationDocumentsDirectory(); + final isar = await Isar.open( + [ + TabsSchema, + EntrySchema, + UserSchema, // Add UserSchema + ], + directory: directory.path, + name: 'MultichoiceDB', + ); + + return isar; + } + + @preResolve + Future get sharedPref async { + final sharedPref = await SharedPreferences.getInstance(); + return sharedPref; + } + + @lazySingleton + FilePicker get filePicker => FilePicker.platform; + + @lazySingleton + FirebaseFirestore get firestore => FirebaseFirestore.instance; + + @lazySingleton + FirebaseAuth get firebaseAuth => FirebaseAuth.instance; + + @lazySingleton + GoogleSignIn get googleSignIn => GoogleSignIn(); +} +``` + +### Step 6: Create Login UI + +Create `apps/multichoice/lib/presentation/auth/login_page.dart`: +```dart +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:core/core.dart'; +import 'package:theme/theme.dart'; + +class LoginPage extends StatelessWidget { + const LoginPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: BlocListener( + listener: (context, state) { + state.mapOrNull( + error: (error) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(error.message), + backgroundColor: Colors.red, + ), + ); + }, + ); + }, + child: SafeArea( + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // App Logo/Title + const Icon( + Icons.apps, + size: 80, + color: Colors.blue, + ), + const SizedBox(height: 24), + Text( + 'Welcome to Multichoice', + style: context.textTheme.headlineMedium, + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + Text( + 'Sign in to continue', + style: context.textTheme.bodyLarge?.copyWith( + color: Colors.grey[600], + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 48), + + // Google Sign-In Button + BlocBuilder( + builder: (context, state) { + return SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: state.maybeWhen( + loading: () => null, + orElse: () => () { + context.read().add( + const AuthEvent.signInWithGoogle(), + ); + }, + ), + icon: state.maybeWhen( + loading: () => const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ), + orElse: () => const Icon(Icons.login), + ), + label: Text( + state.maybeWhen( + loading: () => 'Signing in...', + orElse: () => 'Sign in with Google', + ), + ), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + backgroundColor: Colors.white, + foregroundColor: Colors.black87, + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + ), + ); + }, + ), + ], + ), + ), + ), + ), + ); + } +} +``` + +### Step 7: Create Authentication Wrapper + +Create `apps/multichoice/lib/presentation/auth/auth_wrapper.dart`: +```dart +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:core/core.dart'; +import 'package:multichoice/presentation/auth/login_page.dart'; +import 'package:multichoice/layouts/home_layout/home_layout.dart'; + +class AuthWrapper extends StatelessWidget { + const AuthWrapper({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return state.map( + initial: (_) => const Scaffold( + body: Center(child: CircularProgressIndicator()), + ), + loading: (_) => const Scaffold( + body: Center(child: CircularProgressIndicator()), + ), + authenticated: (_) => const HomeLayout(), + unauthenticated: (_) => const LoginPage(), + error: (error) => Scaffold( + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('Error: ${error.message}'), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () { + context.read().add( + const AuthEvent.checkAuthState(), + ); + }, + child: const Text('Retry'), + ), + ], + ), + ), + ), + ); + }, + ); + } +} +``` + +### Step 8: Update Main App + +Update `apps/multichoice/lib/main.dart`: +```dart +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:core/core.dart'; +import 'package:multichoice/bootstrap.dart'; +import 'package:multichoice/presentation/auth/auth_wrapper.dart'; + +void main() async { + await bootstrap(); + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => coreSl() + ..add(const AuthEvent.checkAuthState()), + ), + // Add other BLoCs as needed + ], + child: MaterialApp( + title: 'Multichoice', + theme: ThemeData( + primarySwatch: Colors.blue, + useMaterial3: true, + ), + home: const AuthWrapper(), + ), + ); + } +} +``` + +### Step 9: Add Sign Out to Drawer + +Update your existing drawer to include sign out functionality: +```dart +// In your drawer widget +ListTile( + leading: const Icon(Icons.logout), + title: const Text('Sign Out'), + onTap: () { + context.read().add(const AuthEvent.signOut()); + }, +), +``` + +## Firebase Configuration + +### 1. Enable Google Sign-In in Firebase Console + +1. Go to Firebase Console โ†’ Authentication โ†’ Sign-in method +2. Enable Google provider +3. Add your app's SHA-1 fingerprint for Android + +### 2. Get SHA-1 Fingerprint + +```bash +cd apps/multichoice/android +./gradlew signingReport +``` + +### 3. Update google-services.json + +Download the updated `google-services.json` from Firebase Console and replace the existing one in `apps/multichoice/android/app/`. + +## Testing + +### 1. Unit Tests + +Create `packages/core/test/src/services/auth_service_test.dart`: +```dart +import 'package:bloc_test/bloc_test.dart'; +import 'package:core/core.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:models/models.dart'; + +import '../../../mocks.mocks.dart'; + +void main() { + group('AuthBloc', () { + late AuthBloc authBloc; + late MockIAuthService mockAuthService; + + setUp(() { + mockAuthService = MockIAuthService(); + authBloc = AuthBloc(mockAuthService); + }); + + tearDown(() { + authBloc.close(); + }); + + test('initial state is Initial', () { + expect(authBloc.state, const AuthState.initial()); + }); + + blocTest( + 'emits [Loading, Authenticated] when signInWithGoogle succeeds', + build: () { + when(mockAuthService.signInWithGoogle()).thenAnswer( + (_) async => Right(UserDTO( + id: 'test-id', + email: 'test@example.com', + createdAt: DateTime.now(), + updatedAt: DateTime.now(), + )), + ); + return authBloc; + }, + act: (bloc) => bloc.add(const AuthEvent.signInWithGoogle()), + expect: () => [ + const AuthState.loading(), + isA(), + ], + ); + }); +} +``` + +### 2. Integration Tests + +Add authentication tests to your existing integration test suite. + +## Security Considerations + +### 1. Firestore Security Rules + +Update your Firestore rules to include user-based access: +```javascript +rules_version = '2'; +service cloud.firestore { + match /databases/{database}/documents { + // Allow authenticated users to read/write their own data + match /users/{userId} { + allow read, write: if request.auth != null && request.auth.uid == userId; + } + + // Allow authenticated users to read/write their own tabs and entries + match /users/{userId}/tabs/{tabId} { + allow read, write: if request.auth != null && request.auth.uid == userId; + } + + match /users/{userId}/entries/{entryId} { + allow read, write: if request.auth != null && request.auth.uid == userId; + } + + // Keep existing feedback rules + match /feedback/{feedbackId} { + allow create: if true; + allow read: if request.auth != null; + allow update, delete: if false; + } + } +} +``` + +### 2. Token Management + +- The existing Session service already handles token storage +- Consider implementing token refresh logic +- Add token validation on app startup + +### 3. Error Handling + +- Implement proper error handling for network issues +- Add retry logic for failed authentication attempts +- Handle edge cases like account deletion + +## Troubleshooting + +### Common Issues + +1. **Google Sign-In not working on Android** + - Verify SHA-1 fingerprint is correct + - Check that google-services.json is up to date + - Ensure Google Play Services is installed + +2. **iOS Sign-In issues** + - Verify REVERSED_CLIENT_ID in Info.plist + - Check that GoogleService-Info.plist is properly configured + +3. **Build errors** + - Run `melos clean` and `melos get` + - Run `melos build:runner` to generate code + - Check that all dependencies are properly added + +### Debug Commands + +```bash +# Clean and rebuild +melos clean +melos get +melos build:runner + +# Run tests +melos test:core +melos test:multichoice + +# Check Firebase configuration +firebase projects:list +firebase auth:export users.json +``` + +## Next Steps + +1. **User Profile Management**: Add user profile editing capabilities +2. **Data Synchronization**: Sync user data with Firestore +3. **Offline Support**: Implement offline-first data handling +4. **Multi-account Support**: Allow users to switch between accounts +5. **Analytics**: Add authentication events to Firebase Analytics + +## Resources + +- [Firebase Authentication Documentation](https://firebase.google.com/docs/auth) +- [Google Sign-In for Flutter](https://pub.dev/packages/google_sign_in) +- [Firebase Auth Flutter Plugin](https://pub.dev/packages/firebase_auth) +- [Clean Architecture in Flutter](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) diff --git a/docs/setting-up-dev-prod-environments.md b/docs/setting-up-dev-prod-environments.md new file mode 100644 index 00000000..aa478711 --- /dev/null +++ b/docs/setting-up-dev-prod-environments.md @@ -0,0 +1,712 @@ +# Setting Up DEV and PROD Environments + +This guide will help you create separate development and production environments so you can safely develop new features without breaking your live app on Google Play Store. + +## Table of Contents + +1. [Overview](#overview) +2. [What You'll Need](#what-youll-need) +3. [Firebase Setup](#firebase-setup) +4. [Android App Configuration](#android-app-configuration) +5. [Google Play Console Setup](#google-play-console-setup) +6. [Environment Configuration](#environment-configuration) +7. [CI/CD Pipeline Updates](#cicd-pipeline-updates) +8. [Testing Strategy](#testing-strategy) +9. [Deployment Flow](#deployment-flow) +10. [Best Practices](#best-practices) +11. [Troubleshooting](#troubleshooting) + +## Overview + +Your current setup has one app connecting to one Firebase project. We'll create: + +- **DEV Environment**: For testing new features safely +- **PROD Environment**: Your current live app (minimal changes) + +This approach allows you to: +- Test new features without affecting production users +- Use separate databases and configurations +- Deploy with confidence knowing dev testing was thorough +- Roll back easily if issues arise + +## What You'll Need + +### Firebase + +- **Current Project**: `multichoice-412309` (will become PROD) +- **New Project**: `multichoice-dev` (for development) + +### Google Play Console + +- **Current App**: Your existing production app +- **Testing Tracks**: Internal testing, Alpha, Beta tracks for dev builds + +### Development Tools + +- Separate app configurations +- Environment-specific secrets +- Updated CI/CD workflows + +## Firebase Setup + +### Step 1: Create Dev Firebase Project + +1. Go to [Firebase Console](https://console.firebase.google.com/) +2. Click "Add project" +3. Name it `multichoice-dev` +4. Follow setup wizard (enable Google Analytics if desired) + +### Step 2: Configure Dev Project Services + +Enable the same services as your production project: + +#### Authentication + +```bash +# Go to Authentication โ†’ Sign-in method +# Enable Google Sign-in +# Add SHA-1 fingerprints for dev builds +``` + +#### Firestore Database + +```bash +# Go to Firestore Database โ†’ Create database +# Choose test mode initially +# Select same region as production +``` + +#### Cloud Functions + +```bash +# Go to Functions +# Copy your production functions to dev project +``` + +#### App Distribution + +```bash +# Go to App Distribution +# This will be used for internal testing +``` + +### Step 3: Set Up Firebase Projects Structure + +``` +Firebase Projects: +โ”œโ”€โ”€ multichoice-412309 (PROD) +โ”‚ โ”œโ”€โ”€ Authentication (production users) +โ”‚ โ”œโ”€โ”€ Firestore (production data) +โ”‚ โ”œโ”€โ”€ Functions (production feedback) +โ”‚ โ””โ”€โ”€ App Distribution (release candidates) +โ””โ”€โ”€ multichoice-dev (DEV) + โ”œโ”€โ”€ Authentication (test users) + โ”œโ”€โ”€ Firestore (test data) + โ”œโ”€โ”€ Functions (development feedback) + โ””โ”€โ”€ App Distribution (dev builds) +``` + +## Android App Configuration + +### Step 1: Create Environment-Specific Configurations + +Create different build flavors in `apps/multichoice/android/app/build.gradle`: + +```gradle +android { + // ... existing config + + flavorDimensions "environment" + + productFlavors { + dev { + dimension "environment" + applicationIdSuffix ".dev" + versionNameSuffix "-dev" + resValue "string", "app_name", "Multichoice Dev" + manifestPlaceholders = [ + firebaseAppId: "your-dev-firebase-app-id" + ] + } + + prod { + dimension "environment" + resValue "string", "app_name", "Multichoice" + manifestPlaceholders = [ + firebaseAppId: "your-prod-firebase-app-id" + ] + } + } + + buildTypes { + debug { + debuggable true + applicationIdSuffix ".debug" + } + release { + minifyEnabled true + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } +} +``` + +### Step 2: Create Environment-Specific Firebase Config + +Create separate directories for Firebase configurations: + +``` +apps/multichoice/android/app/src/ +โ”œโ”€โ”€ dev/ +โ”‚ โ””โ”€โ”€ google-services.json # Dev Firebase config +โ”œโ”€โ”€ prod/ +โ”‚ โ””โ”€โ”€ google-services.json # Prod Firebase config (current) +โ””โ”€โ”€ main/ + โ””โ”€โ”€ ... (existing files) +``` + +### Step 3: Update App Icons and Names + +Create environment-specific app icons: + +``` +apps/multichoice/android/app/src/ +โ”œโ”€โ”€ dev/res/ +โ”‚ โ”œโ”€โ”€ mipmap-hdpi/ic_launcher.png # Dev icon (maybe with "DEV" badge) +โ”‚ โ”œโ”€โ”€ mipmap-mdpi/ic_launcher.png +โ”‚ โ””โ”€โ”€ ... (other densities) +โ””โ”€โ”€ prod/res/ + โ”œโ”€โ”€ mipmap-hdpi/ic_launcher.png # Production icon + โ””โ”€โ”€ ... (other densities) +``` + +### Step 4: Environment-Specific App Names + +Update `apps/multichoice/android/app/src/main/res/values/strings.xml`: + +```xml + + @string/app_name + +``` + +The actual names will come from the build flavors. + +## Google Play Console Setup + +### Option 1: Same App, Different Tracks (Recommended) + +Use your existing app with different testing tracks: + +1. **Internal Testing**: For dev builds +2. **Alpha**: For staging/RC builds +3. **Beta**: For pre-production testing +4. **Production**: For live releases + +### Option 2: Separate Apps (More Isolated) + +Create a separate app for development: + +1. Go to Google Play Console +2. Create new app: "Multichoice Dev" +3. Use different package name: `com.zander.multichoice.dev` +4. Set up completely separate from production + +**Recommendation**: Use Option 1 (same app, different tracks) as it's simpler to manage. + +## Environment Configuration + +### Step 1: Create Environment Configuration Files + +Create `apps/multichoice/lib/config/` + +``` +apps/multichoice/lib/config/ +โ”œโ”€โ”€ app_config.dart +โ”œโ”€โ”€ dev_config.dart +โ””โ”€โ”€ prod_config.dart +``` + +**app_config.dart**: +```dart +abstract class AppConfig { + static late AppConfig _instance; + + static AppConfig get instance => _instance; + + static void setInstance(AppConfig config) { + _instance = config; + } + + String get appName; + String get apiBaseUrl; + String get firebaseProjectId; + bool get debugMode; + String get environment; +} +``` + +**dev_config.dart**: +```dart +import 'app_config.dart'; + +class DevConfig implements AppConfig { + @override + String get appName => 'Multichoice Dev'; + + @override + String get apiBaseUrl => 'https://dev-api.multichoice.com'; + + @override + String get firebaseProjectId => 'multichoice-dev'; + + @override + bool get debugMode => true; + + @override + String get environment => 'development'; +} +``` + +**prod_config.dart**: +```dart +import 'app_config.dart'; + +class ProdConfig implements AppConfig { + @override + String get appName => 'Multichoice'; + + @override + String get apiBaseUrl => 'https://api.multichoice.com'; + + @override + String get firebaseProjectId => 'multichoice-412309'; + + @override + bool get debugMode => false; + + @override + String get environment => 'production'; +} +``` + +### Step 2: Update Main Entry Points + +Create separate main files: + +**apps/multichoice/lib/main_dev.dart**: +```dart +import 'package:flutter/material.dart'; +import 'package:multichoice/bootstrap.dart'; +import 'package:multichoice/config/dev_config.dart'; +import 'package:multichoice/config/app_config.dart'; + +void main() async { + AppConfig.setInstance(DevConfig()); + await bootstrap(); + runApp(const MyApp()); +} +``` + +**apps/multichoice/lib/main_prod.dart**: +```dart +import 'package:flutter/material.dart'; +import 'package:multichoice/bootstrap.dart'; +import 'package:multichoice/config/prod_config.dart'; +import 'package:multichoice/config/app_config.dart'; + +void main() async { + AppConfig.setInstance(ProdConfig()); + await bootstrap(); + runApp(const MyApp()); +} +``` + +**apps/multichoice/lib/main.dart** (update existing): +```dart +// Default to production +export 'main_prod.dart'; +``` + +### Step 3: Update Firebase Options + +Create environment-specific Firebase options: + +**apps/multichoice/lib/firebase_options_dev.dart**: +```dart +// Generated from dev Firebase project +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' + show defaultTargetPlatform, kIsWeb, TargetPlatform; + +class DefaultFirebaseOptions { + static FirebaseOptions get currentPlatform { + if (kIsWeb) { + return web; + } + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return android; + case TargetPlatform.iOS: + return ios; + default: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for this platform.', + ); + } + } + + static const FirebaseOptions web = FirebaseOptions( + apiKey: 'your-dev-web-api-key', + appId: 'your-dev-web-app-id', + messagingSenderId: 'your-dev-sender-id', + projectId: 'multichoice-dev', + // ... other dev config + ); + + static const FirebaseOptions android = FirebaseOptions( + apiKey: 'your-dev-android-api-key', + appId: 'your-dev-android-app-id', + messagingSenderId: 'your-dev-sender-id', + projectId: 'multichoice-dev', + // ... other dev config + ); + + // ... iOS config +} +``` + +### Step 4: Update Bootstrap Configuration + +Update `apps/multichoice/lib/bootstrap.dart`: + +```dart +Future bootstrap() async { + WidgetsFlutterBinding.ensureInitialized(); + + // Initialize Firebase with environment-specific options + await Firebase.initializeApp( + options: AppConfig.instance.environment == 'development' + ? DevFirebaseOptions.currentPlatform + : DefaultFirebaseOptions.currentPlatform, + ); + + // Configure core dependencies + await configureCoreDependencies(); + + // ... rest of bootstrap +} +``` + +## CI/CD Pipeline Updates + +### Step 1: Update GitHub Secrets + +Add environment-specific secrets to your GitHub repository: + +```bash +# Dev Environment +DEV_ANDROID_KEYSTORE_BASE64 +DEV_ANDROID_KEYSTORE_PASSWORD +DEV_ANDROID_KEY_ALIAS +DEV_ANDROID_KEY_PASSWORD +DEV_FIREBASE_TOKEN +DEV_GOOGLE_SERVICES_JSON + +# Prod Environment (existing) +ANDROID_KEYSTORE_BASE64 +ANDROID_KEYSTORE_PASSWORD +ANDROID_KEY_ALIAS +ANDROID_KEY_PASSWORD +FIREBASE_TOKEN +GOOGLE_SERVICES_JSON +``` + +### Step 2: Create Environment-Specific Workflows + +Create `.github/workflows/dev-build.yml`: + +```yaml +name: Dev Build and Deploy + +on: + push: + branches: [ develop ] + pull_request: + branches: [ develop ] + workflow_dispatch: + +concurrency: + group: dev-build-${{ github.ref }} + cancel-in-progress: true + +jobs: + build-dev: + runs-on: ubuntu-latest + environment: development + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' + + - name: Setup Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: '3.19.0' + + - name: Get dependencies + run: | + cd apps/multichoice + flutter pub get + + - name: Run tests + run: | + cd apps/multichoice + flutter test + + - name: Create dev google-services.json + run: | + echo "${{ secrets.DEV_GOOGLE_SERVICES_JSON }}" | base64 --decode > apps/multichoice/android/app/src/dev/google-services.json + + - name: Create dev keystore + run: | + echo "${{ secrets.DEV_ANDROID_KEYSTORE_BASE64 }}" | base64 --decode > apps/multichoice/android/app/dev-keystore.jks + + - name: Create dev key.properties + run: | + cat > apps/multichoice/android/key.properties << EOF + storePassword=${{ secrets.DEV_ANDROID_KEYSTORE_PASSWORD }} + keyPassword=${{ secrets.DEV_ANDROID_KEY_PASSWORD }} + keyAlias=${{ secrets.DEV_ANDROID_KEY_ALIAS }} + storeFile=dev-keystore.jks + EOF + + - name: Build dev APK + run: | + cd apps/multichoice + flutter build apk --flavor dev --target lib/main_dev.dart + + - name: Upload to Firebase App Distribution + uses: wzieba/Firebase-Distribution-Github-Action@v1 + with: + appId: ${{ secrets.DEV_FIREBASE_APP_ID }} + serviceCredentialsFileContent: ${{ secrets.DEV_FIREBASE_SERVICE_ACCOUNT }} + groups: testers + file: apps/multichoice/build/app/outputs/flutter-apk/app-dev-release.apk +``` + +### Step 3: Update Production Workflow + +Update `.github/workflows/production-build.yml`: + +```yaml +name: Production Build and Deploy + +on: + push: + branches: [ main ] + workflow_dispatch: + +# ... (similar structure but with prod secrets and configs) + + - name: Build prod APK + run: | + cd apps/multichoice + flutter build apk --flavor prod --target lib/main_prod.dart + + - name: Build prod AAB + run: | + cd apps/multichoice + flutter build appbundle --flavor prod --target lib/main_prod.dart +``` + +## Testing Strategy + +### Development Testing + +1. **Local Testing**: Use dev flavor locally +2. **Firebase App Distribution**: Share dev builds with team +3. **Internal Testing**: Use Google Play internal track + +### Staging Testing + +1. **Alpha Track**: For broader team testing +2. **Beta Track**: For external testers +3. **Pre-production**: Final testing before prod + +### Production Deployment + +1. **Gradual Rollout**: Start with small percentage +2. **Monitor**: Check crash reports and user feedback +3. **Full Rollout**: Increase to 100% if stable + +## Deployment Flow + +``` +Feature Development โ†’ Local Dev Testing โ†’ Dev Build + Firebase Distribution โ†’ Team Testing + โ†“ +Production Build โ† Merge to main โ† Ready for Prod? โ† Alpha/Beta Testing โ† Merge to staging + โ†“ +Google Play Production +``` + +## Best Practices + +### 1. Database Separation + +- **Dev**: Use test data, can be reset frequently +- **Prod**: Real user data, handle with care + +### 2. Feature Flags + +Consider implementing feature flags: +```dart +class FeatureFlags { + static bool get newFeatureEnabled { + return AppConfig.instance.environment == 'development' || + AppConfig.instance.debugMode; + } +} +``` + +### 3. Logging and Analytics + +```dart +void logEvent(String event, Map parameters) { + if (AppConfig.instance.environment == 'development') { + print('Dev Event: $event - $parameters'); + } else { + // Send to production analytics + FirebaseAnalytics.instance.logEvent(name: event, parameters: parameters); + } +} +``` + +### 4. Error Handling + +```dart +void handleError(dynamic error, StackTrace stackTrace) { + if (AppConfig.instance.environment == 'development') { + print('Dev Error: $error\n$stackTrace'); + } else { + // Send to production crash reporting + FirebaseCrashlytics.instance.recordError(error, stackTrace); + } +} +``` + +### 5. Version Management + +- **Dev**: Use build numbers for internal tracking +- **Prod**: Follow semantic versioning strictly + +## Troubleshooting + +### Build Errors + +**Issue**: Flavor not found +```bash +# Solution: Ensure build.gradle has correct flavor configuration +flutter build apk --flavor dev --target lib/main_dev.dart +``` + +**Issue**: Firebase configuration not found +```bash +# Solution: Verify google-services.json is in correct flavor directory +apps/multichoice/android/app/src/dev/google-services.json +``` + +### Firebase Issues + +**Issue**: Wrong Firebase project +```bash +# Solution: Check firebase_options.dart has correct project ID +# Verify AppConfig is setting correct environment +``` + +**Issue**: Authentication not working +```bash +# Solution: Ensure SHA-1 fingerprints are added to both Firebase projects +# For dev builds: +cd apps/multichoice/android +./gradlew signingReport +``` + +### Google Play Issues + +**Issue**: Upload conflicts +```bash +# Solution: Ensure different version codes for different environments +# Use applicationIdSuffix for dev builds +``` + +### Environment Configuration + +**Issue**: Wrong environment detected +```bash +# Solution: Verify main_dev.dart and main_prod.dart are correctly setting config +# Check bootstrap.dart is using correct Firebase options +``` + +## Next Steps + +1. **Implement the Firebase setup** first (create dev project) +2. **Configure build flavors** in Android +3. **Test locally** with both dev and prod flavors +4. **Update CI/CD workflows** gradually +5. **Train team** on new deployment process +6. **Document** any project-specific customizations + +## Resources + +- [Firebase Project Management](https://firebase.google.com/docs/projects/learn-more) +- [Android Build Variants](https://developer.android.com/studio/build/build-variants) +- [Google Play Testing Tracks](https://support.google.com/googleplay/android-developer/answer/9845334) +- [Flutter Flavors](https://docs.flutter.dev/deployment/flavors) +- [GitHub Actions Environments](https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment) + +--- + +This setup will give you confidence to develop new features knowing your production app is protected. Start with the Firebase setup and gradually implement each section. + +graph TB + subgraph "DEV Environment" + A["Firebase Project:
multichoice-dev"] + B["App Package:
com.zander.multichoice.dev"] + C["Google Play:
Internal Testing Track"] + D["Database:
Test Data"] + end + + subgraph "PROD Environment" + E["Firebase Project:
multichoice-412309"] + F["App Package:
com.zander.multichoice"] + G["Google Play:
Production Track"] + H["Database:
Real User Data"] + end + + subgraph "Development Flow" + I["Feature Development"] --> J["Local Dev Testing"] + J --> K["Dev Build"] + K --> L["Firebase App Distribution"] + L --> M["Team Testing"] + M --> N{"Ready for
Production?"} + N -->|No| I + N -->|Yes| O["Production Build"] + O --> P["Google Play Store"] + end + + K --> A + K --> B + K --> C + K --> D + + O --> E + O --> F + O --> G + O --> H