├── .azuredevops └── pull_request_template.md ├── .github ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── conventional-commits.yml ├── .gitignore ├── .mergify.yml ├── .vscode └── launch.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── build ├── azure-pipelines.yml ├── gitversion-config.yml ├── stage-build.yml ├── stage-commit-validation.yaml ├── stage-release-appstore.yml ├── stage-release-firebase-app-distribution.yml ├── stage-release-googleplay.yml ├── stage-security-scan.yml ├── steps-build-android.yml ├── steps-build-ios.yml ├── steps-build-release-notes.yml ├── steps-build-tests.yml ├── steps-build-windows.yml ├── templates │ ├── build-number.yml │ ├── flutter-diagnostics.yml │ ├── flutter-install.yml │ ├── flutter-prepare.yml │ ├── gitversion.yml │ ├── mobsf-scan.yml │ ├── replace-firebase-config.yml │ └── validate-commits.yaml └── variables.yml ├── doc ├── Architecture.md ├── AzurePipelines.md ├── DependencyInjection.md ├── Diagnostics.md ├── Environment.md ├── FirebaseRemoteConfig.md ├── ForcedUpdate.md ├── HTTP.md ├── KillSwitch.md ├── Localization.md ├── Logging.md ├── Navigation.md ├── SecurityScan.md ├── Serialization.md ├── Testing.md └── diagrams │ ├── architecture-context.png │ ├── architecture-structure.png │ ├── architecture.drawio │ └── solution-structure.png ├── src ├── app │ ├── .env.dev │ ├── .env.prod │ ├── .env.staging │ ├── .gitignore │ ├── .metadata │ ├── README.md │ ├── analysis_options.yaml │ ├── android │ │ ├── .gitignore │ │ ├── app │ │ │ ├── build.gradle │ │ │ ├── google-services.json │ │ │ └── src │ │ │ │ └── main │ │ │ │ ├── AndroidManifest.xml │ │ │ │ ├── kotlin │ │ │ │ └── com │ │ │ │ │ └── nventive │ │ │ │ │ └── internal │ │ │ │ │ └── flutterapptemplate │ │ │ │ │ └── MainActivity.kt │ │ │ │ └── res │ │ │ │ ├── drawable-v21 │ │ │ │ └── launch_background.xml │ │ │ │ ├── drawable │ │ │ │ └── launch_background.xml │ │ │ │ ├── mipmap-hdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── values-night │ │ │ │ └── styles.xml │ │ │ │ └── values │ │ │ │ └── styles.xml │ │ ├── build.gradle │ │ ├── gradle.properties │ │ ├── gradle │ │ │ └── wrapper │ │ │ │ └── gradle-wrapper.properties │ │ └── settings.gradle │ ├── assets │ │ ├── fonts │ │ │ ├── guillon_black.ttf │ │ │ ├── guillon_bold.ttf │ │ │ ├── guillon_light.ttf │ │ │ ├── guillon_regular.ttf │ │ │ └── guillon_semibold.ttf │ │ └── openSourceSoftwareLicenses.json │ ├── firebase.json │ ├── integration_test │ │ ├── dad_jokes_page_test.dart │ │ ├── forced_update_test.dart │ │ ├── integration_test.dart │ │ └── kill_switch_test.dart │ ├── ios │ │ ├── .gitignore │ │ ├── Flutter │ │ │ ├── AppFrameworkInfo.plist │ │ │ ├── Debug.xcconfig │ │ │ └── Release.xcconfig │ │ ├── Podfile │ │ ├── Podfile.lock │ │ ├── Runner.xcodeproj │ │ │ ├── project.pbxproj │ │ │ ├── project.xcworkspace │ │ │ │ ├── contents.xcworkspacedata │ │ │ │ └── xcshareddata │ │ │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ │ │ └── WorkspaceSettings.xcsettings │ │ │ └── xcshareddata │ │ │ │ └── xcschemes │ │ │ │ └── Runner.xcscheme │ │ ├── Runner.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ │ └── WorkspaceSettings.xcsettings │ │ ├── Runner │ │ │ ├── AppDelegate.swift │ │ │ ├── Assets.xcassets │ │ │ │ ├── AppIcon.appiconset │ │ │ │ │ ├── Contents.json │ │ │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ │ │ ├── Icon-App-20x20@1x.png │ │ │ │ │ ├── Icon-App-20x20@2x.png │ │ │ │ │ ├── Icon-App-20x20@3x.png │ │ │ │ │ ├── Icon-App-29x29@1x.png │ │ │ │ │ ├── Icon-App-29x29@2x.png │ │ │ │ │ ├── Icon-App-29x29@3x.png │ │ │ │ │ ├── Icon-App-40x40@1x.png │ │ │ │ │ ├── Icon-App-40x40@2x.png │ │ │ │ │ ├── Icon-App-40x40@3x.png │ │ │ │ │ ├── Icon-App-60x60@2x.png │ │ │ │ │ ├── Icon-App-60x60@3x.png │ │ │ │ │ ├── Icon-App-76x76@1x.png │ │ │ │ │ ├── Icon-App-76x76@2x.png │ │ │ │ │ └── Icon-App-83.5x83.5@2x.png │ │ │ │ └── LaunchImage.imageset │ │ │ │ │ ├── Contents.json │ │ │ │ │ ├── LaunchImage.png │ │ │ │ │ ├── LaunchImage@2x.png │ │ │ │ │ ├── LaunchImage@3x.png │ │ │ │ │ └── README.md │ │ │ ├── Base.lproj │ │ │ │ ├── LaunchScreen.storyboard │ │ │ │ └── Main.storyboard │ │ │ ├── Info.plist │ │ │ └── Runner-Bridging-Header.h │ │ └── RunnerTests │ │ │ └── RunnerTests.swift │ ├── l10n.yaml │ ├── lib │ │ ├── access │ │ │ ├── bugsee │ │ │ │ ├── bugsee_configuration_data.dart │ │ │ │ └── bugsee_repository.dart │ │ │ ├── dad_jokes │ │ │ │ ├── dad_jokes_mocked_repository.dart │ │ │ │ ├── dad_jokes_repository.dart │ │ │ │ ├── data │ │ │ │ │ ├── dad_joke_child_data.dart │ │ │ │ │ ├── dad_joke_content_data.dart │ │ │ │ │ ├── dad_joke_data.dart │ │ │ │ │ └── dad_joke_response_data.dart │ │ │ │ ├── favorite_dad_jokes_mocked_repository.dart │ │ │ │ ├── favorite_dad_jokes_repository.dart │ │ │ │ └── mocks │ │ │ │ │ ├── dad_jokes_data_mock.dart │ │ │ │ │ └── dad_jokes_list_mock.dart │ │ │ ├── diagnostics │ │ │ │ └── diagnostics_repository.dart │ │ │ ├── environment │ │ │ │ └── environment_repository.dart │ │ │ ├── firebase │ │ │ │ └── firebase_repository.dart │ │ │ ├── forced_update │ │ │ │ ├── current_version_repository.dart │ │ │ │ ├── data │ │ │ │ │ └── version.dart │ │ │ │ ├── minimum_version_repository.dart │ │ │ │ └── minimum_version_repository_mock.dart │ │ │ ├── kill_switch │ │ │ │ ├── kill_switch_repository.dart │ │ │ │ └── kill_switch_repository_mock.dart │ │ │ ├── logger │ │ │ │ ├── alice_output.dart │ │ │ │ ├── custom_console_output.dart │ │ │ │ ├── custom_file_output.dart │ │ │ │ ├── logger_repository.dart │ │ │ │ └── logging_configuration_data.dart │ │ │ ├── mocking │ │ │ │ └── mocking_repository.dart │ │ │ └── persistence_exception.dart │ │ ├── app.dart │ │ ├── app_router.dart │ │ ├── app_shell.dart │ │ ├── business │ │ │ ├── bugsee │ │ │ │ └── bugsee_manager.dart │ │ │ ├── dad_jokes │ │ │ │ ├── dad_joke.dart │ │ │ │ └── dad_jokes_service.dart │ │ │ ├── diagnostics │ │ │ │ └── diagnostics_service.dart │ │ │ ├── environment │ │ │ │ ├── environment.dart │ │ │ │ └── environment_manager.dart │ │ │ ├── forced_update │ │ │ │ └── update_required_service.dart │ │ │ ├── kill_switch │ │ │ │ └── kill_switch_service.dart │ │ │ ├── logger │ │ │ │ ├── level_log_filter.dart │ │ │ │ └── logger_manager.dart │ │ │ └── mocking │ │ │ │ └── mocking_manager.dart │ │ ├── firebase_options.dart │ │ ├── l10n │ │ │ ├── arb │ │ │ │ ├── app_en.arb │ │ │ │ └── app_fr.arb │ │ │ └── localization_extensions.dart │ │ ├── main.dart │ │ ├── presentation │ │ │ ├── dad_jokes │ │ │ │ ├── dad_joke_list_item.dart │ │ │ │ ├── dad_jokes_page.dart │ │ │ │ ├── dad_jokes_page_viewmodel.dart │ │ │ │ ├── favorite_dad_jokes.dart │ │ │ │ └── favorite_dad_jokes_viewmodel.dart │ │ │ ├── diagnostic │ │ │ │ ├── bugsee_configuration_widget.dart │ │ │ │ ├── device_info_widget.dart │ │ │ │ ├── diagnostic_button.dart │ │ │ │ ├── diagnostic_overlay.dart │ │ │ │ ├── diagnostic_switch.dart │ │ │ │ ├── diagnostic_text.dart │ │ │ │ ├── environment_diagnostic_widget.dart │ │ │ │ ├── environment_picker_widget.dart │ │ │ │ ├── expanded_diagnostic_page.dart │ │ │ │ ├── logger_diagnostic_widget.dart │ │ │ │ ├── logging_configuration_widget.dart │ │ │ │ ├── mocking_configuration_widget.dart │ │ │ │ ├── mocking_diagnostic_widget.dart │ │ │ │ ├── navigation_diagnostic_widget.dart │ │ │ │ └── selectable_diagnostic_button.dart │ │ │ ├── forced_update │ │ │ │ └── forced_update_page.dart │ │ │ ├── kill_switch │ │ │ │ └── kill_switch_page.dart │ │ │ ├── mvvm │ │ │ │ ├── mvvm_widget.dart │ │ │ │ └── view_model.dart │ │ │ └── styles │ │ │ │ └── global_theme_data.dart │ │ └── shell.dart │ ├── linux │ │ ├── .gitignore │ │ ├── CMakeLists.txt │ │ ├── flutter │ │ │ └── CMakeLists.txt │ │ ├── main.cc │ │ ├── my_application.cc │ │ └── my_application.h │ ├── macos │ │ ├── .gitignore │ │ ├── Flutter │ │ │ ├── Flutter-Debug.xcconfig │ │ │ └── Flutter-Release.xcconfig │ │ ├── Podfile │ │ ├── Podfile.lock │ │ ├── Runner.xcodeproj │ │ │ ├── project.pbxproj │ │ │ ├── project.xcworkspace │ │ │ │ └── xcshareddata │ │ │ │ │ └── IDEWorkspaceChecks.plist │ │ │ └── xcshareddata │ │ │ │ └── xcschemes │ │ │ │ └── Runner.xcscheme │ │ ├── Runner.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ └── IDEWorkspaceChecks.plist │ │ ├── Runner │ │ │ ├── AppDelegate.swift │ │ │ ├── Assets.xcassets │ │ │ │ └── AppIcon.appiconset │ │ │ │ │ ├── Contents.json │ │ │ │ │ ├── app_icon_1024.png │ │ │ │ │ ├── app_icon_128.png │ │ │ │ │ ├── app_icon_16.png │ │ │ │ │ ├── app_icon_256.png │ │ │ │ │ ├── app_icon_32.png │ │ │ │ │ ├── app_icon_512.png │ │ │ │ │ └── app_icon_64.png │ │ │ ├── Base.lproj │ │ │ │ └── MainMenu.xib │ │ │ ├── Configs │ │ │ │ ├── AppInfo.xcconfig │ │ │ │ ├── Debug.xcconfig │ │ │ │ ├── Release.xcconfig │ │ │ │ └── Warnings.xcconfig │ │ │ ├── DebugProfile.entitlements │ │ │ ├── Info.plist │ │ │ ├── MainFlutterWindow.swift │ │ │ └── Release.entitlements │ │ └── RunnerTests │ │ │ └── RunnerTests.swift │ ├── package_rename_config.yaml │ ├── pubspec.lock │ ├── pubspec.yaml │ ├── test │ │ ├── app_test.dart │ │ └── business │ │ │ ├── dad_jokes_service_test.dart │ │ │ └── update_required_service_test.dart │ ├── web │ │ ├── favicon.png │ │ ├── icons │ │ │ ├── Icon-192.png │ │ │ ├── Icon-512.png │ │ │ ├── Icon-maskable-192.png │ │ │ └── Icon-maskable-512.png │ │ ├── index.html │ │ └── manifest.json │ └── windows │ │ ├── .gitignore │ │ ├── CMakeLists.txt │ │ ├── flutter │ │ └── CMakeLists.txt │ │ └── runner │ │ ├── CMakeLists.txt │ │ ├── Runner.rc │ │ ├── flutter_window.cpp │ │ ├── flutter_window.h │ │ ├── main.cpp │ │ ├── resource.h │ │ ├── resources │ │ └── app_icon.ico │ │ ├── runner.exe.manifest │ │ ├── utils.cpp │ │ ├── utils.h │ │ ├── win32_window.cpp │ │ └── win32_window.h ├── cli │ ├── .azuredevops │ │ ├── gitversion-config.yml │ │ ├── stage-calculate-pubdev-score.yml │ │ ├── stage-publish-cli.yml │ │ ├── stage-test-cli.yaml │ │ └── templates │ │ │ ├── calculate-devscore.yml │ │ │ ├── prepare-package.yml │ │ │ └── publish-package.yml │ ├── .gitignore │ ├── CHANGELOG.md │ ├── LICENSE │ ├── analysis_options.yaml │ ├── bin │ │ └── flutter_application_generator.dart │ ├── build.yaml │ ├── doc │ │ └── Cli.md │ ├── example │ │ └── README.md │ ├── lib │ │ ├── flutter_application_generator.dart │ │ └── src │ │ │ ├── command_runner.dart │ │ │ └── commands │ │ │ ├── create_command.dart │ │ │ └── update_command.dart │ └── pubspec.yaml └── mvvm_lints │ ├── CHANGELOG.md │ ├── README.md │ ├── analysis_options.yaml │ ├── lib │ └── mvvm_lints.dart │ └── pubspec.yaml └── tools └── generateOpenSourceSoftwareLicenses.ps1 /.azuredevops/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Proposed Changes 2 | What kind of change does this pull request introduce? 3 | 4 | [comment]:# (Please check the ones that apply.) 5 | 6 | - [ ] Feature 7 | - [ ] Bugfix 8 | - [ ] Code style update (formatting) 9 | - [ ] Refactoring (no functional changes, no api changes) 10 | - [ ] Pipeline related changes 11 | - [ ] Documentation content changes 12 | - [ ] Other... Please describe: 13 | 14 | ## Description 15 | [comment]:# (Please describe the changes that this PR introduces.) 16 | 17 | ## Checklist 18 | This pull request fulfills the following requirements: 19 | 20 | [comment]:# (Please strikethrough non-applicable items \(https://docs.microsoft.com/en-us/azure/devops/project/wiki/markdown-guidance?view=azure-devops#emphasis-bold-italics-strikethrough\)) 21 | 22 | - [ ] Code documentation was updated. 23 | - [ ] Tested on all relevant platforms. 24 | - [ ] Automated tests were updated. 25 | - [ ] Proper work items are linked to this PR. 26 | - Tasks 27 | - Bug (if this is the last PR for that Bug) 28 | - DoD task (if this is the last PR of the PBI) 29 | - [ ] Documentation files were updated according with the changes. 30 | - Update `README.md` if you made changes to major features. 31 | 32 | [comment]:# (Please provide any additional information if necessary) 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Expected Behavior 2 | 3 | 4 | ## Actual Behavior 5 | 6 | 7 | ## Steps to Reproduce the Problem 8 | 9 | 1. 10 | 2. 11 | 3. 12 | 13 | ## Specifications 14 | 15 | - Version: 16 | - Platform: 17 | - Subsystem: -------------------------------------------------------------------------------- /.github/workflows/conventional-commits.yml: -------------------------------------------------------------------------------- 1 | name: Conventional Commits 2 | 3 | on: [pull_request] 4 | 5 | env: 6 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 7 | 8 | jobs: 9 | validate-commits: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Check out code into the Go module directory 13 | uses: actions/checkout@v1 14 | - name: Commitsar check 15 | uses: docker://aevea/commitsar 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Files and directories created by pub. 2 | .dart_tool/ 3 | .packages 4 | pubspec.lock 5 | 6 | # Files generated during tests. 7 | .test_coverage.dart 8 | coverage/ 9 | .test_runner.dart 10 | 11 | # Android studio and IntelliJ. 12 | .idea 13 | 14 | # Generated Dart files. 15 | *.g.dart 16 | 17 | # Exclusion for the version file generated by the build_version package, 18 | # otherwise CLI template generation will fail. 19 | !src/cli/lib/src/version.g.dart 20 | 21 | # Mockito generated files. 22 | *.mocks.dart 23 | -------------------------------------------------------------------------------- /.mergify.yml: -------------------------------------------------------------------------------- 1 | queue_rules: 2 | - name: default 3 | conditions: 4 | # Conditions to get out of the queue (= merged) 5 | - check-success=nventive.FlutterApplicationTemplate # Replace this with your CI pipeline name 6 | 7 | pull_request_rules: 8 | 9 | - name: automatic strict merge when CI passes, has 2 reviews, no requests for change and is labeled 'ready-to-merge' unless labelled 'do-not-merge/breaking-change' or 'do-not-merge/work-in-progress' 10 | conditions: 11 | # Only pull-requests sent to the main branch 12 | - base=main 13 | 14 | # All Azure builds should be green: 15 | - status-success=nventive.FlutterApplicationTemplate # Replace this with your CI pipeline name 16 | 17 | # CLA check must pass: 18 | #- "status-success=license/cla" 19 | 20 | # Note that this only matches people with write / admin access to the repo, 21 | # see 22 | - "#approved-reviews-by>=2" 23 | - "#changes-requested-reviews-by=0" 24 | 25 | # Pull-request must be labeled with: 26 | - label=ready-to-merge 27 | 28 | # Do not automatically merge pull-requests that are labelled as do-not-merge 29 | # see 30 | - label!=do-not-merge/breaking-change 31 | - label!=do-not-merge/work-in-progress 32 | 33 | # Note: mergify cannot break branch protection rules 34 | actions: 35 | queue: 36 | method: merge 37 | name: default 38 | 39 | - name: automatic merge for allcontributors pull requests 40 | conditions: 41 | - author=allcontributors[bot] 42 | actions: 43 | merge: 44 | method: merge 45 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Flutter Application Generator", 9 | "program": "src/cli/bin/flutter_application_generator.dart", 10 | "request": "launch", 11 | "type": "dart" 12 | }, 13 | { 14 | "name": "ApplicationTemplate - Debug", 15 | "request": "launch", 16 | "type": "dart", 17 | "program": "src/app/lib/main.dart", 18 | "toolArgs": [ 19 | "--dart-define", 20 | "ENV=Development" 21 | ] 22 | }, 23 | { 24 | "name": "ApplicationTemplate - Staging", 25 | "request": "launch", 26 | "type": "dart", 27 | "flutterMode": "release", 28 | "program": "src/app/lib/main.dart", 29 | "toolArgs": [ 30 | "--dart-define", 31 | "ENV=Staging", 32 | "--dart-define", 33 | "BUGSEE_TOKEN=" 34 | ] 35 | }, 36 | { 37 | "name": "ApplicationTemplate - Production", 38 | "request": "launch", 39 | "type": "dart", 40 | "flutterMode": "release", 41 | "program": "src/app/lib/main.dart", 42 | "toolArgs": [ 43 | "--dart-define", 44 | "ENV=Production" 45 | ] 46 | }, 47 | { 48 | "name": "ApplicationTemplate - Integration Tests", 49 | "type": "dart", 50 | "request": "launch", 51 | "flutterMode": "debug", 52 | "program": "src/app/integration_test/integration_test.dart", 53 | "toolArgs": [ 54 | "--dart-define", 55 | "ENV=Development" 56 | ] 57 | }, 58 | { 59 | "name": "ApplicationTemplate - Unit Tests", 60 | "type": "dart", 61 | "request": "launch", 62 | "flutterMode": "debug", 63 | "program": "src/app/test/", 64 | "toolArgs": [ 65 | "--dart-define", 66 | "ENV=Development" 67 | ] 68 | } 69 | ] 70 | } -------------------------------------------------------------------------------- /build/gitversion-config.yml: -------------------------------------------------------------------------------- 1 | assembly-versioning-scheme: MajorMinorPatch 2 | mode: ContinuousDeployment 3 | next-version: 1.0.0 4 | continuous-delivery-fallback-tag: "" 5 | branches: 6 | main: 7 | regex: ^master$|^main$ 8 | tag: dev 9 | increment: none 10 | ignore: 11 | sha: [] -------------------------------------------------------------------------------- /build/stage-commit-validation.yaml: -------------------------------------------------------------------------------- 1 | # This stage is responsible for running the template to validate the commits of the PR 2 | jobs: 3 | - job: OnWindows_ValidateCommits 4 | pool: 5 | vmImage : $(windowsHostedAgentImage) 6 | steps: 7 | - template: templates/validate-commits.yaml -------------------------------------------------------------------------------- /build/stage-release-appstore.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | applicationEnvironment: '' # e.g. "Staging", "Production" 3 | deploymentEnvironment: '' # e.g. "GooglePlay", "AppStore", "Firebase App Distribution" 4 | iosVariableGroup: '' 5 | ServiceConnection: '' 6 | 7 | jobs: 8 | - deployment: AppStore_iOS_${{ parameters.deploymentEnvironment}} 9 | 10 | pool: 11 | vmImage: $(macOSHostedAgentImage) 12 | 13 | environment: ${{ parameters.deploymentEnvironment }} 14 | 15 | variables: 16 | - group: ${{ parameters.iosVariableGroup }} 17 | - name: artifactName 18 | value: $(iOSArtifactName)_${{ parameters.applicationEnvironment }} 19 | 20 | strategy: 21 | runOnce: 22 | deploy: 23 | steps: 24 | - download: current 25 | artifact: $(artifactName) 26 | 27 | - task: AppStoreRelease@1 28 | displayName: 'Publish to AppStore' 29 | inputs: 30 | serviceEndpoint: $(AppStoreServiceConnection) 31 | appIdentifier: $(ApplicationIdentifier) 32 | appType: 'iOS' 33 | ipaPath: '$(Pipeline.Workspace)/$(artifactName)/*.ipa' 34 | releaseTrack: 'TestFlight' 35 | shouldSkipWaitingForProcessing: true 36 | teamId: $(AppleTeamId) 37 | teamName: $(AppleTeamName) 38 | appSpecificPassword: '$(AppleAppSpecificPasswordFastLane)' 39 | appSpecificId: '$(AppleStoreConnectIdentifier)' # Allows to bypass 2FA when specified with 'appSpecificPassword'. 40 | 41 | - task: DeleteFiles@1 42 | displayName: "Remove downloaded artifacts" 43 | condition: always() 44 | inputs: 45 | SourceFolder: $(Pipeline.Workspace)/$(artifactName) 46 | RemoveSourceFolder: True 47 | Contents: '**' -------------------------------------------------------------------------------- /build/stage-release-firebase-app-distribution.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | applicationEnvironment: '' # e.g. "Staging", "Production" 3 | deploymentEnvironment: '' # e.g. "GooglePlay", "AppStore", "Firebase App Distribution" 4 | androidVariableGroup: '' 5 | artifactFileName: '' 6 | firebaseAppDistributionJson: "" 7 | 8 | jobs: 9 | - deployment: Firebase_Android 10 | pool: 11 | vmImage: $(windowsHostedAgentImage) 12 | variables: 13 | - group: ${{ parameters.androidVariableGroup }} 14 | - name: pathToBinary 15 | value: '$(Pipeline.Workspace)/$(AndroidArtifactName)_${{ parameters.applicationEnvironment }}/${{ parameters.artifactFileName }}' 16 | environment: ${{ parameters.deploymentEnvironment }} 17 | strategy: 18 | runOnce: 19 | deploy: 20 | steps: 21 | # Step 1: Install Firebase tools 22 | - script: "npm install -g firebase-tools" 23 | displayName: "Install Firebase Tools" 24 | 25 | # Step 2: Download the service connection key file 26 | - task: DownloadSecureFile@1 27 | inputs: 28 | secureFile: ${{ parameters.firebaseAppDistributionJson }} 29 | name: DistributionServiceConnection 30 | displayName: "Download Firebase Service Connection" 31 | 32 | # Step 3: Deploy to Firebase App Distribution 33 | - script: | 34 | echo "Deploying to Firebase App Distribution..." 35 | echo "App ID: $(FirebaseAppId)" 36 | echo "Path to binary: $(pathToBinary)" 37 | echo "Path with backslash: $pathWithBackslash" 38 | firebase appdistribution:distribute $(pathToBinary) --app $(FirebaseAppId) --release-notes "FlutterApplicationTemplate" --groups "nventive" --debug 39 | env: 40 | GOOGLE_APPLICATION_CREDENTIALS: $(DistributionServiceConnection.secureFilePath) 41 | displayName: "Deploy to Firebase" -------------------------------------------------------------------------------- /build/stage-release-googleplay.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | applicationEnvironment: '' # e.g. "Staging", "Production" 3 | deploymentEnvironment: '' # e.g. "GooglePlay", "AppStore", "Firebase App Distribution" 4 | 5 | jobs: 6 | - deployment: GooglePlay_Android 7 | 8 | pool: 9 | vmImage: $(windowsHostedAgentImage) 10 | 11 | environment: ${{ parameters.deploymentEnvironment }} 12 | 13 | variables: 14 | - group: FlutterApplicationTemplate.Distribution.GooglePlay 15 | - name: artifactName 16 | value: $(AndroidArtifactName)_${{ parameters.applicationEnvironment }} 17 | 18 | strategy: 19 | runOnce: 20 | deploy: 21 | steps: 22 | - download: current 23 | artifact: $(artifactName) 24 | 25 | - task: GooglePlayRelease@4 26 | displayName: 'Release AAB' 27 | inputs: 28 | serviceConnection: $(GooglePlayServiceConnection) 29 | applicationId: '$(ApplicationIdentifier)' 30 | action: 'SingleBundle' 31 | bundleFile: '$(Pipeline.Workspace)/$(artifactName)/app-release.aab' 32 | track: 'internal' 33 | changesNotSentForReview: true 34 | 35 | - task: DeleteFiles@1 36 | displayName: "Remove Downloaded Artifacts" 37 | condition: always() 38 | inputs: 39 | SourceFolder: $(Pipeline.Workspace)/$(artifactName) 40 | RemoveSourceFolder: true 41 | Contents: '**' 42 | -------------------------------------------------------------------------------- /build/stage-security-scan.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | - name: applicationEnvironment 3 | type: string 4 | default: '' 5 | - name: enableIosSecurityScan 6 | type: boolean 7 | default: false 8 | - name: enableAndroidSecurityScan 9 | type: boolean 10 | default: false 11 | 12 | jobs: 13 | - job: OnLinux_iOS_SecurityScan 14 | condition: eq(${{parameters.enableIosSecurityScan}}, true) 15 | dependsOn: [] 16 | pool: 17 | vmImage: $(ubuntuHostedAgentImage) 18 | steps: 19 | - template: templates/mobsf-scan.yml 20 | parameters: 21 | platform: 'iOS' 22 | fileExtension: $(ipaFileExtension) 23 | mobSfApiKey: $(mobSfApiKey) 24 | artifactName: '$(iOSArtifactName)_${{ parameters.applicationEnvironment }}' 25 | 26 | - job: OnLinux_Android_SecurityScan 27 | condition: eq(${{parameters.enableAndroidSecurityScan}}, true) 28 | dependsOn: [] 29 | pool: 30 | vmImage: $(ubuntuHostedAgentImage) 31 | steps: 32 | - template: templates/mobsf-scan.yml 33 | parameters: 34 | platform: 'android' 35 | fileExtension: $(aabFileExtension) 36 | mobSfApiKey: $(mobSfApiKey) 37 | artifactName: '$(AndroidArtifactName)_${{ parameters.applicationEnvironment }}' 38 | -------------------------------------------------------------------------------- /build/steps-build-release-notes.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | - name: additionalReleaseNotesFile 3 | type: string 4 | default: 'additionalReleaseNotes.md' 5 | - name: removeHyperlinksFromReleaseNotes 6 | type: boolean 7 | default: false 8 | 9 | steps: 10 | - powershell: New-Item -Path '$(ArtifactName)' -ItemType Directory 11 | displayName: Create ReleaseNotes folder 12 | 13 | - task: nventiveReleaseNotesCompiler@6 14 | displayName: 'Compile Release Notes' 15 | inputs: 16 | EnvironmentName: $(ApplicationEnvironment) 17 | AdditionalReleaseNotesFile: ${{ parameters.additionalReleaseNotesFile }} 18 | OutputFilePath: '$(ArtifactName)/ReleaseNotes.md' 19 | CreateTruncatedVersion: true 20 | TruncatedOutputFilePath: '$(ArtifactName)/ReleaseNotes-Excerpt.md' 21 | CharacterLimit: 5000 22 | RemoveHyperlinks: ${{ parameters.removeHyperlinksFromReleaseNotes }} 23 | 24 | - publish: $(ArtifactName) 25 | displayName: 'Publish Release Notes' 26 | artifact: $(ArtifactName) 27 | -------------------------------------------------------------------------------- /build/steps-build-tests.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | - name: pathToSrc 3 | type: string 4 | default: '$(Build.SourcesDirectory)/src' 5 | - name: projectName 6 | type: string 7 | default: '' 8 | - name: applicationEnvironment 9 | type: string 10 | default: '' 11 | 12 | steps: 13 | - template: templates/flutter-install.yml 14 | 15 | #-if false 16 | - download: current 17 | condition: eq('GeneratedApp', '${{ parameters.projectName }}') 18 | artifact: GeneratedApp 19 | #-endif 20 | 21 | - template: templates/flutter-prepare.yml 22 | parameters: 23 | projectDirectory: '${{ parameters.pathToSrc }}/app' 24 | 25 | - task: FlutterTest@0 26 | displayName: "Run Tests" 27 | inputs: 28 | generateCodeCoverageReport: true 29 | projectDirectory: '${{ parameters.pathToSrc }}/app' 30 | 31 | # TODO #316303 : Disable integration tests for now, will be fixed and re-enabled in the future. 32 | # - task: FlutterCommand@0 33 | # displayName: "Run Integration Tests" 34 | # inputs: 35 | # projectDirectory: '${{ parameters.pathToSrc }}/app' 36 | # arguments: test integration_test/integration_test.dart --dart-define ENV=${{ parameters.applicationEnvironment }} -d all 37 | 38 | - template: templates/flutter-diagnostics.yml 39 | parameters: 40 | projectDirectory: '${{ parameters.pathToSrc }}/app' 41 | condition: failed() 42 | 43 | - script: | 44 | set PATH=%PATH%;C:\Users\VssAdministrator\AppData\Local\Pub\Cache\bin 45 | dart pub global activate cobertura 46 | cd ${{ parameters.pathToSrc }}/app 47 | cobertura convert 48 | displayName: Convert Code Coverage from LCOV to Cobertura 49 | 50 | - task: PublishCodeCoverageResults@2 51 | displayName: Publish Code Coverage Result 52 | condition: succeeded() 53 | inputs: 54 | summaryFileLocation: "${{ parameters.pathToSrc }}/app/coverage/cobertura.xml" 55 | 56 | - publish: $(Build.ArtifactStagingDirectory) 57 | displayName: 'Publish Artifact' 58 | condition: always() 59 | artifact: $(ArtifactName) 60 | 61 | - task: PostBuildCleanup@4 62 | displayName: 'Post-Build cleanup: Cleanup files to keep build server clean!' 63 | condition: always() 64 | -------------------------------------------------------------------------------- /build/steps-build-windows.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | - name: pathToSrc 3 | type: string 4 | default: '' 5 | - name: projectName 6 | type: string 7 | default: '$(ProjectName)' 8 | 9 | steps: 10 | - template: templates/gitversion.yml 11 | - template: templates/build-number.yml 12 | - template: templates/flutter-install.yml 13 | 14 | #-if false 15 | - download: current 16 | condition: eq('GeneratedApp', '${{ parameters.projectName }}') 17 | artifact: GeneratedApp 18 | #-endif 19 | 20 | - template: templates/flutter-prepare.yml 21 | parameters: 22 | projectDirectory: '${{ parameters.pathToSrc }}/app' 23 | 24 | - task: FlutterBuild@0 25 | displayName: 'Build solution in $(ApplicationConfiguration)' 26 | inputs: 27 | target: windows 28 | buildName: '$(MajorMinorPatch)' 29 | buildNumber: '$(BuildNumber)' 30 | projectDirectory: '${{ parameters.pathToSrc }}/app' 31 | verboseMode: true 32 | dartDefine: ENV=$(applicationEnvironment) 33 | 34 | - template: templates/flutter-diagnostics.yml 35 | parameters: 36 | projectDirectory: '${{ parameters.pathToSrc }}/app' 37 | condition: failed() 38 | 39 | - publish: $(Build.ArtifactStagingDirectory) 40 | displayName: 'Publish artifact $(ApplicationConfiguration)' 41 | condition: always() 42 | artifact: $(ArtifactName) 43 | 44 | - task: PostBuildCleanup@4 45 | displayName: 'Post-Build Cleanup: Cleanup files to keep build server clean!' 46 | condition: always() 47 | -------------------------------------------------------------------------------- /build/templates/build-number.yml: -------------------------------------------------------------------------------- 1 | steps: 2 | # We provide the option to add padding to the build number (PreReleaseNumber) 3 | # in cases where a branch would generate a build number lower than the latest build on both Google Play and Apple App Stores. 4 | - task: PowerShell@2 5 | displayName: Add Padding to Build Number if necessary 6 | inputs: 7 | targetType: inline 8 | script: | 9 | $buildNumberWithPadding = [int]$(PreReleaseNumber) + [int]$(BuildPadding) 10 | Write-Host "##vso[task.setvariable variable=BuildNumber]$buildNumberWithPadding" 11 | Write-Host "Overridden build number: $buildNumberWithPadding" 12 | -------------------------------------------------------------------------------- /build/templates/flutter-diagnostics.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | - name: projectDirectory 3 | type: string 4 | default: '$(Build.SourcesDirectory)/src/app' 5 | - name: condition 6 | type: string 7 | default: always() 8 | 9 | steps: 10 | - task: FlutterCommand@0 11 | condition: ${{ parameters.condition }} 12 | displayName: "Run Flutter Diagnostics" 13 | inputs: 14 | projectDirectory: ${{ parameters.projectDirectory }} 15 | arguments: 'doctor -v' 16 | -------------------------------------------------------------------------------- /build/templates/flutter-install.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | - name: projectDirectory 3 | type: string 4 | default: '$(Build.SourcesDirectory)/src/app' 5 | 6 | steps: 7 | - task: FlutterInstall@0 8 | displayName: 'Run Flutter Install' 9 | inputs: 10 | mode: 'auto' 11 | channel: 'stable' 12 | version: 'custom' 13 | customVersion : $(FlutterVersion) 14 | -------------------------------------------------------------------------------- /build/templates/flutter-prepare.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | - name: projectDirectory 3 | type: string 4 | 5 | steps: 6 | - task: FlutterCommand@0 7 | displayName: "Run Flutter Clean" 8 | inputs: 9 | projectDirectory: ${{ parameters.projectDirectory }} 10 | arguments: 'clean' 11 | 12 | - task: FlutterCommand@0 13 | displayName: 'Run Flutter Pub Get' 14 | inputs: 15 | projectDirectory: ${{ parameters.projectDirectory }} 16 | arguments: 'pub get' 17 | 18 | - task: PowerShell@2 19 | displayName: 'Execute Build Runner' 20 | inputs: 21 | targetType: 'inline' 22 | script: | 23 | Set-Location ${{ parameters.projectDirectory }} 24 | $(DartToolPath)/dart run build_runner build --delete-conflicting-outputs 25 | -------------------------------------------------------------------------------- /build/templates/gitversion.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | - name: configFilePath 3 | type: string 4 | default: '$(Build.SourcesDirectory)/build/gitversion-config.yml' 5 | 6 | steps: 7 | - task: gitversion/setup@3 8 | retryCountOnTaskFailure: 3 9 | inputs: 10 | versionSpec: '5.12.0' 11 | displayName: 'Install GitVersion' 12 | 13 | - task: gitversion/execute@3 14 | inputs: 15 | useConfigFile: true 16 | configFilePath: ${{ parameters.configFilePath }} 17 | displayName: 'Calculate Version' 18 | -------------------------------------------------------------------------------- /build/templates/replace-firebase-config.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | - name: pathToSrc 3 | type: string 4 | - name: firebaseJsonFile 5 | type: string 6 | - name: firebaseOptionsDartFile 7 | type: string 8 | - name: googleServicesJsonFile 9 | type: string 10 | 11 | steps: 12 | - task: DownloadSecureFile@1 13 | name: firebaseJson 14 | displayName: "Download Keystore from Secure Files" 15 | inputs: 16 | secureFile: ${{ parameters.firebaseJsonFile }} 17 | 18 | - task: DownloadSecureFile@1 19 | name: firebaseOptionsDart 20 | displayName: "Download Keystore from Secure Files" 21 | inputs: 22 | secureFile: ${{ parameters.firebaseOptionsDartFile }} 23 | 24 | - task: DownloadSecureFile@1 25 | name: googleServicesJson 26 | displayName: "Download Keystore from Secure Files" 27 | inputs: 28 | secureFile: ${{ parameters.googleServicesJsonFile }} 29 | 30 | - task: PowerShell@2 31 | displayName: Copy Firebase Configuration Files 32 | inputs: 33 | targetType: 'inline' 34 | script: | 35 | Copy-Item -Path '$(firebaseJson.secureFilePath)' -Destination '${{ parameters.pathToSrc }}\app\firebase.json' 36 | Write-Host 'Firebase.json copied to ${{ parameters.pathToSrc }}/app/firebase.json' 37 | 38 | Copy-Item -Path '$(firebaseOptionsDart.secureFilePath)' -Destination '${{ parameters.pathToSrc }}\app\lib\firebase_options.dart' 39 | Write-Host 'FirebaseOptions.Dart copied to ${{ parameters.pathToSrc }}/app/lib/firebase_options.dart' 40 | 41 | Copy-Item -Path '$(googleServicesJson.secureFilePath)' -Destination '${{ parameters.pathToSrc }}\app\android\app\google-services.json' 42 | Write-Host 'GoogleServices.json copied to ${{ parameters.pathToSrc }}\app\android\app\google-services.json' 43 | -------------------------------------------------------------------------------- /build/templates/validate-commits.yaml: -------------------------------------------------------------------------------- 1 | # This template is used to validate that the commit messages follow the Conventional Commits specification (https://www.conventionalcommits.org/en/v1.0.0/). 2 | # Consider placing this at the beginning of the build pipeline to ensure that the commits are valid before proceeding with longer build steps. 3 | steps: 4 | - task: PowerShell@2 5 | condition: eq(variables['Build.Reason'], 'PullRequest') 6 | inputs: 7 | targetType: 'inline' 8 | script: | 9 | # Pre-Validation Logging 10 | Write-Host "Starting PR Validation..." 11 | Write-Host "Source Branch: $(System.PullRequest.SourceBranch)" 12 | Write-Host "Target Branch: $(System.PullRequest.TargetBranch)" 13 | Write-Host "Pull Request ID: $(System.PullRequest.PullRequestId)" 14 | write-Host "Repository: $(Build.Repository.Name)" 15 | Write-Host "Build.SourceBranch: $(Build.SourceBranch)" 16 | 17 | # Fetch commit range 18 | Write-Host "Retrieving commits..." 19 | git fetch origin 20 | Write-Host "Commit Range: origin/$(System.PullRequest.TargetBranch)..origin/$(System.PullRequest.SourceBranch)" 21 | $commits = git log origin/$(System.PullRequest.TargetBranch)..origin/$(System.PullRequest.SourceBranch) --pretty=format:"%s" 22 | $commitArray = $commits -split "`n" 23 | $commitCount = $commitArray.Count 24 | Write-Host "Commits found: $commitCount" 25 | 26 | # Regex pattern for Conventional Commits 27 | $pattern = '^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test){1}(\([\w\.\-]+\))?(!)?: ([\w ])+([\s\S]*)|^(Merged PR \d+: .+)|^(Merge pull request #\d+ from .+)|^(Merge branch .+)' 28 | Write-Host "Regular Expression: $pattern" 29 | 30 | # Validate each commit message 31 | $invalidCommits = @() 32 | foreach ($commit in $commitArray) { 33 | $commitMessage = $commit.Trim() 34 | Write-Host "Validating commit: $commitMessage" 35 | 36 | if ($commitMessage -notmatch $pattern) { 37 | $invalidCommits += $commitMessage 38 | } 39 | } 40 | 41 | if($invalidCommits.count -gt 0) { 42 | Write-Error "The following commit messages do no follow the Conventional Commits standard: `n$($invalidCommits -join "`n")" 43 | exit 1 44 | } else { 45 | Write-Host "All commit messages are valid." 46 | } 47 | displayName: 'Validate Commit Messages' -------------------------------------------------------------------------------- /doc/DependencyInjection.md: -------------------------------------------------------------------------------- 1 | # Dependency injection 2 | 3 | We use [GetIt](https://pub.dev/packages/get_it) for any IoC related work. 4 | 5 | ## Registering 6 | 7 | - You can register a singleton service using `GetIt.I.registerSingleton(ServiceImplementation())`. 8 | This service will be accessible from anywhere. 9 | 10 | - You can register a service that will be created everytime you request it by using `GetIt.I.registerFactory(() => ServiceImplementation())`. 11 | 12 | ## Resolving 13 | 14 | - Dependencies are injected manually into the constructors of the registered services. 15 | 16 | ``` dart 17 | GetIt.I.registerSingleton(MyOtherService()); 18 | GetIt.I.registerSingleton(MyService(myOtherService: GetIt.I())); 19 | 20 | class MyService 21 | { 22 | final MyOtherService _myOtherService; 23 | 24 | MyService({MyOtherService myOtherService}) : _myOtherService = myOtherService; 25 | } 26 | ``` 27 | 28 | - If you can't use manual constructor injection, you can use `GetIt.I()` to resolve a service. 29 | This will throw an exception if the type `IService` is not registered. 30 | 31 | - Circular dependencies will not work with this container. 32 | If you do have them, you might get an exception or an infinite loop. 33 | 34 | - You can access the service provider **statically** using `GetIt.I`. 35 | -------------------------------------------------------------------------------- /doc/Diagnostics.md: -------------------------------------------------------------------------------- 1 | # Diagnostic Tools 2 | 3 | This template comes with multiple built-in diagnostic tools. 4 | This is not available in the prod environment. 5 | 6 | ## Diagnostics Overlay 7 | 8 | When you start the application, you'll notice a box on the side of the screen. 9 | This overlay shows a few buttons. 10 | The overlay is accessible from anywhere in your app. 11 | This is useful when you want to see something happening live. 12 | 13 | ![Diagnostic](https://github.com/nventive/FlutterApplicationTemplate/assets/90481654/cd481d4b-79c5-4a06-bf2f-dce22e42e8b6) 14 | 15 | The default buttons include commands such as the following. 16 | - **Expand/Minimize** opens or closes the expanded view of the overlay. 17 | - **Move** moves the overlay left or right. 18 | - **X** hides the overlay for the remaining of the app cycle. 19 | - If you want to permanently hide the diagnostic, go to the environment section within the expanded overlay. 20 | 21 | ## Expanded overlay widgets 22 | 23 | The expanded diagnostics overlay has by default two widgets. 24 | 25 | The navigation widget has some buttons with commands on them that allow you to do some navigation in the app. 26 | The DeviceInfo widgets contains some information on the device and the app. 27 | The environment widget which lets the user change the environment. See [Environment.md](./Environment.md) for more details. 28 | The loggers widget which lets the user test logging and modify logging configuration. See [Logging.md](./Logging.md) for more details. -------------------------------------------------------------------------------- /doc/Environment.md: -------------------------------------------------------------------------------- 1 | # Environments 2 | 3 | We use `.env` files to store environment configuration with the help of [dotenv](https://pub.dev/packages/dotenv). 4 | 5 | ## Runtime environments 6 | 7 | By default, the template offers the following runtime environments, defined by their corresponding `.env` files. 8 | 9 | - Development (`.env.dev`) 10 | - Staging (`.env.staging`) 11 | - Production (`.env.prod`) 12 | 13 | You can add / remove runtime environments by simply following those steps: 14 | - Add or remove `.env` files (e.g. `.env.myenvironment`). 15 | - Add or remove a value in the [Environment](../src/app/lib/business/environment/environment.dart) enum. 16 | - Add or remove the associated item in `_EnvironmentManager._environmentFileNames`. 17 | 18 | This is governed by `EnvironmentManager`. It's initialized in the main method by calling `EnvironmentManager.Load`. 19 | Once initialized, the current environment configuration can be accessed with `dotenv` For example, you can do the following: 20 | ```dart 21 | ForcedUpdatePage({super.key}) { 22 | if (Platform.isIOS) { 23 | _url = Uri.parse(dotenv.env['APP_STORE_URL_IOS']!); 24 | } else { 25 | _url = Uri.parse(dotenv.env['APP_STORE_URL_Android']!); 26 | } 27 | } 28 | ``` 29 | 30 | - The default runtime environment is set via the environment variable `ENV`. 31 | - For local deployment, see `toolArgs` inside [launch.json](../.vscode/launch.json). 32 | - For builds, it's injected in the CI in the Flutter build task. 33 | - You can get the current environment using `EnvironmentManager.current`. 34 | - You can get all the possible environments using `EnvironmentManager.environments`. 35 | - You can set the environment using `environmentManager.setEnvironment`. If the environment doesn't exist, you will get an exception. 36 | - You can see what will be the next environment using `environmentManager.next` (because `current` might not change instantly). 37 | - When using `_EnvironmentManager` (the default implementation of `EnvironmentManager`), the current environment is persisted into the native storage that is processed at startup. 38 | 39 | ## Diagnostics 40 | 41 | Multiple environment features can be tested from the diagnostics overlay. 42 | This is configured in [EnvironmentPickerWidget](../src/app/lib/presentation/diagnostic/environment_picker_widget.dart). 43 | 44 | - You can see the current runtime environment. 45 | - You can see what the environment will be overriden to. 46 | - You can switch to another runtime environment. 47 | - You can reset the environment to its default value. -------------------------------------------------------------------------------- /doc/FirebaseRemoteConfig.md: -------------------------------------------------------------------------------- 1 | # Firebase Remote Configuration 2 | 3 | This apps uses Firebase for remote configurations, you can learn more about Firebase remote configurations [here](https://firebase.google.com/docs/remote-config/get-started?platform=flutter). 4 | 5 | To use Remote Config, you must create a Firebase project and link it to this project using the `flutterfire configure --project=yourFireBaseProjectName` CLI command. 6 | You can look at [this documentation](https://firebase.google.com/docs/flutter/setup?platform=ios) for more details. 7 | 8 | It's important to note that connecting Firebase to your project will expose API keys. 9 | According to Firebase documentation, however, this is not problematic. 10 | You can learn more about it [here](https://firebase.google.com/docs/projects/api-keys). 11 | -------------------------------------------------------------------------------- /doc/ForcedUpdate.md: -------------------------------------------------------------------------------- 1 | # Forced Update 2 | 3 | The forced update feature is for when you want to force the user to update the app. 4 | You could use this, for example, when the backend changes and you do not want the users to still use the old API. 5 | 6 | To force an update, we wait for a `Future` `checkUpdateRequired()` to be resolved from the `UpdateRequiredservice` in the [main file of the app.](../src/app/lib/main.dart). 7 | 8 | This will redirect the user to [a page](../src/app/lib/presentation/forced_update/forced_update_page.dart) from which they cannot navigate back. 9 | The minimum update required is defined in a [Firebase Remote Config](/doc/FirebaseRemoteConfig.md). 10 | -------------------------------------------------------------------------------- /doc/HTTP.md: -------------------------------------------------------------------------------- 1 | # HTTP 2 | 3 | We use [Dio](https://pub.dev/packages/dio) for any HTTP related work. 4 | 5 | For more documentation on HTTP requests, read the references listed at the bottom. 6 | 7 | ## HTTP clients 8 | 9 | - We use `Interceptor` to create HTTP request / response pipelines. There are lot of interceptors implementation in the community. 10 | 11 | - We use [Retrofit](https://pub.dev/packages/retrofit) to generate the HTTP implementations in the data access layer. 12 | 13 | ## References 14 | - [Interceptors](https://pub.dev/packages/dio#interceptors) 15 | - [What is Retrofit](https://github.com/trevorwang/retrofit.dart/) 16 | -------------------------------------------------------------------------------- /doc/KillSwitch.md: -------------------------------------------------------------------------------- 1 | # Kill Switch 2 | 3 | The kill switch feature is for when you want to temporarily lock the user out of the app. 4 | This could be used for example when the server is down for some time, to avoid the users getting a ton of errors and getting reports from those users. 5 | 6 | To trigger the kill switch, we subscribe to the `isKillSwitchActivatedStream` `stream` from the `KillSwitchService` in the [Main](../src/app/lib/main.dart). 7 | 8 | If the kill switch is activated, the user is brought to the `KillSwitchPage` where he can see a message that tells him the app is currently unavailable. 9 | If the kill switch is deactivated afterwards, the user is brought back to the initial navigation flow, which means he will be in the login page if he is not connected and to the home page if he is connected. 10 | 11 | Whether the kill switch is activated or not on mobile and web platforms is defined in a [remote config](FirebaseRemoteConfig.md). 12 | -------------------------------------------------------------------------------- /doc/Localization.md: -------------------------------------------------------------------------------- 1 | # Localization 2 | 3 | We use [flutter_localization](https://pub.dev/packages/flutter_localization) for localization work. 4 | 5 | For more documentation on localization, you can read [the official internationalization documentation for Flutter apps.](https://docs.flutter.dev/ui/accessibility-and-internationalization/internationalization) 6 | 7 | ## Text localization 8 | 9 | - We use `.arb` files located under the [arb folder](../lib/l10n/arb) to localize texts. 10 | 11 | - We use an [extension](../src/app/lib/l10n/localization_extensions.dart) on `BuildContext` to resolve those localized texts. 12 | ``` dart 13 | final local = context.local; 14 | 15 | String myString = local.myString; 16 | ``` 17 | 18 | - The generated files are generated at [lib/l10n/gen_l10n](../src/app/lib/l10n/gen_l10n/) instead of the default location due to problems with the pipeline and artificial packages. 19 | 20 | > 💡 Please note that to be able to access the strings, you need to run `flutter pub get` to generate them. 21 | 22 | - After running the app for the first time, localization files should be created automatically, if not it might be necessary to run the command `flutter gen-l10n` to generate the localization files. -------------------------------------------------------------------------------- /doc/Navigation.md: -------------------------------------------------------------------------------- 1 | # Navigation 2 | 3 | This project uses the [go_router](https://pub.dev/packages/go_router) to handle navigation. 4 | 5 | ## Handling sections with bottom navigation bar 6 | 7 | To handle pages that share a menu, we use a [StatefulShellRoute](https://pub.dev/documentation/go_router/latest/go_router/StatefulShellRoute-class.html). 8 | 9 | ## Registering routes 10 | 11 | To register a new section, you must add a stateful branch. 12 | Each branch represents a section. 13 | Each page you add below the primary route will have the shell and anything that comes with it (like the bottom navigation bar). 14 | When you add a route nested in another route, if you navigate to that page and then navigate back, it will navigate back to the parent page. 15 | 16 | ```dart 17 | StatefulShellBranch( 18 | routes: [ 19 | GoRoute( 20 | path: '/parent', 21 | builder: (context, state) => const ParentPage(), 22 | routes: [ 23 | GoRoute( 24 | path: 'child', 25 | builder: (context, state) => const ChildPage(), 26 | ), 27 | ], 28 | ), 29 | ], 30 | ), 31 | ``` 32 | If you want a page without the shell, you can declare the route outside of the `StatefulShellRoute`. 33 | 34 | ## How to navigate 35 | 36 | To navigate, you can reference the `router`, which is a global variable in the project. 37 | To navigate to a different page, you can use `go` or `push`. 38 | 39 | Here are a few examples: 40 | 41 | ```dart 42 | // This will go to the child page. If you press the back button on that page, you will be on the parent page. 43 | router.go('/parent/child'); 44 | 45 | // This will push the unrelated page to the top of the current navigation stack. 46 | router.push('/unrelated-page'); 47 | 48 | // This will replace the current navigation stack with the unrelated page. 49 | router.go('/unrelated-page'); 50 | ``` 51 | 52 | To change the section, you can simply navigate to a route created under the section's StatefulShellBranch. 53 | -------------------------------------------------------------------------------- /doc/Serialization.md: -------------------------------------------------------------------------------- 1 | ## Serialization 2 | 3 | We use [Json Serializable](https://pub.dev/packages/json_serializable) for (de)serialization. 4 | -------------------------------------------------------------------------------- /doc/diagrams/architecture-context.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nventive/FlutterApplicationTemplate/3ecb4f9a2a43a70cfcab226f6e037748f296cdf4/doc/diagrams/architecture-context.png -------------------------------------------------------------------------------- /doc/diagrams/architecture-structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nventive/FlutterApplicationTemplate/3ecb4f9a2a43a70cfcab226f6e037748f296cdf4/doc/diagrams/architecture-structure.png -------------------------------------------------------------------------------- /doc/diagrams/solution-structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nventive/FlutterApplicationTemplate/3ecb4f9a2a43a70cfcab226f6e037748f296cdf4/doc/diagrams/solution-structure.png -------------------------------------------------------------------------------- /src/app/.env.dev: -------------------------------------------------------------------------------- 1 | IS_CONSOLE_LOGGING_ENABLED=true 2 | IS_FILE_LOGGING_ENABLED=true 3 | MINIMUM_LEVEL='debug' 4 | DAD_JOKES_BASE_URL='https://www.reddit.com/r/dadjokes' 5 | APP_STORE_URL_IOS=https://apps.apple.com/us/app/uno-calculator/id1464736591 6 | APP_STORE_URL_Android=https://play.google.com/store/apps/details?id=uno.platform.calculator 7 | REMOTE_CONFIG_FETCH_INTERVAL_MINUTES=1 8 | DIAGNOSTIC_ENABLED=true -------------------------------------------------------------------------------- /src/app/.env.prod: -------------------------------------------------------------------------------- 1 | IS_CONSOLE_LOGGING_ENABLED=false 2 | IS_FILE_LOGGING_ENABLED=true 3 | MINIMUM_LEVEL='warning' 4 | DAD_JOKES_BASE_URL='https://www.reddit.com/r/dadjokes' 5 | APP_STORE_URL_IOS=https://apps.apple.com/us/app/uno-calculator/id1464736591 6 | APP_STORE_URL_Android=https://play.google.com/store/apps/details?id=uno.platform.calculator 7 | REMOTE_CONFIG_FETCH_INTERVAL_MINUTES=720 8 | DIAGNOSTIC_ENABLED=false -------------------------------------------------------------------------------- /src/app/.env.staging: -------------------------------------------------------------------------------- 1 | IS_CONSOLE_LOGGING_ENABLED=false 2 | IS_FILE_LOGGING_ENABLED=true 3 | MINIMUM_LEVEL='info' 4 | DAD_JOKES_BASE_URL='https://www.reddit.com/r/dadjokes' 5 | APP_STORE_URL_IOS=https://apps.apple.com/us/app/uno-calculator/id1464736591 6 | APP_STORE_URL_Android=https://play.google.com/store/apps/details?id=uno.platform.calculator 7 | REMOTE_CONFIG_FETCH_INTERVAL_MINUTES=1 8 | DIAGNOSTIC_ENABLED=true -------------------------------------------------------------------------------- /src/app/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # Flutter/Dart/Pub related 20 | **/doc/api/ 21 | **/ios/Flutter/.last_build_id 22 | .dart_tool/ 23 | .flutter-plugins 24 | .flutter-plugins-dependencies 25 | .pub-cache/ 26 | .pub/ 27 | /build/ 28 | 29 | # Symbolication related 30 | app.*.symbols 31 | 32 | # Obfuscation related 33 | app.*.map.json 34 | 35 | # Localization related 36 | lib/l10n/gen_l10n/* -------------------------------------------------------------------------------- /src/app/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: "7482962148e8d758338d8a28f589f317e1e42ba4" 8 | channel: "stable" 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: 7482962148e8d758338d8a28f589f317e1e42ba4 17 | base_revision: 7482962148e8d758338d8a28f589f317e1e42ba4 18 | - platform: android 19 | create_revision: 7482962148e8d758338d8a28f589f317e1e42ba4 20 | base_revision: 7482962148e8d758338d8a28f589f317e1e42ba4 21 | - platform: ios 22 | create_revision: 7482962148e8d758338d8a28f589f317e1e42ba4 23 | base_revision: 7482962148e8d758338d8a28f589f317e1e42ba4 24 | - platform: linux 25 | create_revision: 7482962148e8d758338d8a28f589f317e1e42ba4 26 | base_revision: 7482962148e8d758338d8a28f589f317e1e42ba4 27 | - platform: macos 28 | create_revision: 7482962148e8d758338d8a28f589f317e1e42ba4 29 | base_revision: 7482962148e8d758338d8a28f589f317e1e42ba4 30 | - platform: web 31 | create_revision: 7482962148e8d758338d8a28f589f317e1e42ba4 32 | base_revision: 7482962148e8d758338d8a28f589f317e1e42ba4 33 | - platform: windows 34 | create_revision: 7482962148e8d758338d8a28f589f317e1e42ba4 35 | base_revision: 7482962148e8d758338d8a28f589f317e1e42ba4 36 | 37 | # User provided section 38 | 39 | # List of Local paths (relative to this file) that should be 40 | # ignored by the migrate tool. 41 | # 42 | # Files that are not part of the templates will be ignored by default. 43 | unmanaged_files: 44 | - 'lib/main.dart' 45 | - 'ios/Runner.xcodeproj/project.pbxproj' 46 | -------------------------------------------------------------------------------- /src/app/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | analyzer: 13 | exclude: 14 | - '**.g.dart' 15 | plugins: 16 | - custom_lint 17 | 18 | linter: 19 | # The lint rules applied to this project can be customized in the 20 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 21 | # included above or to enable additional rules. A list of all available lints 22 | # and their documentation is published at https://dart.dev/lints. 23 | # 24 | # Instead of disabling a lint rule for the entire project in the 25 | # section below, it can also be suppressed for a single line of code 26 | # or a specific dart file by using the `// ignore: name_of_lint` and 27 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 28 | # producing the lint. 29 | rules: 30 | annotate_overrides: true 31 | avoid_print: true 32 | always_declare_return_types: true 33 | always_use_package_imports: true 34 | require_trailing_commas: true 35 | 36 | # Additional information about this file can be found at 37 | # https://dart.dev/guides/language/analysis-options -------------------------------------------------------------------------------- /src/app/android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | 15 | # Android Studio will place build artifacts here. 16 | app/debug 17 | app/profile 18 | app/release -------------------------------------------------------------------------------- /src/app/android/app/google-services.json: -------------------------------------------------------------------------------- 1 | { 2 | "project_info": { 3 | "project_number": "139514080243", 4 | "project_id": "flutterapplicationtempla-79caa", 5 | "storage_bucket": "flutterapplicationtempla-79caa.appspot.com" 6 | }, 7 | "client": [ 8 | { 9 | "client_info": { 10 | "mobilesdk_app_id": "1:139514080243:android:877033ea3d429ad3f0c086", 11 | "android_client_info": { 12 | "package_name": "com.nventive.internal.flutterapptemplate" 13 | } 14 | }, 15 | "oauth_client": [], 16 | "api_key": [ 17 | { 18 | "current_key": "AIzaSyDYwPrW2yvBCfSGnAf1E4SddbRfk02P_m0" 19 | } 20 | ], 21 | "services": { 22 | "appinvite_service": { 23 | "other_platform_oauth_client": [] 24 | } 25 | } 26 | } 27 | ], 28 | "configuration_version": "1" 29 | } -------------------------------------------------------------------------------- /src/app/android/app/src/main/kotlin/com/nventive/internal/flutterapptemplate/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.nventive.internal.flutterapptemplate 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() 6 | -------------------------------------------------------------------------------- /src/app/android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /src/app/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /src/app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nventive/FlutterApplicationTemplate/3ecb4f9a2a43a70cfcab226f6e037748f296cdf4/src/app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /src/app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nventive/FlutterApplicationTemplate/3ecb4f9a2a43a70cfcab226f6e037748f296cdf4/src/app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /src/app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nventive/FlutterApplicationTemplate/3ecb4f9a2a43a70cfcab226f6e037748f296cdf4/src/app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /src/app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nventive/FlutterApplicationTemplate/3ecb4f9a2a43a70cfcab226f6e037748f296cdf4/src/app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /src/app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nventive/FlutterApplicationTemplate/3ecb4f9a2a43a70cfcab226f6e037748f296cdf4/src/app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /src/app/android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /src/app/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /src/app/android/build.gradle: -------------------------------------------------------------------------------- 1 | allprojects { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | } 6 | } 7 | 8 | rootProject.buildDir = '../build' 9 | subprojects { 10 | project.buildDir = "${rootProject.buildDir}/${project.name}" 11 | } 12 | subprojects { 13 | project.evaluationDependsOn(':app') 14 | } 15 | 16 | tasks.register("clean", Delete) { 17 | delete rootProject.buildDir 18 | } 19 | -------------------------------------------------------------------------------- /src/app/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx4G 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | android.defaults.buildfeatures.buildconfig=true 5 | android.nonTransitiveRClass=false 6 | android.nonFinalResIds=false 7 | -------------------------------------------------------------------------------- /src/app/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /src/app/android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | def flutterSdkPath = { 3 | def properties = new Properties() 4 | file("local.properties").withInputStream { properties.load(it) } 5 | def flutterSdkPath = properties.getProperty("flutter.sdk") 6 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 7 | return flutterSdkPath 8 | } 9 | settings.ext.flutterSdkPath = flutterSdkPath() 10 | 11 | includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle") 12 | 13 | repositories { 14 | google() 15 | mavenCentral() 16 | gradlePluginPortal() 17 | } 18 | } 19 | 20 | plugins { 21 | id "dev.flutter.flutter-plugin-loader" version "1.0.0" 22 | id "com.android.application" version "8.7.3" apply false 23 | // START: FlutterFire Configuration 24 | id "com.google.gms.google-services" version "4.3.15" apply false 25 | // END: FlutterFire Configuration 26 | id "org.jetbrains.kotlin.android" version "2.1.10" apply false 27 | } 28 | 29 | include ":app" 30 | -------------------------------------------------------------------------------- /src/app/assets/fonts/guillon_black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nventive/FlutterApplicationTemplate/3ecb4f9a2a43a70cfcab226f6e037748f296cdf4/src/app/assets/fonts/guillon_black.ttf -------------------------------------------------------------------------------- /src/app/assets/fonts/guillon_bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nventive/FlutterApplicationTemplate/3ecb4f9a2a43a70cfcab226f6e037748f296cdf4/src/app/assets/fonts/guillon_bold.ttf -------------------------------------------------------------------------------- /src/app/assets/fonts/guillon_light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nventive/FlutterApplicationTemplate/3ecb4f9a2a43a70cfcab226f6e037748f296cdf4/src/app/assets/fonts/guillon_light.ttf -------------------------------------------------------------------------------- /src/app/assets/fonts/guillon_regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nventive/FlutterApplicationTemplate/3ecb4f9a2a43a70cfcab226f6e037748f296cdf4/src/app/assets/fonts/guillon_regular.ttf -------------------------------------------------------------------------------- /src/app/assets/fonts/guillon_semibold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nventive/FlutterApplicationTemplate/3ecb4f9a2a43a70cfcab226f6e037748f296cdf4/src/app/assets/fonts/guillon_semibold.ttf -------------------------------------------------------------------------------- /src/app/firebase.json: -------------------------------------------------------------------------------- 1 | {"flutter":{"platforms":{"android":{"default":{"projectId":"flutterapplicationtempla-79caa","appId":"1:139514080243:android:877033ea3d429ad3f0c086","fileOutput":"android/app/google-services.json"}},"dart":{"lib/firebase_options.dart":{"projectId":"flutterapplicationtempla-79caa","configurations":{"android":"1:139514080243:android:877033ea3d429ad3f0c086","ios":"1:139514080243:ios:b64a363f3d032097f0c086","macos":"1:139514080243:ios:b64a363f3d032097f0c086","web":"1:139514080243:web:1fc151a0dc66e448f0c086","windows":"1:139514080243:web:019e1b6ba816e998f0c086"}}}}}} -------------------------------------------------------------------------------- /src/app/integration_test/forced_update_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/access/forced_update/current_version_repository.dart'; 2 | import 'package:app/access/forced_update/data/version.dart'; 3 | import 'package:app/access/forced_update/minimum_version_repository.dart'; 4 | import 'package:app/access/forced_update/minimum_version_repository_mock.dart'; 5 | import 'package:app/app.dart'; 6 | import 'package:app/app_router.dart'; 7 | import 'package:flutter/foundation.dart'; 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | import 'package:get_it/get_it.dart'; 11 | 12 | /// Test for the Forced update. 13 | Future forcedUpdateTest() async { 14 | testWidgets( 15 | 'Navigate to ForcedUpdatePage when current version is lower than the minimum required version', 16 | (WidgetTester tester) async { 17 | // Arrange 18 | await tester.pumpWidget(const App()); 19 | 20 | var currentVersionRepo = GetIt.I.get(); 21 | 22 | var minimumVersionRepo = 23 | GetIt.I.get() as MinimumVersionRepositoryMock; 24 | 25 | var currentVersion = await currentVersionRepo.getCurrentVersion(); 26 | 27 | var newMinimumVersion = Version(currentVersion.major + 1, 0, 0); 28 | 29 | // Act 30 | minimumVersionRepo.updateMinimumVersion(version: newMinimumVersion); 31 | 32 | await tester.pumpAndSettle(); 33 | 34 | // Assert 35 | var forcedUpdateScaffold = find.byKey(const Key('forcedUpdateScaffold')); 36 | 37 | // Check if the ForcedUpdatePage is present in the widget tree. 38 | expect(forcedUpdateScaffold, findsOne); 39 | 40 | expect(router.canPop(), false); 41 | }); 42 | } 43 | -------------------------------------------------------------------------------- /src/app/integration_test/integration_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/access/mocking/mocking_repository.dart'; 2 | import 'package:app/main.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | import 'package:get_it/get_it.dart'; 5 | import 'package:integration_test/integration_test.dart'; 6 | 7 | import 'dad_jokes_page_test.dart'; 8 | import 'forced_update_test.dart'; 9 | import 'kill_switch_test.dart'; 10 | 11 | /// All integration tests are run here because of this issue: https://github.com/flutter/flutter/issues/135673 12 | Future main() async { 13 | IntegrationTestWidgetsFlutterBinding.ensureInitialized(); 14 | await initializeComponents(isMocked: true); 15 | 16 | tearDownAll( 17 | () async => await GetIt.I.get().setMocking(false), 18 | ); 19 | 20 | await dadJokeTest(); 21 | await killSwitchTest(); 22 | await forcedUpdateTest(); 23 | } 24 | -------------------------------------------------------------------------------- /src/app/ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | -------------------------------------------------------------------------------- /src/app/ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 15.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/app/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /src/app/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /src/app/ios/Podfile: -------------------------------------------------------------------------------- 1 | platform :ios, '16.0' 2 | 3 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 4 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 5 | 6 | project 'Runner', { 7 | 'Debug' => :debug, 8 | 'Profile' => :release, 9 | 'Release' => :release, 10 | } 11 | 12 | def flutter_root 13 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 14 | unless File.exist?(generated_xcode_build_settings_path) 15 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 16 | end 17 | 18 | File.foreach(generated_xcode_build_settings_path) do |line| 19 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 20 | return matches[1].strip if matches 21 | end 22 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 23 | end 24 | 25 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 26 | 27 | flutter_ios_podfile_setup 28 | 29 | target 'Runner' do 30 | use_frameworks! 31 | use_modular_headers! 32 | 33 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 34 | target 'RunnerTests' do 35 | inherit! :search_paths 36 | end 37 | end 38 | 39 | post_install do |installer| 40 | installer.pods_project.targets.each do |target| 41 | flutter_additional_ios_build_settings(target) 42 | 43 | # Workaround. 44 | # See https://github.com/CocoaPods/CocoaPods/issues/11402 for more details. 45 | target.build_configurations.each do |config| 46 | config.build_settings['CODE_SIGNING_REQUIRED'] = "NO" 47 | config.build_settings['CODE_SIGNING_ALLOWED'] = "NO" 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /src/app/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/app/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/app/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/app/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/app/ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nventive/FlutterApplicationTemplate/3ecb4f9a2a43a70cfcab226f6e037748f296cdf4/src/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /src/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nventive/FlutterApplicationTemplate/3ecb4f9a2a43a70cfcab226f6e037748f296cdf4/src/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /src/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nventive/FlutterApplicationTemplate/3ecb4f9a2a43a70cfcab226f6e037748f296cdf4/src/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /src/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nventive/FlutterApplicationTemplate/3ecb4f9a2a43a70cfcab226f6e037748f296cdf4/src/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /src/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nventive/FlutterApplicationTemplate/3ecb4f9a2a43a70cfcab226f6e037748f296cdf4/src/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /src/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nventive/FlutterApplicationTemplate/3ecb4f9a2a43a70cfcab226f6e037748f296cdf4/src/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /src/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nventive/FlutterApplicationTemplate/3ecb4f9a2a43a70cfcab226f6e037748f296cdf4/src/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /src/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nventive/FlutterApplicationTemplate/3ecb4f9a2a43a70cfcab226f6e037748f296cdf4/src/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /src/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nventive/FlutterApplicationTemplate/3ecb4f9a2a43a70cfcab226f6e037748f296cdf4/src/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /src/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nventive/FlutterApplicationTemplate/3ecb4f9a2a43a70cfcab226f6e037748f296cdf4/src/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /src/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nventive/FlutterApplicationTemplate/3ecb4f9a2a43a70cfcab226f6e037748f296cdf4/src/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /src/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nventive/FlutterApplicationTemplate/3ecb4f9a2a43a70cfcab226f6e037748f296cdf4/src/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /src/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nventive/FlutterApplicationTemplate/3ecb4f9a2a43a70cfcab226f6e037748f296cdf4/src/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /src/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nventive/FlutterApplicationTemplate/3ecb4f9a2a43a70cfcab226f6e037748f296cdf4/src/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /src/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nventive/FlutterApplicationTemplate/3ecb4f9a2a43a70cfcab226f6e037748f296cdf4/src/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /src/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nventive/FlutterApplicationTemplate/3ecb4f9a2a43a70cfcab226f6e037748f296cdf4/src/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /src/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nventive/FlutterApplicationTemplate/3ecb4f9a2a43a70cfcab226f6e037748f296cdf4/src/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /src/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nventive/FlutterApplicationTemplate/3ecb4f9a2a43a70cfcab226f6e037748f296cdf4/src/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /src/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /src/app/ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/app/ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/app/ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | ApplicationTemplate 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | ApplicationTemplate 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(FLUTTER_BUILD_NUMBER) 25 | LSRequiresIPhoneOS 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | CADisableMinimumFrameDurationOnPhone 45 | 46 | UIApplicationSupportsIndirectInputEvents 47 | 48 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /src/app/ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /src/app/ios/RunnerTests/RunnerTests.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 3 | import XCTest 4 | 5 | class RunnerTests: XCTestCase { 6 | 7 | func testExample() { 8 | // If you add code to the Runner application, consider adding tests here. 9 | // See https://developer.apple.com/documentation/xctest for more information about using XCTest. 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/app/l10n.yaml: -------------------------------------------------------------------------------- 1 | arb-dir: lib/l10n/arb 2 | template-arb-file: app_en.arb 3 | synthetic-package: false 4 | output-dir: lib/l10n/gen_l10n 5 | output-localization-file: app_localizations.dart 6 | nullable-getter: false -------------------------------------------------------------------------------- /src/app/lib/access/bugsee/bugsee_configuration_data.dart: -------------------------------------------------------------------------------- 1 | final class BugseeConfigurationData { 2 | /// Gets whether the Bugsee SDK is enabled or not. if [Null] it fallbacks to a new installed app so it will be enabled. 3 | final bool? isBugseeEnabled; 4 | 5 | /// Indicate whether the video capturing feature in Bugsee is enabled or not. 6 | final bool? isVideoCaptureEnabled; 7 | 8 | const BugseeConfigurationData({ 9 | required this.isBugseeEnabled, 10 | required this.isVideoCaptureEnabled, 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /src/app/lib/access/bugsee/bugsee_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/access/bugsee/bugsee_configuration_data.dart'; 2 | import 'package:app/access/persistence_exception.dart'; 3 | import 'package:shared_preferences/shared_preferences.dart'; 4 | 5 | abstract interface class BugseeRepository { 6 | factory BugseeRepository() = _BugseeRepository; 7 | 8 | /// Load the current bugsee configuration stored in shared prefs. 9 | Future getBugseeConfiguration(); 10 | 11 | /// Update the current Bugsee enabled flag in shared prefs. 12 | Future setIsBugseeEnabled(bool isBugseeEnabled); 13 | 14 | /// Update the current video captured or not flag in shared prefs. 15 | Future setIsVideoCaptureEnabled(bool isVideoCaptureEnabled); 16 | } 17 | 18 | final class _BugseeRepository implements BugseeRepository { 19 | final String _bugseeEnabledKey = 'bugseeEnabledKey'; 20 | final String _videoCaptureKey = 'videoCaptureKey'; 21 | 22 | @override 23 | Future getBugseeConfiguration() async { 24 | final sharedPrefInstance = await SharedPreferences.getInstance(); 25 | return BugseeConfigurationData( 26 | isBugseeEnabled: sharedPrefInstance.getBool(_bugseeEnabledKey), 27 | isVideoCaptureEnabled: sharedPrefInstance.getBool(_videoCaptureKey), 28 | ); 29 | } 30 | 31 | @override 32 | Future setIsBugseeEnabled(bool isBugseeEnabled) async { 33 | final sharedPrefInstance = await SharedPreferences.getInstance(); 34 | 35 | bool isSaved = await sharedPrefInstance.setBool( 36 | _bugseeEnabledKey, 37 | isBugseeEnabled, 38 | ); 39 | 40 | if (!isSaved) { 41 | throw PersistenceException( 42 | message: 'Error while setting $_bugseeEnabledKey $isBugseeEnabled', 43 | ); 44 | } 45 | } 46 | 47 | @override 48 | Future setIsVideoCaptureEnabled(bool isVideoCaptureEnabled) async { 49 | final sharedPrefInstance = await SharedPreferences.getInstance(); 50 | 51 | bool isSaved = await sharedPrefInstance.setBool( 52 | _videoCaptureKey, 53 | isVideoCaptureEnabled, 54 | ); 55 | 56 | if (!isSaved) { 57 | throw PersistenceException( 58 | message: 'Error while setting $_videoCaptureKey $isVideoCaptureEnabled', 59 | ); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/app/lib/access/dad_jokes/dad_jokes_mocked_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/access/dad_jokes/dad_jokes_repository.dart'; 2 | import 'package:app/access/dad_jokes/data/dad_joke_response_data.dart'; 3 | import 'package:app/access/dad_jokes/mocks/dad_jokes_data_mock.dart'; 4 | 5 | final class DadJokesMockedRepository implements DadJokesRepository { 6 | 7 | DadJokesMockedRepository(); 8 | 9 | @override 10 | Future getDadJokes() async { 11 | return mockedDadJokeResponse; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/app/lib/access/dad_jokes/dad_jokes_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/access/dad_jokes/data/dad_joke_response_data.dart'; 2 | import 'package:dio/dio.dart'; 3 | import 'package:retrofit/retrofit.dart'; 4 | 5 | part 'dad_jokes_repository.g.dart'; 6 | 7 | @RestApi(baseUrl: 'https://www.reddit.com/r/dadjokes') 8 | abstract interface class DadJokesRepository { 9 | factory DadJokesRepository(Dio dio, {String baseUrl}) = _DadJokesRepository; 10 | 11 | @GET('/top.json') 12 | Future getDadJokes(); 13 | } 14 | -------------------------------------------------------------------------------- /src/app/lib/access/dad_jokes/data/dad_joke_child_data.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/access/dad_jokes/data/dad_joke_content_data.dart'; 2 | import 'package:json_annotation/json_annotation.dart'; 3 | 4 | part 'dad_joke_child_data.g.dart'; 5 | 6 | @JsonSerializable(createToJson: false) 7 | final class DadJokeChildData { 8 | @JsonKey(name: 'data') 9 | final DadJokeContentData dadJokeContentData; 10 | 11 | const DadJokeChildData({required this.dadJokeContentData}); 12 | 13 | factory DadJokeChildData.fromJson(Map json) => _$DadJokeChildDataFromJson(json); 14 | } -------------------------------------------------------------------------------- /src/app/lib/access/dad_jokes/data/dad_joke_content_data.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | 3 | part 'dad_joke_content_data.g.dart'; 4 | 5 | @JsonSerializable() 6 | final class DadJokeContentData { 7 | final String id; 8 | final String title; 9 | 10 | @JsonKey(name: 'selftext') 11 | final String selfText; 12 | 13 | const DadJokeContentData({ 14 | required this.id, 15 | required this.title, 16 | required this.selfText, 17 | }); 18 | 19 | factory DadJokeContentData.fromJson(Map json) => 20 | _$DadJokeContentDataFromJson(json); 21 | 22 | Map toJson() => _$DadJokeContentDataToJson(this); 23 | } 24 | -------------------------------------------------------------------------------- /src/app/lib/access/dad_jokes/data/dad_joke_data.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/access/dad_jokes/data/dad_joke_child_data.dart'; 2 | import 'package:json_annotation/json_annotation.dart'; 3 | 4 | part 'dad_joke_data.g.dart'; 5 | 6 | @JsonSerializable(createToJson: false) 7 | final class DadJokeData { 8 | @JsonKey(name: 'children') 9 | final List dadJokeChildrenData; 10 | 11 | const DadJokeData({required this.dadJokeChildrenData}); 12 | 13 | factory DadJokeData.fromJson(Map json) => _$DadJokeDataFromJson(json); 14 | } -------------------------------------------------------------------------------- /src/app/lib/access/dad_jokes/data/dad_joke_response_data.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/access/dad_jokes/data/dad_joke_data.dart'; 2 | import 'package:json_annotation/json_annotation.dart'; 3 | 4 | part 'dad_joke_response_data.g.dart'; 5 | 6 | @JsonSerializable(createToJson: false) 7 | final class DadJokeResponseData { 8 | @JsonKey(name: 'data') 9 | final DadJokeData dadJokeData; 10 | 11 | const DadJokeResponseData({required this.dadJokeData}); 12 | 13 | factory DadJokeResponseData.fromJson(Map json) => _$DadJokeResponseDataFromJson(json); 14 | } 15 | -------------------------------------------------------------------------------- /src/app/lib/access/dad_jokes/favorite_dad_jokes_mocked_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/access/dad_jokes/data/dad_joke_content_data.dart'; 2 | import 'package:app/access/dad_jokes/favorite_dad_jokes_repository.dart'; 3 | import 'package:app/access/dad_jokes/mocks/dad_jokes_list_mock.dart'; 4 | 5 | final class FavoriteDadJokesMockedRepository 6 | implements FavoriteDadJokesRepository { 7 | List mockedFavoriteDadJokesList = 8 | getMockedFavoriteDadJokesList(); 9 | 10 | FavoriteDadJokesMockedRepository(); 11 | 12 | @override 13 | Future> getFavoriteDadJokes() async { 14 | var favJokes = [...mockedFavoriteDadJokesList]; 15 | 16 | await Future.value(); 17 | 18 | return favJokes; 19 | } 20 | 21 | @override 22 | Future setFavoriteDadJokes(List favoriteDadJokes) async { 23 | mockedFavoriteDadJokesList = favoriteDadJokes; 24 | 25 | return Future.value(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/app/lib/access/dad_jokes/favorite_dad_jokes_repository.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:app/access/dad_jokes/data/dad_joke_content_data.dart'; 4 | import 'package:shared_preferences/shared_preferences.dart'; 5 | 6 | /// Interface for the favorite dad jokes repository. 7 | abstract interface class FavoriteDadJokesRepository { 8 | factory FavoriteDadJokesRepository() = _FavoriteDadJokesRepository; 9 | 10 | /// Gets the favorite dad jokes. 11 | Future> getFavoriteDadJokes(); 12 | 13 | /// Sets the favorite dad jokes. 14 | Future setFavoriteDadJokes(List favoriteDadJokes); 15 | } 16 | 17 | /// Implementation of [FavoriteDadJokesRepository]. 18 | final class _FavoriteDadJokesRepository implements FavoriteDadJokesRepository { 19 | /// The key used to store favorite dad jokes in shared preferences. 20 | final String _favoriteDadJokesKey = 'favoriteDadJokes'; 21 | 22 | _FavoriteDadJokesRepository(); 23 | 24 | @override 25 | Future> getFavoriteDadJokes() async { 26 | final sharedPreferences = await SharedPreferences.getInstance(); 27 | final favoriteDadJokesJson = sharedPreferences.getStringList(_favoriteDadJokesKey) ?? []; 28 | 29 | return favoriteDadJokesJson 30 | .map( 31 | (favoriteDadJokeJson) => DadJokeContentData.fromJson(jsonDecode(favoriteDadJokeJson)), 32 | ) 33 | .toList(); 34 | } 35 | 36 | @override 37 | Future setFavoriteDadJokes(List favoriteDadJokes) async { 38 | final sharedPreferences = await SharedPreferences.getInstance(); 39 | await sharedPreferences.setStringList( 40 | _favoriteDadJokesKey, 41 | favoriteDadJokes.map((dadJoke) => jsonEncode(dadJoke)).toList(), 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/app/lib/access/dad_jokes/mocks/dad_jokes_list_mock.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/access/dad_jokes/data/dad_joke_content_data.dart'; 2 | import 'package:app/access/dad_jokes/mocks/dad_jokes_data_mock.dart'; 3 | 4 | List getMockedDadJokesList() { 5 | return mockedDadJokeResponse.dadJokeData.dadJokeChildrenData 6 | .map( 7 | (joke) => (joke.dadJokeContentData), 8 | ) 9 | .toList(); 10 | } 11 | 12 | List getMockedFavoriteDadJokesList() { 13 | return [ 14 | const DadJokeContentData( 15 | id: "17urj7q", 16 | title: 'My wife just completed a 40 week body building program this morning', 17 | selfText: '"It\'s a girl and weighs 7lbs 12 oz."', 18 | ), 19 | ]; 20 | } 21 | -------------------------------------------------------------------------------- /src/app/lib/access/diagnostics/diagnostics_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/access/persistence_exception.dart'; 2 | import 'package:shared_preferences/shared_preferences.dart'; 3 | 4 | /// Interface for Diagnostic Repository. 5 | abstract interface class DiagnosticsRepository { 6 | factory DiagnosticsRepository(bool defaultIsEnabled) = _DiagnosticsRepository; 7 | 8 | /// Gets whether the diagnostic is enabled. 9 | Future isDiagnosticEnabled(); 10 | 11 | /// Sets the diagnostic as dismissed. 12 | Future disableDiagnostics(); 13 | } 14 | 15 | /// Implementation of [DiagnosticRepository]. 16 | final class _DiagnosticsRepository implements DiagnosticsRepository { 17 | /// The key used to store the diagnostic dismissed state in shared preferences. 18 | final String _diagnosticDisabledKey = 'diagnosticDisabled'; 19 | 20 | /// Whether we want the diagnostics to be accessible by default. 21 | final bool _defaultIsEnabled; 22 | 23 | _DiagnosticsRepository(this._defaultIsEnabled); 24 | 25 | @override 26 | Future isDiagnosticEnabled() async { 27 | final sharedPreferences = await SharedPreferences.getInstance(); 28 | final isDiagnosticDisabled = 29 | sharedPreferences.getBool(_diagnosticDisabledKey); 30 | 31 | return isDiagnosticDisabled != null 32 | ? !isDiagnosticDisabled 33 | : _defaultIsEnabled; 34 | } 35 | 36 | @override 37 | Future disableDiagnostics() async { 38 | final sharedPreferences = await SharedPreferences.getInstance(); 39 | var isSaved = await sharedPreferences.setBool( 40 | _diagnosticDisabledKey, 41 | true, 42 | ); 43 | 44 | if (!isSaved) { 45 | throw const PersistenceException(); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/app/lib/access/environment/environment_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/access/persistence_exception.dart'; 2 | import 'package:shared_preferences/shared_preferences.dart'; 3 | 4 | /// Contract repository to handle environment selection. 5 | abstract interface class EnvironmentRepository { 6 | factory EnvironmentRepository() = _EnvironmentRepository; 7 | 8 | /// Loads the current selected environment. 9 | Future getEnvironment(); 10 | 11 | /// Sets the environment to apply on next app launch. 12 | Future setEnvironment(String environment); 13 | } 14 | 15 | /// Implementation of [EnvironmentRepository]. 16 | final class _EnvironmentRepository implements EnvironmentRepository { 17 | /// The key used to store the selected environment in shared preferences. 18 | final String _environmentKey = 'environment'; 19 | 20 | @override 21 | Future setEnvironment(String environment) async { 22 | final sharedPreferences = await SharedPreferences.getInstance(); 23 | var isSaved = 24 | await sharedPreferences.setString(_environmentKey, environment); 25 | 26 | if (!isSaved) { 27 | throw const PersistenceException(); 28 | } 29 | } 30 | 31 | @override 32 | Future getEnvironment() async { 33 | final sharedPreferences = await SharedPreferences.getInstance(); 34 | return sharedPreferences.getString(_environmentKey); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/app/lib/access/forced_update/current_version_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/access/forced_update/data/version.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | import 'package:package_info_plus/package_info_plus.dart'; 4 | 5 | /// Repository to get what is the current version of the app. 6 | abstract interface class CurrentVersionRepository { 7 | factory CurrentVersionRepository() = _CurrentVersionRepository; 8 | 9 | Future getCurrentVersion(); 10 | } 11 | 12 | /// Implementation of [CurrentVersionRepository]. 13 | final class _CurrentVersionRepository implements CurrentVersionRepository { 14 | _CurrentVersionRepository(); 15 | 16 | Version? _currentVersion; 17 | 18 | @override 19 | Future getCurrentVersion() async { 20 | if (_currentVersion == null) { 21 | var packageInfo = await PackageInfo.fromPlatform(); 22 | 23 | _currentVersion = Version.fromString(packageInfo.version); 24 | } 25 | 26 | return SynchronousFuture(_currentVersion!); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/app/lib/access/forced_update/data/version.dart: -------------------------------------------------------------------------------- 1 | final class Version implements Comparable { 2 | final int major; 3 | final int minor; 4 | final int patch; 5 | 6 | /// Optional, depending on your versioning needs. 7 | final int? build; 8 | 9 | const Version(this.major, this.minor, this.patch, [this.build]); 10 | 11 | factory Version.fromString(String versionString) { 12 | final parts = versionString.split('.'); 13 | final major = int.parse(parts[0]); 14 | final minor = int.parse(parts[1]); 15 | final patch = int.parse(parts[2]); 16 | int? build; 17 | 18 | if (parts.length > 3) { 19 | build = int.parse(parts[3]); 20 | } 21 | 22 | return Version(major, minor, patch, build); 23 | } 24 | 25 | @override 26 | String toString() { 27 | if (build != null) { 28 | return '$major.$minor.$patch.$build'; 29 | } 30 | return '$major.$minor.$patch'; 31 | } 32 | 33 | @override 34 | bool operator ==(Object other) { 35 | if (identical(this, other)) return true; 36 | return other is Version && 37 | other.major == major && 38 | other.minor == minor && 39 | other.patch == patch && 40 | other.build == build; 41 | } 42 | 43 | @override 44 | int get hashCode => Object.hash(major, minor, patch, build); 45 | 46 | @override 47 | int compareTo(Version other) { 48 | if (major != other.major) return major.compareTo(other.major); 49 | if (minor != other.minor) return minor.compareTo(other.minor); 50 | if (patch != other.patch) return patch.compareTo(other.patch); 51 | if (build != null && other.build != null) { 52 | return build!.compareTo(other.build!); 53 | } else if (build == null && other.build == null) { 54 | return 0; 55 | } else if (build == null) { 56 | return -1; // Treat no build as lesser. 57 | } else { 58 | return 1; // Treat no build on `other` as lesser. 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/app/lib/access/forced_update/minimum_version_repository.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:app/access/forced_update/data/version.dart'; 4 | 5 | /// Repository that gets the minimum version required to use the application. 6 | abstract interface class MinimumVersionRepository { 7 | /// Stream of the minimum version required to use the application. 8 | Stream get minimumVersionStream; 9 | } 10 | -------------------------------------------------------------------------------- /src/app/lib/access/forced_update/minimum_version_repository_mock.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:app/access/forced_update/data/version.dart'; 4 | import 'package:app/access/forced_update/minimum_version_repository.dart'; 5 | import 'package:get_it/get_it.dart'; 6 | import 'package:rxdart/rxdart.dart'; 7 | 8 | /// Mock implementation of [MinimumVersionRepository]. 9 | final class MinimumVersionRepositoryMock 10 | implements MinimumVersionRepository, Disposable { 11 | final BehaviorSubject _minimumVersionBehaviorSubject = 12 | BehaviorSubject(); 13 | 14 | /// Updates the minimum version required to use the application. 15 | void updateMinimumVersion({Version version = const Version(2, 0, 0)}) { 16 | _minimumVersionBehaviorSubject.add(version); 17 | } 18 | 19 | @override 20 | Stream get minimumVersionStream => 21 | _minimumVersionBehaviorSubject.stream; 22 | 23 | @override 24 | FutureOr onDispose() { 25 | _minimumVersionBehaviorSubject.close(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/app/lib/access/kill_switch/kill_switch_repository.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | /// Repository that emits whether the kill switch of the application is activated. 4 | abstract interface class KillSwitchRepository { 5 | /// Stream that emits the state of the kill switch. 6 | Stream get isKillSwitchActivatedStream; 7 | } 8 | -------------------------------------------------------------------------------- /src/app/lib/access/kill_switch/kill_switch_repository_mock.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:app/access/kill_switch/kill_switch_repository.dart'; 4 | import 'package:get_it/get_it.dart'; 5 | import 'package:rxdart/rxdart.dart'; 6 | 7 | /// Mock implementation of the [KillSwitchRepository]. 8 | final class KillSwitchRepositoryMock 9 | implements KillSwitchRepository, Disposable { 10 | BehaviorSubject isKillSwitchActivatedStreamController = 11 | BehaviorSubject(); 12 | 13 | KillSwitchRepositoryMock() { 14 | isKillSwitchActivatedStreamController.add(isKillSwitchActivated); 15 | } 16 | 17 | bool isKillSwitchActivated = false; 18 | 19 | /// Toggles the state of the kill switch. 20 | void setOrToggleKillSwitchState() { 21 | isKillSwitchActivated = !isKillSwitchActivated; 22 | isKillSwitchActivatedStreamController.add(isKillSwitchActivated); 23 | } 24 | 25 | /// Sets the state of the kill switch. 26 | void setKillSwitchState(bool isActivated) { 27 | isKillSwitchActivated = isActivated; 28 | isKillSwitchActivatedStreamController.add(isKillSwitchActivated); 29 | } 30 | 31 | @override 32 | Stream get isKillSwitchActivatedStream => 33 | isKillSwitchActivatedStreamController.stream; 34 | 35 | @override 36 | FutureOr onDispose() { 37 | isKillSwitchActivatedStreamController.close(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/app/lib/access/logger/alice_output.dart: -------------------------------------------------------------------------------- 1 | import 'package:alice/alice.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | import 'package:logger/logger.dart'; 4 | 5 | /// Custom implementation of [LogOutput] to add [Alice] logs writting support. 6 | final class AliceOutput extends LogOutput { 7 | final Alice _alice; 8 | 9 | AliceOutput({required Alice alice}) 10 | : _alice = alice, 11 | super(); 12 | 13 | DiagnosticLevel _mapLogLevel(Level level) { 14 | switch (level) { 15 | case Level.trace: 16 | return DiagnosticLevel.debug; 17 | case Level.debug: 18 | return DiagnosticLevel.debug; 19 | case Level.info: 20 | return DiagnosticLevel.info; 21 | case Level.warning: 22 | return DiagnosticLevel.warning; 23 | case Level.error: 24 | return DiagnosticLevel.error; 25 | case Level.fatal: 26 | return DiagnosticLevel.error; 27 | default: 28 | return DiagnosticLevel.debug; 29 | } 30 | } 31 | 32 | @override 33 | void output(OutputEvent event) { 34 | _alice.addLog( 35 | AliceLog( 36 | level: _mapLogLevel(event.origin.level), 37 | message: event.origin.message, 38 | error: event.origin.error, 39 | stackTrace: event.origin.stackTrace, 40 | ), 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/app/lib/access/logger/custom_console_output.dart: -------------------------------------------------------------------------------- 1 | import 'dart:developer'; 2 | 3 | import 'package:logger/logger.dart'; 4 | 5 | /// Workaround for colorization on iOS. 6 | /// See https://github.com/flutter/flutter/issues/64491 for more details. 7 | final class CustomConsoleOutput extends LogOutput { 8 | @override 9 | void output(OutputEvent event) { 10 | final StringBuffer buffer = StringBuffer(); 11 | event.lines.forEach(buffer.writeln); 12 | log(buffer.toString()); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/app/lib/access/logger/custom_file_output.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | import 'package:logger/logger.dart'; 5 | 6 | /// Custom implementation of [LogOutput] to write the log output into a file. 7 | final class CustomFileOutput extends LogOutput { 8 | final File _file; 9 | final bool _overrideExisting; 10 | final Encoding _encoding; 11 | 12 | /// Regex used to remove ansi color codes from from log files. 13 | final _ansiPattern = RegExp(r'\x1B\[\d+(;\d+)*m'); 14 | 15 | /// It's a reference to [_file] used to write logs inside. 16 | IOSink? _sink; 17 | 18 | CustomFileOutput({ 19 | required File file, 20 | bool overrideExisting = false, 21 | Encoding encoding = utf8, 22 | }) : _encoding = encoding, 23 | _overrideExisting = overrideExisting, 24 | _file = file; 25 | 26 | @override 27 | Future init() async { 28 | _sink = _file.openWrite( 29 | mode: _overrideExisting ? FileMode.writeOnly : FileMode.writeOnlyAppend, 30 | encoding: _encoding, 31 | ); 32 | } 33 | 34 | @override 35 | void output(OutputEvent event) async { 36 | // File may have been deleted. 37 | if (!_file.existsSync()) { 38 | await _file.create(); 39 | await destroy(); 40 | await init(); 41 | } 42 | 43 | _sink?.writeAll( 44 | event.lines.map((line) => line.replaceAll(_ansiPattern, '')), '\n'); 45 | _sink?.writeln(); 46 | } 47 | 48 | @override 49 | Future destroy() async { 50 | await _sink?.flush(); 51 | await _sink?.close(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/app/lib/access/logger/logger_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/access/logger/logging_configuration_data.dart'; 2 | import 'package:app/access/persistence_exception.dart'; 3 | import 'package:shared_preferences/shared_preferences.dart'; 4 | 5 | /// Contract repository to handle logging configuration. 6 | abstract interface class LoggerRepository { 7 | factory LoggerRepository() = _LoggerRepository; 8 | 9 | /// Loads the current logging configuration. 10 | Future getLoggingConfiguration(); 11 | 12 | /// Sets whether console logging should be enabled on next app launch. 13 | Future setIsConsoleLoggingEnabled(bool isConsoleLoggingEnabled); 14 | 15 | /// Sets whether file logging should be enabled on next app launch. 16 | Future setIsFileLoggingEnabled(bool isFileLoggingEnabled); 17 | } 18 | 19 | /// Implementation of [LoggerRepository]. 20 | final class _LoggerRepository implements LoggerRepository { 21 | /// The key used to store the selected environment in shared preferences. 22 | final String _consoleLoggingKey = 'consoleLogging'; 23 | final String _fileloggingKey = 'fileLogging'; 24 | 25 | @override 26 | Future getLoggingConfiguration() async { 27 | final sharedPreferences = await SharedPreferences.getInstance(); 28 | return LoggingConfigurationData( 29 | isConsoleLoggingEnabled: sharedPreferences.getBool(_consoleLoggingKey), 30 | isFileLoggingEnabled: sharedPreferences.getBool(_fileloggingKey), 31 | ); 32 | } 33 | 34 | @override 35 | Future setIsConsoleLoggingEnabled(bool isConsoleLoggingEnabled) async { 36 | final sharedPreferences = await SharedPreferences.getInstance(); 37 | var isSaved = await sharedPreferences.setBool( 38 | _consoleLoggingKey, 39 | isConsoleLoggingEnabled, 40 | ); 41 | 42 | if (!isSaved) { 43 | throw const PersistenceException(); 44 | } 45 | } 46 | 47 | @override 48 | Future setIsFileLoggingEnabled(bool isFileLoggingEnabled) async { 49 | final sharedPreferences = await SharedPreferences.getInstance(); 50 | var isSaved = await sharedPreferences.setBool( 51 | _fileloggingKey, 52 | isFileLoggingEnabled, 53 | ); 54 | 55 | if (!isSaved) { 56 | throw const PersistenceException(); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/app/lib/access/logger/logging_configuration_data.dart: -------------------------------------------------------------------------------- 1 | /// Entity that represents the current state of the logging configuration persisted. 2 | final class LoggingConfigurationData { 3 | /// Gets whether console logging is enabled. If [Null], it fallbacks to the value set from the current environment. 4 | final bool? isConsoleLoggingEnabled; 5 | 6 | /// Gets whether file logging is enabled. If [Null], it fallbacks to the value set from the current environment. 7 | final bool? isFileLoggingEnabled; 8 | 9 | const LoggingConfigurationData({ 10 | required this.isConsoleLoggingEnabled, 11 | required this.isFileLoggingEnabled, 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /src/app/lib/access/mocking/mocking_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:shared_preferences/shared_preferences.dart'; 2 | 3 | /// Defines the repository for mocking data throughout the app. 4 | abstract interface class MockingRepository { 5 | factory MockingRepository() = _MockingRepository; 6 | 7 | /// Gets whether the mocking is enabled. 8 | Future isMockingEnabled(); 9 | 10 | /// Sets the mocking state. 11 | Future setMocking(bool enabled); 12 | } 13 | 14 | /// Implementation of [MockingRepository]. 15 | final class _MockingRepository implements MockingRepository { 16 | /// The key used to store the mocking state in shared preferences. 17 | final String _mockingEnabledKey = 'mockingEnabled'; 18 | 19 | @override 20 | Future isMockingEnabled() async { 21 | final sharedPreferences = await SharedPreferences.getInstance(); 22 | return sharedPreferences.getBool(_mockingEnabledKey) ?? false; 23 | } 24 | 25 | @override 26 | Future setMocking(bool enabled) async { 27 | final sharedPreferences = await SharedPreferences.getInstance(); 28 | await sharedPreferences.setBool(_mockingEnabledKey, enabled); 29 | } 30 | } -------------------------------------------------------------------------------- /src/app/lib/access/persistence_exception.dart: -------------------------------------------------------------------------------- 1 | /// Exception thrown when something couldn't be persisted in the shared preference. 2 | /// It was created due to https://github.com/flutter/flutter/issues/146070. 3 | final class PersistenceException implements Exception { 4 | /// A descriptive message detailing the persistence exception 5 | final String? message; 6 | 7 | const PersistenceException({this.message}); 8 | } 9 | -------------------------------------------------------------------------------- /src/app/lib/app.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/app_router.dart'; 2 | import 'package:app/l10n/gen_l10n/app_localizations.dart'; 3 | import 'package:app/presentation/styles/global_theme_data.dart'; 4 | import 'package:flutter/material.dart'; 5 | 6 | final class App extends StatelessWidget { 7 | const App({super.key}); 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | return MaterialApp.router( 12 | theme: GlobalThemeData.lightThemeData, 13 | darkTheme: GlobalThemeData.darkThemeData, 14 | routerConfig: router, 15 | localizationsDelegates: AppLocalizations.localizationsDelegates, 16 | supportedLocales: AppLocalizations.supportedLocales, 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/app/lib/app_shell.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/l10n/localization_extensions.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:go_router/go_router.dart'; 4 | 5 | /// The shell of the application. 6 | final class AppShell extends StatelessWidget { 7 | const AppShell({super.key, required this.navigationShell}); 8 | 9 | final StatefulNavigationShell navigationShell; 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return Scaffold( 14 | body: navigationShell, 15 | bottomNavigationBar: BottomNavigationBar( 16 | items: [ 17 | BottomNavigationBarItem( 18 | icon: const Icon(Icons.theater_comedy), 19 | label: context.local.dadJokesPageLabel, 20 | ), 21 | BottomNavigationBarItem( 22 | icon: const Icon(Icons.favorite), 23 | label: context.local.favoriteDadJokesPageLabel, 24 | ), 25 | ], 26 | currentIndex: navigationShell.currentIndex, 27 | onTap: navigationShell.goBranch, 28 | ), 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/app/lib/business/dad_jokes/dad_joke.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/access/dad_jokes/data/dad_joke_content_data.dart'; 2 | import 'package:equatable/equatable.dart'; 3 | 4 | /// Represents a dad joke. 5 | final class DadJoke extends Equatable { 6 | final String id; 7 | final String title; 8 | final String text; 9 | final bool isFavorite; 10 | 11 | const DadJoke({ 12 | required this.id, 13 | required this.title, 14 | required this.text, 15 | required this.isFavorite, 16 | }); 17 | 18 | DadJoke.fromData(DadJokeContentData dadJokeContentData, {required bool isFavorite}) : this( 19 | id: dadJokeContentData.id, 20 | title: dadJokeContentData.title, 21 | text: dadJokeContentData.selfText, 22 | isFavorite: isFavorite, 23 | ); 24 | 25 | DadJoke copyWith({ 26 | String? id, 27 | String? title, 28 | String? text, 29 | bool? isFavorite, 30 | }) => 31 | DadJoke( 32 | id: id ?? this.id, 33 | title: title ?? this.title, 34 | text: text ?? this.text, 35 | isFavorite: isFavorite ?? this.isFavorite, 36 | ); 37 | 38 | @override 39 | List get props => [id, title, text, isFavorite]; 40 | } 41 | -------------------------------------------------------------------------------- /src/app/lib/business/diagnostics/diagnostics_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/access/diagnostics/diagnostics_repository.dart'; 2 | 3 | /// Interface for the Diagnostic Service. 4 | abstract interface class DiagnosticsService { 5 | factory DiagnosticsService(DiagnosticsRepository diagnosticRepository) = 6 | _DiagnosticsService; 7 | 8 | /// Gets whether the diagnostic was dismissed. 9 | Future checkDiagnosticDismissal(); 10 | 11 | /// Sets the diagnostic as dismissed only for the remaining of the app cycle. 12 | void dismissDiagnostics(); 13 | 14 | /// Disable the diagnostic overlay permanently. 15 | Future disableDiagnostics(); 16 | } 17 | 18 | /// Implementation of [DiagnosticService]. 19 | final class _DiagnosticsService implements DiagnosticsService { 20 | final DiagnosticsRepository _diagnosticRepository; 21 | 22 | bool _isDismissed = false; 23 | 24 | _DiagnosticsService(DiagnosticsRepository diagnosticRepository) 25 | : _diagnosticRepository = diagnosticRepository; 26 | 27 | @override 28 | Future checkDiagnosticDismissal() async { 29 | final isEnabled = await _diagnosticRepository.isDiagnosticEnabled(); 30 | return !isEnabled || _isDismissed; 31 | } 32 | 33 | @override 34 | void dismissDiagnostics() { 35 | _isDismissed = true; 36 | } 37 | 38 | @override 39 | Future disableDiagnostics() { 40 | return _diagnosticRepository.disableDiagnostics(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/app/lib/business/environment/environment.dart: -------------------------------------------------------------------------------- 1 | /// Enum representing all available environments. 2 | enum Environment { 3 | development, 4 | staging, 5 | production; 6 | 7 | @override 8 | String toString() => switch (this) { 9 | development => 'Development', 10 | staging => 'Staging', 11 | production => 'Production', 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /src/app/lib/business/environment/environment_manager.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/access/environment/environment_repository.dart'; 2 | import 'package:app/business/environment/environment.dart'; 3 | import 'package:flutter_dotenv/flutter_dotenv.dart'; 4 | 5 | /// Service to use as the source of truth for application environment (e.g. Staging, Production, etc.). 6 | abstract interface class EnvironmentManager { 7 | factory EnvironmentManager(EnvironmentRepository repository) = 8 | _EnvironmentManager; 9 | 10 | /// Gets the current environment (which can be overridden). 11 | Environment get current; 12 | 13 | /// Gets the next environment that will become the current one when the application restarts. 14 | /// This value changes when using [setEnvironment]. 15 | Environment? get next; 16 | 17 | // Gets a list of all environments. 18 | List get environments; 19 | 20 | /// Loads the environment. 21 | Future load(String defaultEnvironment); 22 | 23 | /// Sets the environment to apply on next app launch. 24 | Future setEnvironment(Environment environment); 25 | } 26 | 27 | /// Implementation of [EnvironmentManager]. 28 | final class _EnvironmentManager implements EnvironmentManager { 29 | /// Map of environment names to their respective environment file names. 30 | final Map _environmentFileNames = { 31 | Environment.development: '.env.dev', 32 | Environment.staging: '.env.staging', 33 | Environment.production: '.env.prod', 34 | }; 35 | 36 | @override 37 | late List environments; 38 | 39 | @override 40 | late Environment current; 41 | 42 | @override 43 | late Environment? next; 44 | 45 | final EnvironmentRepository _repository; 46 | 47 | _EnvironmentManager(this._repository); 48 | 49 | @override 50 | Future load(String defaultEnvironment) async { 51 | environments = _environmentFileNames.entries.map((e) => e.key).toList(); 52 | 53 | final persistedEnvironment = await _repository.getEnvironment(); 54 | final selectedEnvironment = persistedEnvironment ?? defaultEnvironment; 55 | current = Environment.values.firstWhere( 56 | (environment) => environment.toString() == selectedEnvironment, 57 | ); 58 | next = null; 59 | 60 | await dotenv.load( 61 | fileName: _environmentFileNames[current]!, 62 | ); 63 | } 64 | 65 | @override 66 | Future setEnvironment(Environment environment) async { 67 | await _repository.setEnvironment(environment.toString()); 68 | next = environment; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/app/lib/business/forced_update/update_required_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:app/access/forced_update/current_version_repository.dart'; 4 | import 'package:app/access/forced_update/minimum_version_repository.dart'; 5 | 6 | /// This service checks if the application is running the minimum required version. 7 | abstract interface class UpdateRequiredService { 8 | factory UpdateRequiredService( 9 | MinimumVersionRepository minimumVersionRepository, 10 | CurrentVersionRepository currentVersionService, 11 | ) = _UpdateRequiredService; 12 | 13 | /// Waits for the minimum required version to be higher than the current version. 14 | /// We use a Future since this is a one-time check. 15 | Future waitForUpdateRequired(); 16 | } 17 | 18 | /// Implementation of [UpdateRequiredService]. 19 | final class _UpdateRequiredService implements UpdateRequiredService { 20 | final MinimumVersionRepository _minimumVersionRepository; 21 | final CurrentVersionRepository _currentVersionService; 22 | 23 | _UpdateRequiredService( 24 | this._minimumVersionRepository, 25 | this._currentVersionService, 26 | ); 27 | 28 | @override 29 | Future waitForUpdateRequired() async { 30 | final currentVersion = await _currentVersionService.getCurrentVersion(); 31 | 32 | return _minimumVersionRepository.minimumVersionStream 33 | .where((minVersion) => currentVersion.compareTo(minVersion) < 0) 34 | .first 35 | .then((_) {}); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/app/lib/business/kill_switch/kill_switch_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:app/access/kill_switch/kill_switch_repository.dart'; 4 | 5 | /// Repository that emits whether the kill switch of the application is activated. 6 | abstract interface class KillSwitchService { 7 | factory KillSwitchService( 8 | KillSwitchRepository killSwitchRepository, 9 | ) = _KillSwitchService; 10 | 11 | /// Stream that emits the state of the kill switch. 12 | Stream isKillSwitchActivatedStream(); 13 | } 14 | 15 | /// Implementation of [KillSwitchService]. 16 | final class _KillSwitchService implements KillSwitchService { 17 | final KillSwitchRepository _killSwitchRepository; 18 | 19 | _KillSwitchService( 20 | KillSwitchRepository killSwitchRepository, 21 | ) : _killSwitchRepository = killSwitchRepository; 22 | 23 | @override 24 | Stream isKillSwitchActivatedStream() { 25 | return _killSwitchRepository.isKillSwitchActivatedStream; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/app/lib/business/logger/level_log_filter.dart: -------------------------------------------------------------------------------- 1 | import 'package:logger/logger.dart'; 2 | 3 | /// Custom implementation of [LogFilter] to ensure logging for a given minimum[Level]. 4 | final class LevelLogFilter extends LogFilter { 5 | Level minimumLevel; 6 | 7 | LevelLogFilter(this.minimumLevel); 8 | 9 | @override 10 | bool shouldLog(LogEvent event) { 11 | return event.level.index >= minimumLevel.index; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/app/lib/business/mocking/mocking_manager.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/access/mocking/mocking_repository.dart'; 2 | 3 | /// Service that manages everything related to mocking. 4 | abstract interface class MockingManager { 5 | factory MockingManager(MockingRepository mockingRepository) = _MockingManager; 6 | 7 | /// Gets whether file logging is enabled. 8 | bool get isMockingEnabled; 9 | 10 | /// Gets whether mocking configuration been changed via either [setIsConsoleLoggingEnabled]. 11 | bool get hasConfigurationBeenChanged; 12 | 13 | /// Sets whether mocking should be enabled on next app launch. 14 | Future setIsMockingEnabled(bool isMockingEnabled); 15 | 16 | /// Initializes the mocking manager. 17 | Future initialize(); 18 | } 19 | 20 | /// Implementation of [MockingManager]. 21 | /// This class is responsible for managing the mocking configuration. 22 | final class _MockingManager implements MockingManager { 23 | final MockingRepository _mockingRepository; 24 | 25 | late bool _initialIsMockingEnabled; 26 | 27 | _MockingManager(this._mockingRepository); 28 | 29 | @override 30 | bool hasConfigurationBeenChanged = false; 31 | 32 | @override 33 | late bool isMockingEnabled; 34 | 35 | @override 36 | Future initialize() async { 37 | _initialIsMockingEnabled = await _mockingRepository.isMockingEnabled(); 38 | isMockingEnabled = _initialIsMockingEnabled; 39 | } 40 | 41 | @override 42 | Future setIsMockingEnabled(bool isMockingEnabled) async { 43 | await _mockingRepository.setMocking(isMockingEnabled); 44 | 45 | this.isMockingEnabled = isMockingEnabled; 46 | hasConfigurationBeenChanged = isMockingEnabled != _initialIsMockingEnabled; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/app/lib/l10n/arb/app_en.arb: -------------------------------------------------------------------------------- 1 | { 2 | "dadJokesPageTitle": "Dad Jokes", 3 | "favoriteDadJokesPageTitle": "Dad Jokes", 4 | "dadJokesPageLabel": "Dad Jokes", 5 | "favoriteDadJokesPageLabel": "Favorites", 6 | "error": "Error: {error}", 7 | "forcedUpdatePageTitle": "Forced Update Page", 8 | "forcedUpdatePageUpdateRequiredMessage": "An update is required to continue using the application.", 9 | "forcedUpdatePageUpdateButton": "Update", 10 | "forcedUpdatePageUrlLaunchException": "Could not launch {url}", 11 | "killSwitchPageTitle": "Kill Switch Page", 12 | "killSwitchPageMessage": "The app is not available at the moment, please come back later." 13 | } -------------------------------------------------------------------------------- /src/app/lib/l10n/arb/app_fr.arb: -------------------------------------------------------------------------------- 1 | { 2 | "dadJokesPageTitle": "Blagues de Papa", 3 | "favoriteDadJokesPageTitle": "Blagues de Papa", 4 | "dadJokesPageLabel": "Blagues de Papa", 5 | "favoriteDadJokesPageLabel": "Favoris", 6 | "error": "Erreur: {error}", 7 | "forcedUpdatePageTitle": "Page de mise à jour forcée", 8 | "forcedUpdatePageUpdateRequiredMessage": "Une mise à jour est nécessaire pour continuer à utiliser l'application.", 9 | "forcedUpdatePageUpdateButton": "Mettre à jour", 10 | "forcedUpdatePageUrlLaunchException": "Impossible de lancer {url}", 11 | "killSwitchPageTitle": "Kill Switch Page", 12 | "killSwitchPageMessage": "L'application n'est pas disponible pour le moment, veuillez revenir plus tard." 13 | } -------------------------------------------------------------------------------- /src/app/lib/l10n/localization_extensions.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/l10n/gen_l10n/app_localizations.dart'; 2 | import 'package:flutter/widgets.dart'; 3 | 4 | extension LocalizationExtensions on BuildContext { 5 | AppLocalizations get local => AppLocalizations.of(this); 6 | } 7 | -------------------------------------------------------------------------------- /src/app/lib/presentation/dad_jokes/dad_joke_list_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/business/dad_jokes/dad_joke.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | /// A dad joke list item. 5 | final class DadJokeListItem extends StatelessWidget { 6 | /// The dad joke. 7 | final DadJoke dadJoke; 8 | final Future Function(DadJoke dadJoke) toggleIsFavorite; 9 | 10 | const DadJokeListItem({super.key, required this.dadJoke, required this.toggleIsFavorite}); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return Card( 15 | child: ListTile( 16 | title: Text(dadJoke.title), 17 | subtitle: Text(dadJoke.text), 18 | trailing: dadJoke.isFavorite 19 | ? const Icon( 20 | Icons.favorite, 21 | color: Colors.red, 22 | ) 23 | : const Icon( 24 | Icons.favorite_border, 25 | ), 26 | titleAlignment: ListTileTitleAlignment.top, 27 | contentPadding: const EdgeInsets.all(16), 28 | onTap: () => toggleIsFavorite(dadJoke), 29 | ), 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/app/lib/presentation/dad_jokes/dad_jokes_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/l10n/localization_extensions.dart'; 2 | import 'package:app/presentation/dad_jokes/dad_joke_list_item.dart'; 3 | import 'package:app/presentation/dad_jokes/dad_jokes_page_viewmodel.dart'; 4 | import 'package:app/presentation/mvvm/mvvm_widget.dart'; 5 | import 'package:flutter/material.dart'; 6 | 7 | /// The dad jokes page. 8 | final class DadJokesPage extends MvvmWidget { 9 | const DadJokesPage({super.key}); 10 | 11 | @override 12 | DadJokesPageViewModel getViewModel() { 13 | return DadJokesPageViewModel(); 14 | } 15 | 16 | @override 17 | Widget build(BuildContext context, DadJokesPageViewModel viewModel) { 18 | final local = context.local; 19 | return Scaffold( 20 | appBar: AppBar( 21 | title: Text(local.dadJokesPageTitle), 22 | ), 23 | body: 24 | StreamBuilder(stream: viewModel.dadJokesStream, builder: (context, snapshot) { 25 | if (snapshot.hasData) { 26 | final dadJokes = snapshot.data; 27 | return Container( 28 | key: const Key('DadJokesContainer'), 29 | child: ListView.builder( 30 | itemCount: dadJokes?.length ?? 0, 31 | itemBuilder: (context, index) { 32 | final dadJoke = dadJokes![index]; 33 | return DadJokeListItem( 34 | dadJoke: dadJoke, 35 | toggleIsFavorite: viewModel.toggleIsFavorite, 36 | ); 37 | }, 38 | ), 39 | ); 40 | } else if (snapshot.hasError) { 41 | return Text( 42 | local.error(snapshot.error!), 43 | ); 44 | } else { 45 | return const Center( 46 | child: CircularProgressIndicator(), 47 | ); 48 | } 49 | }), 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/app/lib/presentation/dad_jokes/dad_jokes_page_viewmodel.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/business/dad_jokes/dad_joke.dart'; 2 | import 'package:app/business/dad_jokes/dad_jokes_service.dart'; 3 | import 'package:app/presentation/mvvm/view_model.dart'; 4 | import 'package:get_it/get_it.dart'; 5 | 6 | class DadJokesPageViewModel extends ViewModel { 7 | final _dadJokesService = GetIt.I(); 8 | 9 | Stream> get dadJokesStream => 10 | getLazy("dadJokesStream", () => _dadJokesService.dadJokesStream); 11 | 12 | Future toggleIsFavorite(DadJoke dadJoke) async { 13 | if (dadJoke.isFavorite) { 14 | await _dadJokesService.removeFavoriteDadJoke(dadJoke); 15 | } else { 16 | await _dadJokesService.addFavoriteDadJoke(dadJoke); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/app/lib/presentation/dad_jokes/favorite_dad_jokes.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/l10n/localization_extensions.dart'; 2 | import 'package:app/presentation/dad_jokes/dad_joke_list_item.dart'; 3 | import 'package:app/presentation/dad_jokes/favorite_dad_jokes_viewmodel.dart'; 4 | import 'package:app/presentation/mvvm/mvvm_widget.dart'; 5 | import 'package:flutter/material.dart'; 6 | 7 | /// The favorite dad jokes page. 8 | final class FavoriteDadJokesPage extends MvvmWidget { 9 | const FavoriteDadJokesPage({super.key}); 10 | 11 | @override 12 | FavoriteDadJokesViewModel getViewModel() { 13 | return FavoriteDadJokesViewModel(); 14 | } 15 | 16 | @override 17 | Widget build(BuildContext context, FavoriteDadJokesViewModel viewModel) { 18 | final local = context.local; 19 | return Scaffold( 20 | appBar: AppBar( 21 | title: Text(local.favoriteDadJokesPageTitle), 22 | ), 23 | body: StreamBuilder( 24 | stream: viewModel.favorites, 25 | builder: (context, snapshot) { 26 | if (snapshot.hasData) { 27 | final dadJokes = snapshot.data; 28 | return Container( 29 | key: const Key("FavoriteJokesContainer"), 30 | child: ListView.builder( 31 | itemCount: dadJokes?.length ?? 0, 32 | itemBuilder: (context, index) { 33 | final dadJoke = dadJokes![index]; 34 | return DadJokeListItem( 35 | dadJoke: dadJoke, 36 | toggleIsFavorite: viewModel.toggleIsFavorite, 37 | ); 38 | }, 39 | ), 40 | ); 41 | } else if (snapshot.hasError) { 42 | return Text( 43 | local.error(snapshot.error!), 44 | ); 45 | } else { 46 | return const Center( 47 | child: CircularProgressIndicator(), 48 | ); 49 | } 50 | }), 51 | ); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/app/lib/presentation/dad_jokes/favorite_dad_jokes_viewmodel.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/business/dad_jokes/dad_joke.dart'; 2 | import 'package:app/business/dad_jokes/dad_jokes_service.dart'; 3 | import 'package:app/presentation/mvvm/view_model.dart'; 4 | import 'package:get_it/get_it.dart'; 5 | 6 | class FavoriteDadJokesViewModel extends ViewModel { 7 | final _dadJokesService = GetIt.I(); 8 | 9 | Stream> get favorites => getLazy( 10 | "favorites", 11 | () => _dadJokesService.dadJokesStream.map((List jokes) => 12 | jokes.where((DadJoke joke) => joke.isFavorite).toList())); 13 | 14 | Future toggleIsFavorite(DadJoke dadJoke) async { 15 | if (dadJoke.isFavorite) { 16 | await _dadJokesService.removeFavoriteDadJoke(dadJoke); 17 | } else { 18 | await _dadJokesService.addFavoriteDadJoke(dadJoke); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/app/lib/presentation/diagnostic/diagnostic_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | /// A button used for the diagnostic overlay. 4 | final class DiagnosticButton extends StatelessWidget { 5 | final String label; 6 | final VoidCallback onPressed; 7 | 8 | const DiagnosticButton({ 9 | super.key, 10 | required this.label, 11 | required this.onPressed, 12 | }); 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | return TextButton( 17 | style: TextButton.styleFrom( 18 | foregroundColor: Colors.white, 19 | backgroundColor: Colors.black, 20 | minimumSize: const Size(68, 44), 21 | padding: const EdgeInsets.symmetric(horizontal: 4), 22 | shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero), 23 | ), 24 | onPressed: onPressed, 25 | child: Text(label), 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/app/lib/presentation/diagnostic/diagnostic_switch.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | /// A switch widget used for the diagnostic overlay. 4 | final class DiagnosticSwitch extends StatelessWidget { 5 | const DiagnosticSwitch({ 6 | super.key, 7 | required this.label, 8 | required this.value, 9 | required this.onChanged, 10 | }); 11 | 12 | final String label; 13 | final bool value; 14 | final void Function(bool) onChanged; 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return Material( 19 | child: Row( 20 | mainAxisSize: MainAxisSize.max, 21 | children: [ 22 | Expanded( 23 | child: Text( 24 | label, 25 | style: const TextStyle( 26 | color: Colors.black, 27 | fontSize: 16, 28 | fontWeight: FontWeight.normal, 29 | decoration: TextDecoration.none, 30 | ), 31 | ), 32 | ), 33 | Expanded( 34 | child: Align( 35 | alignment: Alignment.centerRight, 36 | child: Switch( 37 | value: value, 38 | onChanged: onChanged, 39 | ), 40 | ), 41 | ), 42 | ], 43 | ), 44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/app/lib/presentation/diagnostic/diagnostic_text.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | /// A text widget used for the diagnostic overlay. 4 | final class DiagnosticText extends StatelessWidget { 5 | final String text; 6 | final Color? color; 7 | 8 | const DiagnosticText({super.key, required this.text, this.color}); 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return Text( 13 | text, 14 | style: Theme.of(context) 15 | .textTheme 16 | .bodySmall! 17 | .copyWith(color: color ?? Colors.white), 18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/app/lib/presentation/diagnostic/environment_diagnostic_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/business/diagnostics/diagnostics_service.dart'; 2 | import 'package:app/presentation/diagnostic/diagnostic_button.dart'; 3 | import 'package:app/presentation/diagnostic/environment_picker_widget.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:get_it/get_it.dart'; 6 | 7 | /// A widget that displays the current environment. 8 | final class EnvironmentDiagnosticWidget extends StatelessWidget { 9 | final DiagnosticsService _diagnosticsService = 10 | GetIt.I.get(); 11 | 12 | EnvironmentDiagnosticWidget({super.key}); 13 | 14 | void _disableDiagnostics(BuildContext context) async { 15 | var hasUserAgreed = await showDialog( 16 | context: context, 17 | builder: (ctx) => AlertDialog.adaptive( 18 | title: const Text("Disable diagnostic"), 19 | content: const Text( 20 | "Are you sure you want to disable the diagnostic? If you proceed, you will need to reinstall the app to regain access to it.", 21 | ), 22 | actions: [ 23 | TextButton( 24 | onPressed: () => Navigator.of(context).pop(false), 25 | child: const Text("Cancel"), 26 | ), 27 | TextButton( 28 | onPressed: () => Navigator.of(context).pop(true), 29 | child: const Text("Ok"), 30 | ), 31 | ], 32 | ), 33 | ); 34 | 35 | if (hasUserAgreed ?? false) { 36 | await _diagnosticsService.disableDiagnostics(); 37 | } 38 | } 39 | 40 | @override 41 | Widget build(BuildContext context) { 42 | return Column( 43 | crossAxisAlignment: CrossAxisAlignment.stretch, 44 | children: [ 45 | const Text( 46 | "Environment", 47 | textAlign: TextAlign.left, 48 | style: TextStyle( 49 | color: Colors.white, 50 | fontSize: 20, 51 | fontWeight: FontWeight.normal, 52 | decoration: TextDecoration.none, 53 | ), 54 | ), 55 | const SizedBox(height: 8), 56 | const EnvironmentPickerWidget(), 57 | const SizedBox(height: 8), 58 | DiagnosticButton( 59 | label: "DISABLE DIAGNOSTIC", 60 | onPressed: () => _disableDiagnostics(context), 61 | ), 62 | ], 63 | ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/app/lib/presentation/diagnostic/environment_picker_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/business/environment/environment_manager.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:get_it/get_it.dart'; 4 | 5 | /// Widget that handles environment switching. 6 | final class EnvironmentPickerWidget extends StatefulWidget { 7 | const EnvironmentPickerWidget({super.key}); 8 | 9 | @override 10 | State createState() { 11 | return _EnvironmentPickerWidgetState(); 12 | } 13 | } 14 | 15 | class _EnvironmentPickerWidgetState extends State { 16 | final _environmentManager = GetIt.I(); 17 | 18 | bool _restartRequired = false; 19 | late Enum selectedEnviroment; 20 | 21 | _EnvironmentPickerWidgetState() { 22 | selectedEnviroment = 23 | _environmentManager.next ?? _environmentManager.current; 24 | _restartRequired = selectedEnviroment != _environmentManager.current; 25 | } 26 | 27 | @override 28 | Widget build(BuildContext context) { 29 | return Column( 30 | crossAxisAlignment: CrossAxisAlignment.stretch, 31 | children: [ 32 | if (_restartRequired) 33 | Container( 34 | color: const Color.fromARGB(170, 255, 0, 0), 35 | child: const Text( 36 | "Environment changed. Please restart the application to apply the changes.", 37 | style: TextStyle( 38 | color: Colors.white, 39 | fontSize: 20, 40 | fontWeight: FontWeight.bold, 41 | decoration: TextDecoration.none, 42 | ), 43 | ), 44 | ), 45 | ..._environmentManager.environments.map( 46 | (environment) => Material( 47 | child: CheckboxListTile( 48 | value: environment == selectedEnviroment, 49 | key: ValueKey(environment), 50 | title: Text(environment.toString()), 51 | onChanged: (value) async { 52 | _environmentManager.setEnvironment(environment); 53 | 54 | setState(() { 55 | selectedEnviroment = environment; 56 | _restartRequired = environment != _environmentManager.current; 57 | }); 58 | }, 59 | ), 60 | ), 61 | ), 62 | ], 63 | ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/app/lib/presentation/diagnostic/mocking_configuration_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/business/mocking/mocking_manager.dart'; 2 | import 'package:app/presentation/diagnostic/diagnostic_switch.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | /// Widget that handles logging configuration switching. 6 | final class MockingConfigurationWidget extends StatefulWidget { 7 | final MockingManager _mockingManager; 8 | 9 | const MockingConfigurationWidget(MockingManager mockingManager, {super.key}) : _mockingManager = mockingManager; 10 | 11 | @override 12 | State createState() { 13 | return _MockingConfigurationWidgetState(); 14 | } 15 | } 16 | 17 | final class _MockingConfigurationWidgetState 18 | extends State { 19 | bool _restartRequired = false; 20 | late MockingManager _mockingManager; 21 | 22 | @override 23 | void initState() { 24 | super.initState(); 25 | _mockingManager = widget._mockingManager; 26 | _restartRequired = _mockingManager.hasConfigurationBeenChanged; 27 | } 28 | 29 | @override 30 | Widget build(BuildContext context) { 31 | return Column( 32 | crossAxisAlignment: CrossAxisAlignment.stretch, 33 | children: [ 34 | if (_restartRequired) 35 | Container( 36 | color: const Color.fromARGB(170, 255, 0, 0), 37 | child: const Text( 38 | "Mocking configuration changed. Please restart the application to apply the changes.", 39 | style: TextStyle( 40 | color: Colors.white, 41 | fontSize: 20, 42 | fontWeight: FontWeight.bold, 43 | decoration: TextDecoration.none, 44 | ), 45 | ), 46 | ), 47 | DiagnosticSwitch( 48 | label: 'Data mocking', 49 | value: _mockingManager.isMockingEnabled, 50 | onChanged: (value) async { 51 | await _mockingManager.setIsMockingEnabled(value); 52 | 53 | setState(() { 54 | _restartRequired = _mockingManager.hasConfigurationBeenChanged; 55 | }); 56 | }, 57 | ), 58 | ], 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/app/lib/presentation/diagnostic/mocking_diagnostic_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/business/mocking/mocking_manager.dart'; 2 | import 'package:app/presentation/diagnostic/mocking_configuration_widget.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:get_it/get_it.dart'; 5 | 6 | final class MockingDiagnosticWidget extends StatefulWidget { 7 | const MockingDiagnosticWidget({super.key}); 8 | 9 | @override 10 | _MockingDiagnosticWidgetState createState() => 11 | _MockingDiagnosticWidgetState(); 12 | } 13 | 14 | final class _MockingDiagnosticWidgetState extends State { 15 | final MockingManager _mockingManager = GetIt.I.get(); 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | return Column( 20 | children: [ 21 | const Row( 22 | children: [ 23 | Text( 24 | 'Mocking', 25 | style: TextStyle( 26 | color: Colors.white, 27 | fontSize: 20, 28 | fontWeight: FontWeight.normal, 29 | decoration: TextDecoration.none, 30 | ), 31 | ), 32 | ], 33 | ), 34 | MockingConfigurationWidget(_mockingManager), 35 | const SizedBox(height: 8.0), 36 | ], 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/app/lib/presentation/diagnostic/navigation_diagnostic_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/access/forced_update/minimum_version_repository.dart'; 2 | import 'package:app/access/forced_update/minimum_version_repository_mock.dart'; 3 | import 'package:app/access/kill_switch/kill_switch_repository.dart'; 4 | import 'package:app/access/kill_switch/kill_switch_repository_mock.dart'; 5 | import 'package:app/app_router.dart'; 6 | import 'package:app/presentation/diagnostic/diagnostic_button.dart'; 7 | import 'package:flutter/material.dart'; 8 | import 'package:get_it/get_it.dart'; 9 | 10 | /// A widget that has button that allows you to do navigation actions. 11 | final class NavigationDiagnosticWidget extends StatelessWidget { 12 | const NavigationDiagnosticWidget({super.key}); 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | final minimumVersionRepository = GetIt.I.get(); 17 | final killSwitchRepository = GetIt.I.get(); 18 | 19 | return Row( 20 | children: [ 21 | Expanded( 22 | child: Column( 23 | crossAxisAlignment: CrossAxisAlignment.start, 24 | children: [ 25 | const SizedBox(height: 8), 26 | DiagnosticButton( 27 | label: "Go to the dad jokes page", 28 | onPressed: () { 29 | router.go(home); 30 | }, 31 | ), 32 | if (minimumVersionRepository is MinimumVersionRepositoryMock) 33 | DiagnosticButton( 34 | label: "Trigger forced update", 35 | onPressed: () { 36 | minimumVersionRepository.updateMinimumVersion(); 37 | }, 38 | ), 39 | if (killSwitchRepository is KillSwitchRepositoryMock) 40 | DiagnosticButton( 41 | label: "Toggle kill switch state", 42 | onPressed: () { 43 | killSwitchRepository.setOrToggleKillSwitchState(); 44 | }, 45 | ), 46 | ], 47 | ), 48 | ), 49 | ], 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/app/lib/presentation/diagnostic/selectable_diagnostic_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/presentation/diagnostic/diagnostic_button.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | /// A button used for the diagnostic overlay that can be selected. 5 | final class SelectableDiagnosticButton extends StatelessWidget { 6 | const SelectableDiagnosticButton({ 7 | super.key, 8 | required this.label, 9 | required this.onPressed, 10 | required this.isSelected, 11 | }); 12 | 13 | final String label; 14 | final VoidCallback onPressed; 15 | final bool isSelected; 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | return Padding( 20 | padding: const EdgeInsets.symmetric(horizontal: 2.0), 21 | child: Column( 22 | children: [ 23 | DiagnosticButton( 24 | label: label, 25 | onPressed: onPressed, 26 | ), 27 | if (isSelected) 28 | Container( 29 | width: 68, 30 | height: 0.5, 31 | color: Colors.white, 32 | ), 33 | ], 34 | ), 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/app/lib/presentation/forced_update/forced_update_page.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:app/l10n/localization_extensions.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter_dotenv/flutter_dotenv.dart'; 6 | import 'package:url_launcher/url_launcher.dart'; 7 | 8 | final class ForcedUpdatePage extends StatelessWidget { 9 | ForcedUpdatePage({super.key}) { 10 | if (Platform.isIOS) { 11 | _url = Uri.parse(dotenv.env['APP_STORE_URL_IOS']!); 12 | } else { 13 | _url = Uri.parse(dotenv.env['APP_STORE_URL_Android']!); 14 | } 15 | } 16 | 17 | late final Uri _url; 18 | 19 | Future _launchUrl(BuildContext context) async { 20 | // We get the error message from the localization file first to avoid 21 | // using the context after an asynchronous operation. 22 | var errorMessage = context.local.forcedUpdatePageUrlLaunchException(_url); 23 | if (!await launchUrl(_url)) { 24 | throw Exception(errorMessage); 25 | } 26 | } 27 | 28 | @override 29 | Widget build(BuildContext context) { 30 | final local = context.local; 31 | return Scaffold( 32 | key: const Key('forcedUpdateScaffold'), 33 | appBar: AppBar( 34 | title: Text(local.forcedUpdatePageTitle), 35 | ), 36 | body: Center( 37 | child: Column( 38 | mainAxisAlignment: MainAxisAlignment.center, 39 | children: [ 40 | Text( 41 | local.forcedUpdatePageUpdateRequiredMessage, 42 | style: Theme.of(context).textTheme.headlineMedium, 43 | ), 44 | OutlinedButton( 45 | child: Text(local.forcedUpdatePageUpdateButton), 46 | onPressed: () { 47 | _launchUrl(context); 48 | }, 49 | ), 50 | ], 51 | ), 52 | ), 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/app/lib/presentation/kill_switch/kill_switch_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/l10n/localization_extensions.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | final class KillSwitchPage extends StatelessWidget { 5 | const KillSwitchPage({super.key}); 6 | 7 | @override 8 | Widget build(BuildContext context) { 9 | final local = context.local; 10 | return Scaffold( 11 | key: const Key('KillSwitchScaffold'), 12 | appBar: AppBar( 13 | title: Text(local.killSwitchPageTitle), 14 | ), 15 | body: Center( 16 | child: Column( 17 | mainAxisAlignment: MainAxisAlignment.center, 18 | children: [ 19 | Text( 20 | local.killSwitchPageMessage, 21 | style: Theme.of(context).textTheme.headlineMedium, 22 | ), 23 | ], 24 | ), 25 | ), 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/app/lib/presentation/mvvm/mvvm_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/presentation/mvvm/view_model.dart'; 2 | import 'package:flutter/widgets.dart'; 3 | 4 | class MvvmWidget extends StatefulWidget { 5 | const MvvmWidget( 6 | {super.key, 7 | TViewModel? viewModel, 8 | Widget Function(BuildContext context, TViewModel viewModel)? build}) 9 | : _viewModelFromParameter = viewModel, 10 | _buildFunctionFromParameter = build; 11 | 12 | final TViewModel? _viewModelFromParameter; 13 | final Widget Function(BuildContext context, TViewModel viewModel)? 14 | _buildFunctionFromParameter; 15 | 16 | TViewModel getViewModel() { 17 | if (_viewModelFromParameter != null) { 18 | return _viewModelFromParameter; 19 | } 20 | 21 | throw Exception( 22 | 'You must override getViewModel or provide a viewModel parameter.'); 23 | } 24 | 25 | @override 26 | State createState() { 27 | return _MvvmWidgetState(); 28 | } 29 | 30 | Widget build(BuildContext context, TViewModel viewModel) { 31 | if (_buildFunctionFromParameter != null) { 32 | return _buildFunctionFromParameter(context, viewModel); 33 | } 34 | 35 | throw Exception('You must override build or provide a build parameter.'); 36 | } 37 | } 38 | 39 | class _MvvmWidgetState 40 | extends State> { 41 | late TViewModel _viewModel; 42 | 43 | @override 44 | void initState() { 45 | super.initState(); 46 | _viewModel = widget.getViewModel(); 47 | _viewModel.addListener(onPropertyChanged); 48 | } 49 | 50 | void onPropertyChanged() { 51 | setState(() {}); 52 | } 53 | 54 | @override 55 | void dispose() { 56 | super.dispose(); 57 | _viewModel.removeListener(onPropertyChanged); 58 | _viewModel.dispose(); 59 | } 60 | 61 | @override 62 | Widget build(BuildContext context) { 63 | _viewModel.startRecordingPropertiesToNotify(); 64 | final result = widget.build(context, _viewModel); 65 | _viewModel.stopRecordingPropertiesToNotify(); 66 | return result; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/app/lib/presentation/mvvm/view_model.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:collection'; 3 | 4 | import 'package:flutter/foundation.dart'; 5 | 6 | abstract class ViewModel extends ChangeNotifier { 7 | final Map _properties = {}; 8 | final HashSet _propertiesToNotify = HashSet(); 9 | final Map _streamSubscriptions = {}; 10 | 11 | bool _isRecordingPropertiesToNotify = false; 12 | 13 | void startRecordingPropertiesToNotify() { 14 | _propertiesToNotify.clear(); 15 | _isRecordingPropertiesToNotify = true; 16 | } 17 | 18 | void stopRecordingPropertiesToNotify() { 19 | _isRecordingPropertiesToNotify = false; 20 | } 21 | 22 | void _recordPropertyName(String propertyName) { 23 | if (_isRecordingPropertiesToNotify) { 24 | _propertiesToNotify.add(propertyName); 25 | } 26 | } 27 | 28 | void notifyPropertyChanged(String propertyName) { 29 | if (_propertiesToNotify.contains(propertyName)) { 30 | notifyListeners(); 31 | } 32 | } 33 | 34 | T get(String propertyName, T initialValue) { 35 | _recordPropertyName(propertyName); 36 | return _properties.putIfAbsent(propertyName, () => initialValue); 37 | } 38 | 39 | T getLazy(String propertyName, T Function() initialValueProvider) { 40 | _recordPropertyName(propertyName); 41 | return _properties.putIfAbsent(propertyName, initialValueProvider); 42 | } 43 | 44 | void set(String propertyName, T value) { 45 | _properties[propertyName] = value; 46 | notifyPropertyChanged(propertyName); 47 | } 48 | 49 | T getFromStream(String propertyName, Stream Function() getStream, T initialValue) { 50 | if (!_streamSubscriptions.containsKey(propertyName)) { 51 | final subscription = getStream().listen((value) { 52 | set(propertyName, value); 53 | }); 54 | _streamSubscriptions[propertyName] = subscription; 55 | } 56 | return get(propertyName, initialValue); 57 | } 58 | 59 | @override 60 | void dispose() { 61 | super.dispose(); 62 | for (final subscription in _streamSubscriptions.values) { 63 | subscription.cancel(); 64 | } 65 | _streamSubscriptions.clear(); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/app/lib/presentation/styles/global_theme_data.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class GlobalThemeData { 4 | static ThemeData lightThemeData = themeData(lightColorScheme); 5 | static ThemeData darkThemeData = themeData(darkColorScheme); 6 | 7 | static ThemeData themeData(ColorScheme colorScheme) { 8 | return ThemeData( 9 | useMaterial3: true, 10 | fontFamily: 'Guillon', 11 | colorScheme: colorScheme, 12 | ); 13 | } 14 | 15 | static const ColorScheme lightColorScheme = ColorScheme( 16 | primary: Color(0xFF0D59CD), 17 | onPrimary: Colors.white, 18 | secondary: Color(0xFF5BC5F2), 19 | onSecondary: Color(0xFF001E60), 20 | error: Color(0xFFD93B27), 21 | onError: Color(0xFFFDEFED), 22 | surface: Color(0xFFF5FAFF), 23 | onSurface: Color(0xFF001E60), 24 | brightness: Brightness.light, 25 | ); 26 | 27 | static const ColorScheme darkColorScheme = ColorScheme( 28 | primary: Color(0xFF89C5FF), 29 | onPrimary: Color(0xFF121821), 30 | secondary: Color(0xFFFCA58B), 31 | onSecondary: Color(0xFF302B29), 32 | error: Color(0xFFE8897D), 33 | onError: Color(0xFF32110D), 34 | surface: Color(0xFF2C3034), 35 | onSurface: Colors.white, 36 | brightness: Brightness.dark, 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /src/app/lib/shell.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/presentation/diagnostic/diagnostic_overlay.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | /// The shell of the application with [DiagnosticOverlay]. 5 | final class Shell extends StatelessWidget { 6 | const Shell({super.key, required this.child}); 7 | 8 | final Widget child; 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return Stack( 13 | children: [ 14 | child, 15 | // Add global widgets here. 16 | const DiagnosticOverlay(), 17 | ], 18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/app/linux/.gitignore: -------------------------------------------------------------------------------- 1 | flutter/ephemeral 2 | flutter/generated_plugin_registrant.h 3 | flutter/generated_plugin_registrant.cc 4 | flutter/generated_plugins.cmake 5 | -------------------------------------------------------------------------------- /src/app/linux/main.cc: -------------------------------------------------------------------------------- 1 | #include "my_application.h" 2 | 3 | int main(int argc, char** argv) { 4 | g_autoptr(MyApplication) app = my_application_new(); 5 | return g_application_run(G_APPLICATION(app), argc, argv); 6 | } 7 | -------------------------------------------------------------------------------- /src/app/linux/my_application.h: -------------------------------------------------------------------------------- 1 | #ifndef FLUTTER_MY_APPLICATION_H_ 2 | #define FLUTTER_MY_APPLICATION_H_ 3 | 4 | #include 5 | 6 | G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, 7 | GtkApplication) 8 | 9 | /** 10 | * my_application_new: 11 | * 12 | * Creates a new Flutter-based application. 13 | * 14 | * Returns: a new #MyApplication. 15 | */ 16 | MyApplication* my_application_new(); 17 | 18 | #endif // FLUTTER_MY_APPLICATION_H_ 19 | -------------------------------------------------------------------------------- /src/app/macos/.gitignore: -------------------------------------------------------------------------------- 1 | # Flutter-related 2 | **/Flutter/ephemeral/ 3 | **/Flutter/GeneratedPluginRegistrant.swift 4 | **/Pods/ 5 | 6 | # Xcode-related 7 | **/dgph 8 | **/xcuserdata/ 9 | -------------------------------------------------------------------------------- /src/app/macos/Flutter/Flutter-Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "ephemeral/Flutter-Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /src/app/macos/Flutter/Flutter-Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "ephemeral/Flutter-Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /src/app/macos/Podfile: -------------------------------------------------------------------------------- 1 | platform :osx, '10.14' 2 | 3 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 4 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 5 | 6 | project 'Runner', { 7 | 'Debug' => :debug, 8 | 'Profile' => :release, 9 | 'Release' => :release, 10 | } 11 | 12 | def flutter_root 13 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) 14 | unless File.exist?(generated_xcode_build_settings_path) 15 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" 16 | end 17 | 18 | File.foreach(generated_xcode_build_settings_path) do |line| 19 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 20 | return matches[1].strip if matches 21 | end 22 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" 23 | end 24 | 25 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 26 | 27 | flutter_macos_podfile_setup 28 | 29 | target 'Runner' do 30 | use_frameworks! 31 | use_modular_headers! 32 | 33 | flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) 34 | target 'RunnerTests' do 35 | inherit! :search_paths 36 | end 37 | end 38 | 39 | post_install do |installer| 40 | installer.pods_project.targets.each do |target| 41 | flutter_additional_macos_build_settings(target) 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /src/app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/app/macos/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/app/macos/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | @NSApplicationMain 5 | class AppDelegate: FlutterAppDelegate { 6 | override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 7 | return true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "16x16", 5 | "idiom" : "mac", 6 | "filename" : "app_icon_16.png", 7 | "scale" : "1x" 8 | }, 9 | { 10 | "size" : "16x16", 11 | "idiom" : "mac", 12 | "filename" : "app_icon_32.png", 13 | "scale" : "2x" 14 | }, 15 | { 16 | "size" : "32x32", 17 | "idiom" : "mac", 18 | "filename" : "app_icon_32.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "32x32", 23 | "idiom" : "mac", 24 | "filename" : "app_icon_64.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "128x128", 29 | "idiom" : "mac", 30 | "filename" : "app_icon_128.png", 31 | "scale" : "1x" 32 | }, 33 | { 34 | "size" : "128x128", 35 | "idiom" : "mac", 36 | "filename" : "app_icon_256.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "256x256", 41 | "idiom" : "mac", 42 | "filename" : "app_icon_256.png", 43 | "scale" : "1x" 44 | }, 45 | { 46 | "size" : "256x256", 47 | "idiom" : "mac", 48 | "filename" : "app_icon_512.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "512x512", 53 | "idiom" : "mac", 54 | "filename" : "app_icon_512.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "512x512", 59 | "idiom" : "mac", 60 | "filename" : "app_icon_1024.png", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nventive/FlutterApplicationTemplate/3ecb4f9a2a43a70cfcab226f6e037748f296cdf4/src/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png -------------------------------------------------------------------------------- /src/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nventive/FlutterApplicationTemplate/3ecb4f9a2a43a70cfcab226f6e037748f296cdf4/src/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png -------------------------------------------------------------------------------- /src/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nventive/FlutterApplicationTemplate/3ecb4f9a2a43a70cfcab226f6e037748f296cdf4/src/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png -------------------------------------------------------------------------------- /src/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nventive/FlutterApplicationTemplate/3ecb4f9a2a43a70cfcab226f6e037748f296cdf4/src/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png -------------------------------------------------------------------------------- /src/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nventive/FlutterApplicationTemplate/3ecb4f9a2a43a70cfcab226f6e037748f296cdf4/src/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png -------------------------------------------------------------------------------- /src/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nventive/FlutterApplicationTemplate/3ecb4f9a2a43a70cfcab226f6e037748f296cdf4/src/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png -------------------------------------------------------------------------------- /src/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nventive/FlutterApplicationTemplate/3ecb4f9a2a43a70cfcab226f6e037748f296cdf4/src/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png -------------------------------------------------------------------------------- /src/app/macos/Runner/Configs/AppInfo.xcconfig: -------------------------------------------------------------------------------- 1 | // Application-level settings for the Runner target. 2 | // 3 | // This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the 4 | // future. If not, the values below would default to using the project name when this becomes a 5 | // 'flutter create' template. 6 | 7 | // The application's name. By default this is also the title of the Flutter window. 8 | PRODUCT_NAME = ApplicationTemplate 9 | 10 | // The application's bundle identifier 11 | PRODUCT_BUNDLE_IDENTIFIER = com.nventive.internal.flutterapptemplate 12 | 13 | // The copyright displayed in application information 14 | PRODUCT_COPYRIGHT = Copyright © 2024 nventive. All rights reserved. 15 | -------------------------------------------------------------------------------- /src/app/macos/Runner/Configs/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Debug.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /src/app/macos/Runner/Configs/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Release.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /src/app/macos/Runner/Configs/Warnings.xcconfig: -------------------------------------------------------------------------------- 1 | WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings 2 | GCC_WARN_UNDECLARED_SELECTOR = YES 3 | CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES 4 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE 5 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES 6 | CLANG_WARN_PRAGMA_PACK = YES 7 | CLANG_WARN_STRICT_PROTOTYPES = YES 8 | CLANG_WARN_COMMA = YES 9 | GCC_WARN_STRICT_SELECTOR_MATCH = YES 10 | CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES 11 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES 12 | GCC_WARN_SHADOW = YES 13 | CLANG_WARN_UNREACHABLE_CODE = YES 14 | -------------------------------------------------------------------------------- /src/app/macos/Runner/DebugProfile.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.cs.allow-jit 8 | 9 | com.apple.security.network.server 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/app/macos/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | NSHumanReadableCopyright 26 | $(PRODUCT_COPYRIGHT) 27 | NSMainNibFile 28 | MainMenu 29 | NSPrincipalClass 30 | NSApplication 31 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/app/macos/Runner/MainFlutterWindow.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | class MainFlutterWindow: NSWindow { 5 | override func awakeFromNib() { 6 | let flutterViewController = FlutterViewController() 7 | let windowFrame = self.frame 8 | self.contentViewController = flutterViewController 9 | self.setFrame(windowFrame, display: true) 10 | 11 | RegisterGeneratedPlugins(registry: flutterViewController) 12 | 13 | super.awakeFromNib() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/app/macos/Runner/Release.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/app/macos/RunnerTests/RunnerTests.swift: -------------------------------------------------------------------------------- 1 | import FlutterMacOS 2 | import Cocoa 3 | import XCTest 4 | 5 | class RunnerTests: XCTestCase { 6 | 7 | func testExample() { 8 | // If you add code to the Runner application, consider adding tests here. 9 | // See https://developer.apple.com/documentation/xctest for more information about using XCTest. 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/app/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: app 2 | description: "A new Flutter project." 3 | publish_to: 'none' 4 | version: 1.0.0+1 5 | 6 | environment: 7 | sdk: '>=3.4.0 <4.0.0' 8 | 9 | dependencies: 10 | dio: ^5.4.1 11 | equatable: ^2.0.5 12 | firebase_analytics: ^11.3.0 13 | firebase_core: ^3.4.0 14 | firebase_remote_config: ^5.1.0 15 | flutter: 16 | sdk: flutter 17 | flutter_dotenv: ^5.1.0 18 | get_it: ^8.0.3 19 | go_router: ^14.0.0 20 | json_annotation: ^4.9.0 21 | package_info_plus: ^8.0.0 22 | retrofit: ^4.1.0 23 | rxdart: ^0.28.0 24 | url_launcher: ^6.2.6 25 | shared_preferences: ^2.2.3 26 | logger: ^2.2.0 27 | path_provider: ^2.1.3 28 | share_plus: ^9.0.0 29 | alice: ^1.0.0-dev.12 30 | alice_dio: ^1.0.6 31 | flutter_localizations: 32 | sdk: flutter 33 | intl: any 34 | bugsee_flutter: ^8.4.0 35 | 36 | # This is required with alice installed unless this PR is merged https://github.com/jhomlala/alice/pull/171 37 | dependency_overrides: 38 | share_plus: ^9.0.0 39 | package_info_plus: ^8.0.0 40 | 41 | dev_dependencies: 42 | flutter_test: 43 | sdk: flutter 44 | integration_test: 45 | sdk: flutter 46 | flutter_lints: ^5.0.0 47 | package_rename: ^1.5.3 48 | build_runner: ^2.4.8 49 | retrofit_generator: ^9.1.7 50 | json_serializable: ^6.9.3 51 | mockito: ^5.4.4 52 | custom_lint: 53 | mvvm_lints: 54 | path: ../mvvm_lints 55 | 56 | 57 | flutter: 58 | uses-material-design: true 59 | 60 | assets: 61 | - assets/openSourceSoftwareLicenses.json 62 | - assets/fonts/guillon_black.ttf 63 | - assets/fonts/guillon_bold.ttf 64 | - assets/fonts/guillon_light.ttf 65 | - assets/fonts/guillon_regular.ttf 66 | - assets/fonts/guillon_semibold.ttf 67 | - .env.dev 68 | - .env.staging 69 | - .env.prod 70 | 71 | fonts: 72 | - family: Guillon 73 | fonts: 74 | - asset: assets/fonts/guillon_light.ttf 75 | weight: 300 76 | - asset: assets/fonts/guillon_regular.ttf 77 | weight: 400 78 | - asset: assets/fonts/guillon_semibold.ttf 79 | weight: 600 80 | - asset: assets/fonts/guillon_bold.ttf 81 | weight: 700 82 | - asset: assets/fonts/guillon_black.ttf 83 | weight: 900 84 | -------------------------------------------------------------------------------- /src/app/test/app_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/app.dart'; 2 | import 'package:app/business/dad_jokes/dad_jokes_service.dart'; 3 | import 'package:app/business/diagnostics/diagnostics_service.dart'; 4 | import 'package:app/shell.dart'; 5 | import 'package:flutter_test/flutter_test.dart'; 6 | import 'package:get_it/get_it.dart'; 7 | import 'package:logger/logger.dart'; 8 | import 'package:mockito/annotations.dart'; 9 | 10 | import 'app_test.mocks.dart'; 11 | 12 | @GenerateNiceMocks([ 13 | MockSpec(), 14 | MockSpec(), 15 | ]) 16 | void main() { 17 | var diagnosticsService = MockDiagnosticsService(); 18 | var dadJokesService = MockDadJokesService(); 19 | 20 | setUp(() { 21 | diagnosticsService = MockDiagnosticsService(); 22 | dadJokesService = MockDadJokesService(); 23 | 24 | GetIt.I.registerSingleton( 25 | diagnosticsService, 26 | ); 27 | GetIt.I.registerSingleton( 28 | dadJokesService, 29 | ); 30 | 31 | GetIt.I.registerSingleton(Logger()); 32 | }); 33 | 34 | testWidgets('Shell Test', (WidgetTester tester) async { 35 | await tester.pumpWidget(const App()); 36 | expect(find.byType(Shell), findsOneWidget); 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /src/app/test/business/update_required_service_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/access/forced_update/current_version_repository.dart'; 2 | import 'package:app/access/forced_update/data/version.dart'; 3 | import 'package:app/access/forced_update/minimum_version_repository_mock.dart'; 4 | import 'package:app/business/forced_update/update_required_service.dart'; 5 | import 'package:flutter_test/flutter_test.dart'; 6 | import 'package:mockito/annotations.dart'; 7 | import 'package:mockito/mockito.dart'; 8 | 9 | import 'update_required_service_test.mocks.dart'; 10 | 11 | @GenerateNiceMocks( 12 | [ 13 | MockSpec(), 14 | ], 15 | ) 16 | void main() { 17 | late CurrentVersionRepository currentVersionRepository; 18 | final MinimumVersionRepositoryMock minimumVersionRepositoryMock = 19 | MinimumVersionRepositoryMock(); 20 | late UpdateRequiredService updateRequiredService; 21 | 22 | setUp(() { 23 | TestWidgetsFlutterBinding.ensureInitialized(); 24 | 25 | var versionDummy = const Version(1, 5, 0); 26 | provideDummy(versionDummy); 27 | currentVersionRepository = MockCurrentVersionRepository(); 28 | when(currentVersionRepository.getCurrentVersion()).thenAnswer( 29 | (_) async => versionDummy, 30 | ); 31 | 32 | updateRequiredService = UpdateRequiredService( 33 | minimumVersionRepositoryMock, 34 | currentVersionRepository, 35 | ); 36 | }); 37 | 38 | group('Minimum version tests', () { 39 | var testCases = [ 40 | {'version': const Version(2, 0, 0), 'expected': true}, 41 | {'version': const Version(1, 0, 0), 'expected': false}, 42 | {'version': const Version(1, 5, 0), 'expected': false}, 43 | ]; 44 | 45 | for (var testCase in testCases) { 46 | test('Minimum version ${testCase['version']} test', () async { 47 | // Arrange 48 | bool updateRequired = false; 49 | 50 | var updateRequiredFuture = 51 | updateRequiredService.waitForUpdateRequired().then((value) { 52 | updateRequired = true; 53 | }); 54 | 55 | // Act 56 | minimumVersionRepositoryMock.updateMinimumVersion( 57 | version: testCase['version'] as Version, 58 | ); 59 | 60 | var timeout = Future.delayed(const Duration(milliseconds: 10)); 61 | 62 | await Future.any([timeout, updateRequiredFuture]); 63 | 64 | // Assert 65 | expect(updateRequired, testCase['expected']); 66 | }); 67 | } 68 | }); 69 | } 70 | -------------------------------------------------------------------------------- /src/app/web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nventive/FlutterApplicationTemplate/3ecb4f9a2a43a70cfcab226f6e037748f296cdf4/src/app/web/favicon.png -------------------------------------------------------------------------------- /src/app/web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nventive/FlutterApplicationTemplate/3ecb4f9a2a43a70cfcab226f6e037748f296cdf4/src/app/web/icons/Icon-192.png -------------------------------------------------------------------------------- /src/app/web/icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nventive/FlutterApplicationTemplate/3ecb4f9a2a43a70cfcab226f6e037748f296cdf4/src/app/web/icons/Icon-512.png -------------------------------------------------------------------------------- /src/app/web/icons/Icon-maskable-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nventive/FlutterApplicationTemplate/3ecb4f9a2a43a70cfcab226f6e037748f296cdf4/src/app/web/icons/Icon-maskable-192.png -------------------------------------------------------------------------------- /src/app/web/icons/Icon-maskable-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nventive/FlutterApplicationTemplate/3ecb4f9a2a43a70cfcab226f6e037748f296cdf4/src/app/web/icons/Icon-maskable-512.png -------------------------------------------------------------------------------- /src/app/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | ApplicationTemplate 31 | 32 | 33 | 37 | 38 | 39 | 40 | 41 | 56 | 57 | -------------------------------------------------------------------------------- /src/app/web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ApplicationTemplate", 3 | "short_name": "ApplicationTemplate", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "background_color": "#0175C2", 7 | "theme_color": "#0175C2", 8 | "description": "Application Template for Flutter", 9 | "orientation": "portrait-primary", 10 | "prefer_related_applications": false, 11 | "icons": [ 12 | { 13 | "src": "icons/Icon-192.png", 14 | "sizes": "192x192", 15 | "type": "image/png" 16 | }, 17 | { 18 | "src": "icons/Icon-512.png", 19 | "sizes": "512x512", 20 | "type": "image/png" 21 | }, 22 | { 23 | "src": "icons/Icon-maskable-192.png", 24 | "sizes": "192x192", 25 | "type": "image/png", 26 | "purpose": "maskable" 27 | }, 28 | { 29 | "src": "icons/Icon-maskable-512.png", 30 | "sizes": "512x512", 31 | "type": "image/png", 32 | "purpose": "maskable" 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /src/app/windows/.gitignore: -------------------------------------------------------------------------------- 1 | flutter/ephemeral/ 2 | flutter/generated_plugin_registrant.cc 3 | flutter/generated_plugin_registrant.h 4 | flutter/generated_plugins.cmake 5 | 6 | # Visual Studio user-specific files. 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # Visual Studio build-related files. 13 | x64/ 14 | x86/ 15 | 16 | # Visual Studio cache files 17 | # files ending in .cache can be ignored 18 | *.[Cc]ache 19 | # but keep track of directories ending in .cache 20 | !*.[Cc]ache/ 21 | -------------------------------------------------------------------------------- /src/app/windows/runner/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.14) 2 | project(runner LANGUAGES CXX) 3 | 4 | # Define the application target. To change its name, change BINARY_NAME in the 5 | # top-level CMakeLists.txt, not the value here, or `flutter run` will no longer 6 | # work. 7 | # 8 | # Any new source files that you add to the application should be added here. 9 | add_executable(${BINARY_NAME} WIN32 10 | "flutter_window.cpp" 11 | "main.cpp" 12 | "utils.cpp" 13 | "win32_window.cpp" 14 | "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" 15 | "Runner.rc" 16 | "runner.exe.manifest" 17 | ) 18 | 19 | # Apply the standard set of build settings. This can be removed for applications 20 | # that need different build settings. 21 | apply_standard_settings(${BINARY_NAME}) 22 | 23 | # Add preprocessor definitions for the build version. 24 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") 25 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") 26 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") 27 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") 28 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") 29 | 30 | # Disable Windows macros that collide with C++ standard library functions. 31 | target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") 32 | 33 | # Add dependency libraries and include directories. Add any application-specific 34 | # dependencies here. 35 | target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) 36 | target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") 37 | target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") 38 | 39 | # Run the Flutter tool portions of the build. This must not be removed. 40 | add_dependencies(${BINARY_NAME} flutter_assemble) 41 | -------------------------------------------------------------------------------- /src/app/windows/runner/flutter_window.cpp: -------------------------------------------------------------------------------- 1 | #include "flutter_window.h" 2 | 3 | #include 4 | 5 | #include "flutter/generated_plugin_registrant.h" 6 | 7 | FlutterWindow::FlutterWindow(const flutter::DartProject& project) 8 | : project_(project) {} 9 | 10 | FlutterWindow::~FlutterWindow() {} 11 | 12 | bool FlutterWindow::OnCreate() { 13 | if (!Win32Window::OnCreate()) { 14 | return false; 15 | } 16 | 17 | RECT frame = GetClientArea(); 18 | 19 | // The size here must match the window dimensions to avoid unnecessary surface 20 | // creation / destruction in the startup path. 21 | flutter_controller_ = std::make_unique( 22 | frame.right - frame.left, frame.bottom - frame.top, project_); 23 | // Ensure that basic setup of the controller was successful. 24 | if (!flutter_controller_->engine() || !flutter_controller_->view()) { 25 | return false; 26 | } 27 | RegisterPlugins(flutter_controller_->engine()); 28 | SetChildContent(flutter_controller_->view()->GetNativeWindow()); 29 | 30 | flutter_controller_->engine()->SetNextFrameCallback([&]() { 31 | this->Show(); 32 | }); 33 | 34 | // Flutter can complete the first frame before the "show window" callback is 35 | // registered. The following call ensures a frame is pending to ensure the 36 | // window is shown. It is a no-op if the first frame hasn't completed yet. 37 | flutter_controller_->ForceRedraw(); 38 | 39 | return true; 40 | } 41 | 42 | void FlutterWindow::OnDestroy() { 43 | if (flutter_controller_) { 44 | flutter_controller_ = nullptr; 45 | } 46 | 47 | Win32Window::OnDestroy(); 48 | } 49 | 50 | LRESULT 51 | FlutterWindow::MessageHandler(HWND hwnd, UINT const message, 52 | WPARAM const wparam, 53 | LPARAM const lparam) noexcept { 54 | // Give Flutter, including plugins, an opportunity to handle window messages. 55 | if (flutter_controller_) { 56 | std::optional result = 57 | flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, 58 | lparam); 59 | if (result) { 60 | return *result; 61 | } 62 | } 63 | 64 | switch (message) { 65 | case WM_FONTCHANGE: 66 | flutter_controller_->engine()->ReloadSystemFonts(); 67 | break; 68 | } 69 | 70 | return Win32Window::MessageHandler(hwnd, message, wparam, lparam); 71 | } 72 | -------------------------------------------------------------------------------- /src/app/windows/runner/flutter_window.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_FLUTTER_WINDOW_H_ 2 | #define RUNNER_FLUTTER_WINDOW_H_ 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include "win32_window.h" 10 | 11 | // A window that does nothing but host a Flutter view. 12 | class FlutterWindow : public Win32Window { 13 | public: 14 | // Creates a new FlutterWindow hosting a Flutter view running |project|. 15 | explicit FlutterWindow(const flutter::DartProject& project); 16 | virtual ~FlutterWindow(); 17 | 18 | protected: 19 | // Win32Window: 20 | bool OnCreate() override; 21 | void OnDestroy() override; 22 | LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, 23 | LPARAM const lparam) noexcept override; 24 | 25 | private: 26 | // The project to run. 27 | flutter::DartProject project_; 28 | 29 | // The Flutter instance hosted by this window. 30 | std::unique_ptr flutter_controller_; 31 | }; 32 | 33 | #endif // RUNNER_FLUTTER_WINDOW_H_ 34 | -------------------------------------------------------------------------------- /src/app/windows/runner/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "flutter_window.h" 6 | #include "utils.h" 7 | 8 | int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, 9 | _In_ wchar_t *command_line, _In_ int show_command) { 10 | // Attach to console when present (e.g., 'flutter run') or create a 11 | // new console when running with a debugger. 12 | if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { 13 | CreateAndAttachConsole(); 14 | } 15 | 16 | // Initialize COM, so that it is available for use in the library and/or 17 | // plugins. 18 | ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); 19 | 20 | flutter::DartProject project(L"data"); 21 | 22 | std::vector command_line_arguments = 23 | GetCommandLineArguments(); 24 | 25 | project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); 26 | 27 | FlutterWindow window(project); 28 | Win32Window::Point origin(10, 10); 29 | Win32Window::Size size(1280, 720); 30 | if (!window.Create(L"ApplicationTemplate", origin, size)) { 31 | return EXIT_FAILURE; 32 | } 33 | window.SetQuitOnClose(true); 34 | 35 | ::MSG msg; 36 | while (::GetMessage(&msg, nullptr, 0, 0)) { 37 | ::TranslateMessage(&msg); 38 | ::DispatchMessage(&msg); 39 | } 40 | 41 | ::CoUninitialize(); 42 | return EXIT_SUCCESS; 43 | } 44 | -------------------------------------------------------------------------------- /src/app/windows/runner/resource.h: -------------------------------------------------------------------------------- 1 | //{{NO_DEPENDENCIES}} 2 | // Microsoft Visual C++ generated include file. 3 | // Used by Runner.rc 4 | // 5 | #define IDI_APP_ICON 101 6 | 7 | // Next default values for new objects 8 | // 9 | #ifdef APSTUDIO_INVOKED 10 | #ifndef APSTUDIO_READONLY_SYMBOLS 11 | #define _APS_NEXT_RESOURCE_VALUE 102 12 | #define _APS_NEXT_COMMAND_VALUE 40001 13 | #define _APS_NEXT_CONTROL_VALUE 1001 14 | #define _APS_NEXT_SYMED_VALUE 101 15 | #endif 16 | #endif 17 | -------------------------------------------------------------------------------- /src/app/windows/runner/resources/app_icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nventive/FlutterApplicationTemplate/3ecb4f9a2a43a70cfcab226f6e037748f296cdf4/src/app/windows/runner/resources/app_icon.ico -------------------------------------------------------------------------------- /src/app/windows/runner/runner.exe.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PerMonitorV2 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/app/windows/runner/utils.cpp: -------------------------------------------------------------------------------- 1 | #include "utils.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | void CreateAndAttachConsole() { 11 | if (::AllocConsole()) { 12 | FILE *unused; 13 | if (freopen_s(&unused, "CONOUT$", "w", stdout)) { 14 | _dup2(_fileno(stdout), 1); 15 | } 16 | if (freopen_s(&unused, "CONOUT$", "w", stderr)) { 17 | _dup2(_fileno(stdout), 2); 18 | } 19 | std::ios::sync_with_stdio(); 20 | FlutterDesktopResyncOutputStreams(); 21 | } 22 | } 23 | 24 | std::vector GetCommandLineArguments() { 25 | // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. 26 | int argc; 27 | wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); 28 | if (argv == nullptr) { 29 | return std::vector(); 30 | } 31 | 32 | std::vector command_line_arguments; 33 | 34 | // Skip the first argument as it's the binary name. 35 | for (int i = 1; i < argc; i++) { 36 | command_line_arguments.push_back(Utf8FromUtf16(argv[i])); 37 | } 38 | 39 | ::LocalFree(argv); 40 | 41 | return command_line_arguments; 42 | } 43 | 44 | std::string Utf8FromUtf16(const wchar_t* utf16_string) { 45 | if (utf16_string == nullptr) { 46 | return std::string(); 47 | } 48 | int target_length = ::WideCharToMultiByte( 49 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, 50 | -1, nullptr, 0, nullptr, nullptr) 51 | -1; // remove the trailing null character 52 | int input_length = (int)wcslen(utf16_string); 53 | std::string utf8_string; 54 | if (target_length <= 0 || target_length > utf8_string.max_size()) { 55 | return utf8_string; 56 | } 57 | utf8_string.resize(target_length); 58 | int converted_length = ::WideCharToMultiByte( 59 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, 60 | input_length, utf8_string.data(), target_length, nullptr, nullptr); 61 | if (converted_length == 0) { 62 | return std::string(); 63 | } 64 | return utf8_string; 65 | } 66 | -------------------------------------------------------------------------------- /src/app/windows/runner/utils.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_UTILS_H_ 2 | #define RUNNER_UTILS_H_ 3 | 4 | #include 5 | #include 6 | 7 | // Creates a console for the process, and redirects stdout and stderr to 8 | // it for both the runner and the Flutter library. 9 | void CreateAndAttachConsole(); 10 | 11 | // Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string 12 | // encoded in UTF-8. Returns an empty std::string on failure. 13 | std::string Utf8FromUtf16(const wchar_t* utf16_string); 14 | 15 | // Gets the command line arguments passed in as a std::vector, 16 | // encoded in UTF-8. Returns an empty std::vector on failure. 17 | std::vector GetCommandLineArguments(); 18 | 19 | #endif // RUNNER_UTILS_H_ 20 | -------------------------------------------------------------------------------- /src/cli/.azuredevops/gitversion-config.yml: -------------------------------------------------------------------------------- 1 | # The version is driven by conventional commits via xxx-version-bump-message. 2 | # Anything merged to main creates a new stable version. 3 | # Only builds from main and feature/* are pushed to Pub.dev. 4 | 5 | assembly-versioning-scheme: MajorMinorPatch 6 | mode: MainLine 7 | next-version: '' # Use git tags to set the base version. 8 | continuous-delivery-fallback-tag: "" 9 | commit-message-incrementing: Enabled 10 | major-version-bump-message: "^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\\([\\w\\s-]*\\))?(!:|:.*\\n\\n((.+\\n)+\\n)?BREAKING CHANGE:\\s.+)" 11 | minor-version-bump-message: "^(feat)(\\([\\w\\s-]*\\))?:" 12 | patch-version-bump-message: "^(build|chore|docs|fix|perf|refactor|revert|style|test)(\\([\\w\\s-]*\\))?:" 13 | no-bump-message: "^(ci)(\\([\\w\\s-]*\\))?:" # You can use the "ci" type to avoid bumping the version when your changes are limited to the .github folders. 14 | branches: 15 | main: 16 | regex: ^master$|^main$ 17 | tag: 'stable' 18 | dev: 19 | regex: dev/.*?/(.*?) 20 | tag: dev.{BranchName} 21 | source-branches: [main] 22 | feature: 23 | tag: feature.{BranchName} 24 | regex: feature/(.*?) 25 | source-branches: [main] 26 | ignore: 27 | sha: [] -------------------------------------------------------------------------------- /src/cli/.azuredevops/stage-calculate-pubdev-score.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | - name: pathToSrc 3 | type: string 4 | default: '$(Build.SourcesDirectory)/src/cli' 5 | - name: artifactName 6 | type: string 7 | default: 'PubDev_Score_Staging' 8 | 9 | jobs: 10 | - job: OnWindows_CalculatePubDev_Score 11 | pool: 12 | vmImage : $(ubuntuHostedAgentImage) 13 | steps: 14 | - template: ../../../build/templates/flutter-install.yml 15 | parameters: 16 | projectDirectory: '${{ parameters.pathToSrc }}' 17 | 18 | - template: templates/calculate-devscore.yml 19 | parameters: 20 | pathToSrc: '${{ parameters.pathToSrc }}' 21 | artifactName: ${{parameters.artifactName}} -------------------------------------------------------------------------------- /src/cli/.azuredevops/stage-publish-cli.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | - name: pathToSrc 3 | type: string 4 | default: '$(Build.SourcesDirectory)/src' 5 | 6 | jobs: 7 | - job: OnWindows_Publish_CLI 8 | pool: 9 | vmImage : $(windowsHostedAgentImage) 10 | steps: 11 | - template: ../../../build/templates/gitversion.yml 12 | parameters: 13 | configFilePath: '${{ parameters.pathToSrc }}/cli/.azuredevops/gitversion-config.yml' 14 | 15 | - template: ../../../build/templates/flutter-install.yml 16 | parameters: 17 | projectDirectory: '${{ parameters.pathToSrc }}/cli' 18 | 19 | - template: templates/prepare-package.yml 20 | parameters: 21 | pathToSrc: '${{ parameters.pathToSrc }}' 22 | majorMinorPatch: '$(GitVersion.MajorMinorPatch)' 23 | sha: '$(GitVersion.Sha)' 24 | shortSha: '$(GitVersion.ShortSha)' 25 | commitDate: '$(GitVersion.CommitDate)' 26 | 27 | - template: ../../../build/templates/flutter-prepare.yml 28 | parameters: 29 | projectDirectory: '${{ parameters.pathToSrc }}/cli' 30 | 31 | - template: templates/publish-package.yml 32 | parameters: 33 | pathToSrc: '${{ parameters.pathToSrc }}' 34 | dryRun: false 35 | 36 | - template: ../../../build/templates/flutter-diagnostics.yml 37 | parameters: 38 | projectDirectory: '${{ parameters.pathToSrc }}/cli' 39 | condition: failed() 40 | 41 | - task: PostBuildCleanup@4 42 | displayName: 'Post-Build cleanup : Cleanup files to keep build server clean!' 43 | condition: always() 44 | -------------------------------------------------------------------------------- /src/cli/.azuredevops/stage-test-cli.yaml: -------------------------------------------------------------------------------- 1 | parameters: 2 | - name: pathToSrc 3 | type: string 4 | default: '$(Build.SourcesDirectory)/src' 5 | 6 | jobs: 7 | # This uses the CLI to generate a project. 8 | # The generated project folder is published to be built in a later stage. 9 | - job: OnWindows_Test_CLI 10 | pool: 11 | vmImage : $(windowsHostedAgentImage) 12 | steps: 13 | - template: ../../../build/templates/gitversion.yml 14 | parameters: 15 | configFilePath: '${{ parameters.pathToSrc }}/cli/.azuredevops/gitversion-config.yml' 16 | 17 | - template: ../../../build/templates/flutter-install.yml 18 | parameters: 19 | projectDirectory: '${{ parameters.pathToSrc }}/cli' 20 | 21 | - template: templates/prepare-package.yml 22 | parameters: 23 | pathToSrc: '${{ parameters.pathToSrc }}' 24 | majorMinorPatch: '$(GitVersion.MajorMinorPatch)' 25 | sha: '$(GitVersion.Sha)' 26 | shortSha: '$(GitVersion.ShortSha)' 27 | commitDate: '$(GitVersion.CommitDate)' 28 | 29 | - template: ../../../build/templates/flutter-prepare.yml 30 | parameters: 31 | projectDirectory: '${{ parameters.pathToSrc }}/cli' 32 | 33 | - template: templates/publish-package.yml 34 | parameters: 35 | pathToSrc: '${{ parameters.pathToSrc }}' 36 | dryRun: true 37 | 38 | - template: ../../../build/templates/flutter-diagnostics.yml 39 | parameters: 40 | projectDirectory: '${{ parameters.pathToSrc }}/cli' 41 | condition: failed() 42 | 43 | - powershell: $(DartToolPath)/dart pub global activate --source=path ${{ parameters.pathToSrc }}/cli 44 | displayName: 'Install Flutter Application Generator' 45 | 46 | - powershell: $(DartToolPath)/dart pub global run flutter_application_generator create --destinationDirectory $(Pipeline.Workspace) --projectName GeneratedApp --applicationName GeneratedApp --packageName com.nventive.internal.flutterapptemplate --organizationName nventive 47 | displayName: 'Generate Project' 48 | 49 | - publish: $(Pipeline.Workspace)/GeneratedApp 50 | displayName: 'Publish GeneratedApp Folder' 51 | artifact: GeneratedApp 52 | -------------------------------------------------------------------------------- /src/cli/.azuredevops/templates/prepare-package.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | - name: pathToSrc 3 | type: string 4 | default: '$(Build.SourcesDirectory)/src' 5 | - name: majorMinorPatch 6 | type: string 7 | - name: sha 8 | type: string 9 | - name: shortSha 10 | type: string 11 | - name: commitDate 12 | type: string 13 | 14 | steps: 15 | - task: PowerShell@2 16 | displayName: 'Set Package Version' 17 | inputs: 18 | targetType: 'inline' 19 | script: | 20 | $(DartToolPath)/dart pub global activate cider 21 | $(DartToolPath)/dart pub global run cider --project-root ${{ parameters.pathToSrc }}/cli version ${{ parameters.majorMinorPatch }} 22 | ((Get-Content ${{ parameters.pathToSrc }}\cli\lib\src\commands\create_command.dart -Raw) -Replace '', '${{ parameters.majorMinorPatch }}').Trim() | Set-Content -Path ${{ parameters.pathToSrc }}\cli\lib\src\commands\create_command.dart -Encoding UTF8 23 | 24 | - powershell: ((Get-Content ${{ parameters.pathToSrc }}\cli\lib\src\commands\create_command.dart -Raw) -Replace '', '${{ parameters.sha }}').Trim() | Set-Content -Path ${{ parameters.pathToSrc }}\cli\lib\src\commands\create_command.dart -Encoding UTF8 25 | displayName: Set Commit Hash 26 | 27 | - powershell: ((Get-Content ${{ parameters.pathToSrc }}\cli\lib\src\commands\create_command.dart -Raw) -Replace '', '${{ parameters.shortSha }}').Trim() | Set-Content -Path ${{ parameters.pathToSrc }}\cli\lib\src\commands\create_command.dart -Encoding UTF8 28 | displayName: Set Short Commit Hash 29 | 30 | - powershell: ((Get-Content ${{ parameters.pathToSrc }}\cli\lib\src\commands\create_command.dart -Raw) -Replace '', '${{ parameters.commitDate }}').Trim() | Set-Content -Path ${{ parameters.pathToSrc }}\cli\lib\src\commands\create_command.dart -Encoding UTF8 31 | displayName: Set Commit Date 32 | 33 | - powershell: Move-Item -Path $(Build.SourcesDirectory)\README.md -Destination ${{ parameters.pathToSrc }}\cli\README.md 34 | displayName: Move README into CLI Folder 35 | -------------------------------------------------------------------------------- /src/cli/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://www.dartlang.org/guides/libraries/private-files 2 | 3 | # Files and directories created by pub 4 | .dart_tool/ 5 | .packages 6 | pubspec.lock 7 | 8 | # Files generated during tests 9 | .test_coverage.dart 10 | coverage/ 11 | .test_runner.dart 12 | 13 | # Android studio and IntelliJ 14 | .idea -------------------------------------------------------------------------------- /src/cli/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the static analysis results for your project (errors, 2 | # warnings, and lints). 3 | # 4 | # This enables the 'recommended' set of lints from `package:lints`. 5 | # This set helps identify many issues that may lead to problems when running 6 | # or consuming Dart code, and enforces writing Dart using a single, idiomatic 7 | # style and format. 8 | # 9 | # If you want a smaller set of lints you can change this to specify 10 | # 'package:lints/core.yaml'. These are just the most critical lints 11 | # (the recommended set includes the core lints). 12 | # The core lints are also what is used by pub.dev for scoring packages. 13 | 14 | include: package:lints/recommended.yaml 15 | 16 | # Uncomment the following section to specify additional rules. 17 | 18 | # linter: 19 | # rules: 20 | # - camel_case_types 21 | 22 | # analyzer: 23 | # exclude: 24 | # - path/to/excluded/files/** 25 | 26 | # For more information about the core and recommended set of lints, see 27 | # https://dart.dev/go/core-lints 28 | 29 | # For additional information about configuring this file, see 30 | # https://dart.dev/guides/language/analysis-options 31 | -------------------------------------------------------------------------------- /src/cli/bin/flutter_application_generator.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter_application_generator/src/command_runner.dart'; 4 | 5 | Future main(List arguments) async { 6 | await _flushThenExit( 7 | await FlutterApplicationGeneratorCommandRunner().run(arguments)); 8 | } 9 | 10 | /// Flushes the stdout and stderr streams, then exits the program with the given 11 | /// status code. 12 | /// 13 | /// This returns a Future that will never complete, since the program will have 14 | /// exited already. This is useful to prevent Future chains from proceeding 15 | /// after you've decided to exit. 16 | Future _flushThenExit(int status) { 17 | return Future.wait([stdout.close(), stderr.close()]) 18 | .then((_) => exit(status)); 19 | } 20 | -------------------------------------------------------------------------------- /src/cli/build.yaml: -------------------------------------------------------------------------------- 1 | targets: 2 | $default: 3 | builders: 4 | build_version: 5 | options: 6 | output: lib/src/version.g.dart 7 | -------------------------------------------------------------------------------- /src/cli/doc/Cli.md: -------------------------------------------------------------------------------- 1 | # CLI 2 | 3 | This Dart application is a command line interface (CLI) designed to create Flutter projects based on a template. 4 | The repository comprises two main sections, the CLI (`src/cli`) and the Flutter application (`src/app`). 5 | When generating a new project, the CLI downloads the GitHub repository to get the latest files. 6 | Because the template files are downloaded, you need internet when creating a new project. 7 | 8 | ## Special Files and Folders 9 | 10 | There are several manipulations on the files of the template when packaging it and running it. 11 | Most files are copied as-is and modified by the renaming phase, but that's not true for everything. 12 | 13 | ### Template files 14 | 15 | When running the template, the `src/cli/` folder is discarded. 16 | 17 | ### GitHub files 18 | 19 | When running the template, everything related to GitHub is discarded. 20 | - The `.github/` folder 21 | - `CODE_OF_CONDUCT.md` 22 | - `CONTRIBUTING.md` 23 | - `LICENSE` 24 | - `.mergify.yml` 25 | 26 | ### READMEs 27 | 28 | When packaging this template, the `README.md` (which describes the template) is moved to `cli` folder. 29 | - `README.md` --> `src/cli/README.md` 30 | 31 | When generating a new project from the template, the application `README.md` is updated with versioning information. 32 | 33 | ## Pipeline conditionals 34 | 35 | The pipelines use some special conditionals. 36 | The `.azure-pipelines.yml` has additional stages that are removed when running the template. 37 | These conditions use the `#-if false` syntax and are configured in `lib/src/commands/create_command.dart`. 38 | 39 | ## GitVersion 40 | 41 | For the template itself, we use the **MainLine** mode for GitVersion. This is the same mode as most of our open source packages. 42 | The configuration is located at `src/cli/.azuredevops/gitversion-config.yml`. 43 | For the generated app, we use the **ContinuousDeployment** mode for GitVersion. 44 | The configuration is located at `build/gitversion-config.yml`. 45 | 46 | ## References 47 | - [Creating custom templates](https://docs.microsoft.com/en-us/dotnet/core/tools/custom-templates) 48 | - [Properties of template.json](https://github.com/dotnet/templating/wiki/Reference-for-template.json) 49 | - [Comment syntax](https://github.com/dotnet/templating/wiki/Reference-for-comment-syntax) 50 | - [Supported files](https://github.com/dotnet/templating/blob/5b5eb6278bd745149a57d0882d655b29d02c70f4/src/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/SimpleConfigModel.cs#L387) 51 | -------------------------------------------------------------------------------- /src/cli/example/README.md: -------------------------------------------------------------------------------- 1 | ## Dad Jokes app example 2 | 3 | The `flutter_application_generator` source code includes a complete working example in the `src/app/` folder. It's a simple joke browser app that uses the Reddit API to display jokes posted to the `r/dadjokes` subreddit and allows you to manage favorites (add / remove). It also includes an authentication example. 4 | 5 | Before running the example, it's necessary to run the `build_runner` package in order to create the required generated files. Using your terminal of choice, navigate to the `src/app/` folder and run the following: 6 | 7 | ```bash 8 | dart run build_runner build --delete-conflicting-outputs 9 | ``` -------------------------------------------------------------------------------- /src/cli/lib/flutter_application_generator.dart: -------------------------------------------------------------------------------- 1 | /// flutter_application_generator. 2 | /// 3 | /// ```sh 4 | /// # activate flutter_application_generator 5 | /// dart pub global activate flutter_application_generator 6 | /// 7 | /// # see usage 8 | /// flutter_application_generator --help 9 | /// ``` 10 | library; 11 | -------------------------------------------------------------------------------- /src/cli/lib/src/commands/update_command.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:args/command_runner.dart'; 4 | import 'package:flutter_application_generator/src/command_runner.dart'; 5 | import 'package:flutter_application_generator/src/version.g.dart'; 6 | import 'package:mason_logger/mason_logger.dart'; 7 | import 'package:pub_updater/pub_updater.dart'; 8 | 9 | /// {@template update_command} 10 | /// A command which updates the CLI. 11 | /// {@endtemplate} 12 | final class UpdateCommand extends Command { 13 | static const String commandName = 'update'; 14 | 15 | final Logger _logger; 16 | final PubUpdater _pubUpdater; 17 | 18 | /// {@macro update_command} 19 | UpdateCommand({ 20 | required Logger logger, 21 | PubUpdater? pubUpdater, 22 | }) : _logger = logger, 23 | _pubUpdater = pubUpdater ?? PubUpdater(); 24 | 25 | @override 26 | String get description => 'Update the CLI.'; 27 | 28 | @override 29 | String get name => commandName; 30 | 31 | @override 32 | Future run() async { 33 | final updateCheckProgress = _logger.progress('Checking for updates.'); 34 | late final String latestVersion; 35 | try { 36 | latestVersion = await _pubUpdater.getLatestVersion(packageName); 37 | } catch (error) { 38 | updateCheckProgress.fail(); 39 | _logger.err('$error'); 40 | return ExitCode.software.code; 41 | } 42 | updateCheckProgress.complete('Checked for updates.'); 43 | 44 | // The variable packageVersion is defined in 'version.g.dart' file. 45 | final isUpToDate = packageVersion == latestVersion; 46 | if (isUpToDate) { 47 | _logger.info('CLI is already at the latest version.'); 48 | return ExitCode.success.code; 49 | } 50 | 51 | final updateProgress = _logger.progress('Updating to $latestVersion.'); 52 | 53 | late final ProcessResult result; 54 | try { 55 | result = await _pubUpdater.update( 56 | packageName: packageName, 57 | versionConstraint: latestVersion, 58 | ); 59 | } catch (error) { 60 | updateProgress.fail(); 61 | _logger.err('$error'); 62 | return ExitCode.software.code; 63 | } 64 | 65 | if (result.exitCode != ExitCode.success.code) { 66 | updateProgress.fail(); 67 | _logger.err('Error updating CLI: ${result.stderr}.'); 68 | return ExitCode.software.code; 69 | } 70 | 71 | updateProgress.complete('Updated to $latestVersion.'); 72 | 73 | return ExitCode.success.code; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/cli/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_application_generator 2 | description: A starter CLI for creating apps using FlutterApplicationTemplate. 3 | version: 1.0.0 4 | repository: https://github.com/nventive/FlutterApplicationTemplate 5 | 6 | environment: 7 | sdk: '>=3.4.0 <4.0.0' 8 | 9 | dependencies: 10 | args: ^2.4.2 11 | path: ^1.9.0 12 | cli_completion: ^0.5.0 13 | mason_logger: ^0.3.2 14 | pub_updater: ^0.5.0 15 | archive: ^4.0.2 16 | http: ^1.2.1 17 | 18 | dev_dependencies: 19 | build_runner: ^2.4.8 20 | build_verify: ^3.1.0 21 | build_version: ^2.1.1 22 | lints: ^5.1.1 23 | test: ^1.24.0 24 | 25 | executables: 26 | flutter_application_generator: 27 | -------------------------------------------------------------------------------- /src/mvvm_lints/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.0.0 2 | 3 | - Initial version. 4 | -------------------------------------------------------------------------------- /src/mvvm_lints/README.md: -------------------------------------------------------------------------------- 1 | 13 | 14 | TODO: Put a short description of the package here that helps potential users 15 | know whether this package might be useful for them. 16 | 17 | ## Features 18 | 19 | TODO: List what your package can do. Maybe include images, gifs, or videos. 20 | 21 | ## Getting started 22 | 23 | TODO: List prerequisites and provide or point to information on how to 24 | start using the package. 25 | 26 | ## Usage 27 | 28 | TODO: Include short and useful examples for package users. Add longer examples 29 | to `/example` folder. 30 | 31 | ```dart 32 | const like = 'sample'; 33 | ``` 34 | 35 | ## Additional information 36 | 37 | TODO: Tell users more about the package: where to find more information, how to 38 | contribute to the package, how to file issues, what response they can expect 39 | from the package authors, and more. 40 | -------------------------------------------------------------------------------- /src/mvvm_lints/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the static analysis results for your project (errors, 2 | # warnings, and lints). 3 | # 4 | # This enables the 'recommended' set of lints from `package:lints`. 5 | # This set helps identify many issues that may lead to problems when running 6 | # or consuming Dart code, and enforces writing Dart using a single, idiomatic 7 | # style and format. 8 | # 9 | # If you want a smaller set of lints you can change this to specify 10 | # 'package:lints/core.yaml'. These are just the most critical lints 11 | # (the recommended set includes the core lints). 12 | # The core lints are also what is used by pub.dev for scoring packages. 13 | 14 | include: package:lints/recommended.yaml 15 | 16 | # Uncomment the following section to specify additional rules. 17 | 18 | # linter: 19 | # rules: 20 | # - camel_case_types 21 | 22 | # analyzer: 23 | # exclude: 24 | # - path/to/excluded/files/** 25 | 26 | # For more information about the core and recommended set of lints, see 27 | # https://dart.dev/go/core-lints 28 | 29 | # For additional information about configuring this file, see 30 | # https://dart.dev/guides/language/analysis-options 31 | -------------------------------------------------------------------------------- /src/mvvm_lints/lib/mvvm_lints.dart: -------------------------------------------------------------------------------- 1 | import 'package:analyzer/dart/ast/ast.dart'; 2 | import 'package:analyzer/dart/element/element.dart'; 3 | import 'package:analyzer/error/error.dart' as error; 4 | import 'package:analyzer/error/listener.dart'; 5 | import 'package:custom_lint_builder/custom_lint_builder.dart'; 6 | 7 | PluginBase createPlugin() => _MvvmLinter(); 8 | 9 | class _MvvmLinter extends PluginBase { 10 | @override 11 | List getLintRules(CustomLintConfigs configs) => [ 12 | PropertyNameMismatchLint() 13 | ]; 14 | } 15 | 16 | /// Lint rule that checks if property names in method calls match their accessor names. 17 | class PropertyNameMismatchLint extends DartLintRule { 18 | PropertyNameMismatchLint() : super(code: _code); 19 | 20 | static const _code = LintCode( 21 | name: 'property_name_mismatch', 22 | problemMessage: 'Property name in method call does not match the accessor name', 23 | errorSeverity: error.ErrorSeverity.ERROR, 24 | ); 25 | 26 | bool _isPropertyNameMethod(MethodInvocation node) { 27 | final method = node.methodName.staticElement; 28 | if (method is! MethodElement) return false; 29 | 30 | final parameters = method.parameters; 31 | if (parameters.isEmpty) return false; 32 | 33 | // Check if first parameter is named 'propertyName' 34 | return parameters.first.name == 'propertyName'; 35 | } 36 | 37 | @override 38 | void run( 39 | CustomLintResolver resolver, 40 | ErrorReporter reporter, 41 | CustomLintContext context, 42 | ) { 43 | context.registry.addMethodDeclaration((node) { 44 | if (!node.isGetter && !node.isSetter) return; 45 | 46 | final body = node.body; 47 | if (body is! ExpressionFunctionBody) return; 48 | 49 | final expression = body.expression; 50 | if (expression is! MethodInvocation) return; 51 | 52 | // Check if the method expects a propertyName parameter 53 | if (!_isPropertyNameMethod(expression)) return; 54 | 55 | final arguments = expression.argumentList.arguments; 56 | if (arguments.isEmpty) return; 57 | 58 | final firstArg = arguments.first; 59 | if (firstArg is! SimpleStringLiteral) return; 60 | 61 | final propertyName = firstArg.value; 62 | final memberName = node.name.lexeme; 63 | 64 | if (propertyName != memberName) { 65 | reporter.atNode(firstArg, code); 66 | } 67 | }); 68 | } 69 | } -------------------------------------------------------------------------------- /src/mvvm_lints/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: mvvm_lints 2 | description: A starting point for Dart libraries or applications. 3 | version: 1.0.0 4 | # repository: https://github.com/my_org/my_repo 5 | 6 | environment: 7 | sdk: ^3.6.1 8 | 9 | # Add regular dependencies here. 10 | dependencies: 11 | custom_lint_builder: ^0.7.3 12 | analyzer: ^7.3.0 13 | analyzer_plugin: ^0.12.0 14 | # path: ^1.8.0 15 | 16 | dev_dependencies: 17 | lints: ^5.0.0 18 | test: ^1.24.0 19 | -------------------------------------------------------------------------------- /tools/generateOpenSourceSoftwareLicenses.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | This script generates the open-source software licenses for the Flutter project. 4 | 5 | .DESCRIPTION 6 | This script generates the open-source software licenses for the Flutter project using the 'flutter_oss_licenses' package. 7 | The 'flutter_oss_licenses' will be activated, the licenses will be generated, and then the package will be deactivated. 8 | See https://pub.dev/packages/flutter_oss_licenses for more details. 9 | 10 | .PARAMETER pubspecDirectoryPath 11 | (Required) The Flutter project root directory path where 'pubspec.lock' is located. 12 | 13 | .PARAMETER outputPath 14 | (Required) The output destination path where the JSON file will be created (Path includes the file name). 15 | 16 | .EXAMPLE 17 | .\generateOpenSourceSoftwareLicenses.ps1 C:\P\FlutterApplicationTemplate\src\app C:\P\FlutterApplicationTemplate\src\app\assets\openSourceSoftwareLicenses.json 18 | This will create a new license file called 'openSourceSoftwareLicenses.json' in 'C:\P\FlutterApplicationTemplate\src\app\assets' based on 'C:\P\FlutterApplicationTemplate\src\app\pubspec.lock'. 19 | #> 20 | 21 | param ( 22 | [string]$pubspecDirectoryPath = $(throw "Please provide a 'pubspec.lock' directory path."), 23 | [string]$outputPath = $(throw "Please provide an output path.") 24 | ) 25 | 26 | dart pub global activate flutter_oss_licenses 27 | 28 | flutter pub get --directory $pubspecDirectoryPath 29 | 30 | generate --output $outputPath --project-root $pubspecDirectoryPath --json 31 | 32 | dart pub global deactivate flutter_oss_licenses 33 | --------------------------------------------------------------------------------