diff --git a/README.md b/README.md index ccb1219..862c8e2 100644 --- a/README.md +++ b/README.md @@ -7,45 +7,54 @@ some mason bricks that we use for our internal workflow > Note that we use `bdaya_flutter_common` as a base library for the generated code > We also need `build_runner` as a dev dependency. -* [bdaya_form](https://brickhub.dev/bricks/bdaya_form) - - generate a form using `reactive_forms_annotations` + `reactive_forms_generator` -* [bdaya_page](https://brickhub.dev/bricks/bdaya_page) - - generate a simple page (view + controller) -* [bdaya_route](https://brickhub.dev/bricks/bdaya_route) - - generate a page that reacts to route changes from `go_router` - +- [bdaya_form](https://brickhub.dev/bricks/bdaya_form) + - generate a form using `reactive_forms_annotations` + `reactive_forms_generator` +- [bdaya_page](https://brickhub.dev/bricks/bdaya_page) + - generate a simple page (view + controller) +- [bdaya_route](https://brickhub.dev/bricks/bdaya_route) + - generate a page that reacts to route changes from `go_router` +- [bdaya_new_flutter_app](https://brickhub.dev/bricks/bdaya_new_flutter_app) + - generate a new bdaya flutter app. ## Usage on git + 1. Activate [mason_cli](https://pub.dev/packages/mason_cli) - - `dart pub global activate mason_cli` + - `dart pub global activate mason_cli` 2. Add Bricks - - bdaya_form - ``` - mason add bdaya_form --git-url https://github.com/Bdaya-Dev/bricks --git-path bdaya_form - ``` - - bdaya_route - ``` - mason add bdaya_route --git-url https://github.com/Bdaya-Dev/bricks --git-path bdaya_route - ``` - - bdaya_page - ``` - mason add bdaya_page --git-url https://github.com/Bdaya-Dev/bricks --git-path bdaya_page - ``` + - bdaya_form + ``` + mason add bdaya_form --git-url https://github.com/Bdaya-Dev/bricks --git-path bdaya_form + ``` + - bdaya_route + ``` + mason add bdaya_route --git-url https://github.com/Bdaya-Dev/bricks --git-path bdaya_route + ``` + - bdaya_page + ``` + mason add bdaya_page --git-url https://github.com/Bdaya-Dev/bricks --git-path bdaya_page + ``` + - bdaya_new_flutter_app + ``` + mason add bdaya_new_flutter_app --git-url https://github.com/Bdaya-Dev/bricks --git-path bdaya_new_flutter_app + ``` 3. `mason get` 4. Make - - bdaya_form: - ``` - mason make bdaya_form -o lib/src/dialogs --name EditUser - ``` - > Note that `bdaya_form` requires some dependencies, which you can add using this command: - > - > `dart pub add reactive_forms_annotations --dev reactive_forms_generator` - - bdaya_route: - ``` - mason make bdaya_route -o lib/src/pages --name UserDetails - ``` - - bdaya_page: - ``` - mason make bdaya_page -o lib/src/pages --name Users - ``` - + - bdaya_form: + ``` + mason make bdaya_form -o lib/src/dialogs --name EditUser + ``` + > Note that `bdaya_form` requires some dependencies, which you can add using this command: + > + > `dart pub add reactive_forms_annotations --dev reactive_forms_generator` + - bdaya_route: + ``` + mason make bdaya_route -o lib/src/pages --name UserDetails + ``` + - bdaya_page: + ``` + mason make bdaya_page -o lib/src/pages --name Users + ``` + - bdaya_new_flutter_app: + ``` + mason make bdaya_new_flutter_app -o "your/projects/path" + ``` diff --git a/bdaya_new_flutter_app/.gitignore b/bdaya_new_flutter_app/.gitignore new file mode 100644 index 0000000..b93e117 --- /dev/null +++ b/bdaya_new_flutter_app/.gitignore @@ -0,0 +1,12 @@ +.DS_Store +.atom/ +.idea/* +.vscode/* + +# Files and directories created by pub +.dart_tool/ +.packages +pubspec.lock + +# Conventional directory for build outputs +build/ diff --git a/bdaya_new_flutter_app/CHANGELOG.md b/bdaya_new_flutter_app/CHANGELOG.md new file mode 100644 index 0000000..23f4b41 --- /dev/null +++ b/bdaya_new_flutter_app/CHANGELOG.md @@ -0,0 +1,3 @@ +# 0.0.1 + +- feat: initial release 🎉 diff --git a/bdaya_new_flutter_app/LICENSE b/bdaya_new_flutter_app/LICENSE new file mode 100644 index 0000000..6b6926d --- /dev/null +++ b/bdaya_new_flutter_app/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Bdaya Development + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/bdaya_new_flutter_app/README.md b/bdaya_new_flutter_app/README.md new file mode 100644 index 0000000..cfbd6b2 --- /dev/null +++ b/bdaya_new_flutter_app/README.md @@ -0,0 +1,89 @@ +# Bdaya New Flutter App + +Inspired from [very_good_core][very_good_core_link] 🦄. + +Developed with 💙 by [Bdaya Development][bdaya_development_link]. + +[![License: MIT][license_badge]][license_link] +[![Powered by Mason](https://img.shields.io/endpoint?url=https%3A%2F%2Ftinyurl.com%2Fmason-badge)](https://github.com/felangel/mason) + +A Bdaya New Flutter App created by [Bdaya Development][bdaya_development_link]. + +## What's Included ✨ + +Out of the box, Bdaya New Flutter App includes: + +- ✅ [Cross Platform Support][flutter_cross_platform_link] - Built-in support for iOS, Android, Web, and Windows (MacOS/Linux coming soon!) +- ✅ [Build Flavors][flutter_flavors_link] - Multiple flavor support for development, staging, and production +- ✅ [Internationalization Support][internationalization_link] - Internationalization support using synthetic code generation to streamline the development process +- ✅ [Sound Null-Safety][null_safety_link] - No more null-dereference exceptions at runtime. Develop with a sound, static type system. +- ✅ [bdaya_flutter_common][bdaya_flutter_common] - A library to combine and standarize the common code we use in our projects into a single package. +- ✅ [get_it][get_it_link] - Integrated get_it architecture for scalable, testable code which offers a simple Service Locator for bdaya app services,and controllers. +- ✅ [bdaya_shared_value][bdaya_shared_value_link] - An opinionated fork of the original package [shared_value][shared_value_link] that is a wrapper over [InheritedModel][InheritedModel_link] , this module allows you to easily manage global state in flutter apps. At a high level, SharedValue puts your variables in an intelligent "container" that is flutter-aware. It can be viewed as a low-boilerplate generalization of the Provider state management solution. +- ✅ [Testing][testing_link] - Unit and Widget Tests with 100% line coverage (Integration Tests coming soon!) +- ✅ [Logging][logging_link] - Built-in, extensible logging to capture uncaught Flutter and Dart Exceptions + +## Output 📦 + +```sh +├── .gitignore +├── .idea +│ └── runConfigurations +│ ├── development.xml +│ ├── production.xml +│ └── staging.xml +├── .vscode +│ ├── extensions.json +│ └── launch.json +├── LICENSE +├── README.md +├── analysis_options.yaml +├── android +├── coverage_badge.svg +├── ios +├── l10n.yaml +├── lib +│ ├── app +│ │ ├── app.dart +│ │ └── view +│ ├── bootstrap.dart +│ ├── counter +│ │ ├── counter.dart +│ │ ├── cubit +│ │ └── view +│ ├── l10n +│ │ ├── arb +│ │ └── l10n.dart +│ ├── main_development.dart +│ ├── main_production.dart +│ └── main_staging.dart +├── pubspec.lock +├── pubspec.yaml +├── test +│ ├── app +│ │ └── view +│ ├── counter +│ │ ├── cubit +│ │ └── view +│ └── helpers +│ ├── helpers.dart +│ └── pump_app.dart +├── web +└── windows +``` + +[bdaya_flutter_common]: https://pub.dev/packages/bdaya_flutter_common +[get_it_link]: https://pub.dev/packages/get_it +[bdaya_shared_value_link]: https://pub.dev/packages/bdaya_shared_value +[shared_value_link]: https://pub.dev/packages/shared_value +[InheritedModel_link]: https://api.flutter.dev/flutter/widgets/InheritedModel-class.html +[flutter_cross_platform_link]: https://flutter.dev/docs/development/tools/sdk/release-notes/supported-platforms +[flutter_flavors_link]: https://flutter.dev/docs/deployment/flavors +[internationalization_link]: https://flutter.dev/docs/development/accessibility-and-localization/internationalization +[license_badge]: https://img.shields.io/badge/license-MIT-blue.svg +[license_link]: https://opensource.org/licenses/MIT +[logging_link]: https://api.flutter.dev/flutter/dart-developer/log.html +[null_safety_link]: https://flutter.dev/docs/null-safety +[testing_link]: https://flutter.dev/docs/testing +[bdaya_development_link]: https://bdaya-dev.com/ +[very_good_core_link]: https://github.com/VeryGoodOpenSource/very_good_templates/tree/main/very_good_core diff --git a/bdaya_new_flutter_app/__brick__/{{project_name.snakeCase()}}/blank.md b/bdaya_new_flutter_app/__brick__/{{project_name.snakeCase()}}/blank.md new file mode 100644 index 0000000..5ecb694 --- /dev/null +++ b/bdaya_new_flutter_app/__brick__/{{project_name.snakeCase()}}/blank.md @@ -0,0 +1,3 @@ +# Blank file + +This File should be removed, If you are reading this, please remove it. diff --git a/bdaya_new_flutter_app/analysis_options.yaml b/bdaya_new_flutter_app/analysis_options.yaml new file mode 100644 index 0000000..162dea2 --- /dev/null +++ b/bdaya_new_flutter_app/analysis_options.yaml @@ -0,0 +1,4 @@ +analyzer: + exclude: + - __brick__/** + - lib/template/** diff --git a/bdaya_new_flutter_app/brick.yaml b/bdaya_new_flutter_app/brick.yaml new file mode 100644 index 0000000..4b51e93 --- /dev/null +++ b/bdaya_new_flutter_app/brick.yaml @@ -0,0 +1,29 @@ +name: bdaya_new_flutter_app +description: A Bdaya New Flutter app created by Bdaya Development. +repository: https://github.com/Bdaya-Dev/bricks/tree/main/bdaya_new_flutter_app +version: 0.0.1 + +environment: + mason: ^0.1.0 + +vars: + project_name: + type: string + description: The project name + default: my_app + prompt: What is the project name? + org_name: + type: string + description: The organization name + default: com.example + prompt: What is the organization name? + application_id: + type: string + description: The application id on Android, Bundle ID on iOS and company name on Windows. If omitted value will be formed by org_name + . + project_name. + default: example.com + prompt: What is the application id? + description: + type: string + description: A short project description + default: A Bdaya Development App + prompt: What is the project description? diff --git a/bdaya_new_flutter_app/config.json b/bdaya_new_flutter_app/config.json new file mode 100644 index 0000000..419d1e6 --- /dev/null +++ b/bdaya_new_flutter_app/config.json @@ -0,0 +1,6 @@ +{ + "project_name": "test_app", + "org_name": "bdaya_development", + "application_id": "bdaya.dev.test", + "description": "bdaya_new_flutter_app test configuration" +} diff --git a/bdaya_new_flutter_app/coverage_badge.svg b/bdaya_new_flutter_app/coverage_badge.svg new file mode 100644 index 0000000..88bfadf --- /dev/null +++ b/bdaya_new_flutter_app/coverage_badge.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + coverage + coverage + 100% + 100% + + \ No newline at end of file diff --git a/bdaya_new_flutter_app/hooks/analysis_options.yaml b/bdaya_new_flutter_app/hooks/analysis_options.yaml new file mode 100644 index 0000000..77a5e52 --- /dev/null +++ b/bdaya_new_flutter_app/hooks/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml +analyzer: + exclude: + - template/** diff --git a/bdaya_new_flutter_app/hooks/lib/bdaya_new_flutter_app_hooks.dart b/bdaya_new_flutter_app/hooks/lib/bdaya_new_flutter_app_hooks.dart new file mode 100644 index 0000000..ab95c8d --- /dev/null +++ b/bdaya_new_flutter_app/hooks/lib/bdaya_new_flutter_app_hooks.dart @@ -0,0 +1 @@ +export 'src/_exports.dart'; diff --git a/bdaya_new_flutter_app/hooks/lib/src/_exports.dart b/bdaya_new_flutter_app/hooks/lib/src/_exports.dart new file mode 100644 index 0000000..767f7ab --- /dev/null +++ b/bdaya_new_flutter_app/hooks/lib/src/_exports.dart @@ -0,0 +1 @@ +export 'models/_exports.dart'; diff --git a/bdaya_new_flutter_app/hooks/lib/src/consts.dart b/bdaya_new_flutter_app/hooks/lib/src/consts.dart new file mode 100644 index 0000000..d76695c --- /dev/null +++ b/bdaya_new_flutter_app/hooks/lib/src/consts.dart @@ -0,0 +1,38 @@ +const kSdk = '>=3.4.0 <4.0.0'; +const kFlutter = '>=3.0.0'; + +const kDep = [ + 'async', + 'bdaya_flutter_common', + 'collection', + 'go_router', + 'grpc', + 'logging', +]; + +const kDevDep = [ + 'build_runner', + 'flutter_lints', + 'injectable_generator', + 'mocktail', +]; + +final kDirs = [ + 'extensions', + 'gen', + 'l10n', + 'mixins', + 'models', + 'pages', + 'services', + 'utils', + 'widgets', +]; + +final kFiles = [ + 'bootstrap.dart', + 'common.dart', + 'get_it_config.dart', + 'injectable_module.dart', + 'routes.dart', +]; diff --git a/bdaya_new_flutter_app/hooks/lib/src/models/_exports.dart b/bdaya_new_flutter_app/hooks/lib/src/models/_exports.dart new file mode 100644 index 0000000..2b93c52 --- /dev/null +++ b/bdaya_new_flutter_app/hooks/lib/src/models/_exports.dart @@ -0,0 +1,7 @@ +export 'android_application_id.dart'; +export 'android_namespace.dart'; +export 'apple_application_id.dart'; +export 'exceptions.dart'; +export 'bdaya_new_flutter_app_configuration.dart'; +export 'windows_application_id.dart'; +export 'progress_args.dart'; diff --git a/bdaya_new_flutter_app/hooks/lib/src/models/android_application_id.dart b/bdaya_new_flutter_app/hooks/lib/src/models/android_application_id.dart new file mode 100644 index 0000000..3c03741 --- /dev/null +++ b/bdaya_new_flutter_app/hooks/lib/src/models/android_application_id.dart @@ -0,0 +1,71 @@ +import 'package:mason/mason.dart'; + +/// {@template android_application_id} +/// Every Android app has a unique application ID that looks like a Java or +/// Kotlin package name, such as `com.example.app`. +/// +/// This ID uniquely identifies your app on the device and in the Google Play +/// Store. +/// +/// See also: +/// +/// * [Set the application ID Android documentation](https://developer.android.com/build/configure-app-module#set-application-id) +/// {@endtemplate} +extension type AndroidApplicationId(String value) { + /// Creates a new [AndroidApplicationId] from the provided [organizationName] + /// and [projectName]. + /// + /// This is the default fallback value for the application ID. + factory AndroidApplicationId.fallback({ + required String organizationName, + required String projectName, + }) { + final segments = []; + for (final segment in organizationName.split('.')) { + if (segment.isEmpty) continue; + segments.add(segment.snakeCase); + } + segments.add(projectName.snakeCase); + + return AndroidApplicationId(segments.join('.')); + } + + /// Checks if the [AndroidApplicationId] is valid, returning `true` if it is + /// and `false` otherwise. + /// + /// Although the application ID looks like a traditional Kotlin or Java + /// package name, the naming rules for the application ID are a bit more + /// restrictive: + /// + /// * It must have at least two segments (one or more dots). + /// * Each segment must start with a letter. + /// * All characters must be alphanumeric or an underscore [a-zA-Z0-9_]. + /// + /// See also: + /// + /// * [Set the application ID Android documentation](https://developer.android.com/build/configure-app-module#set-application-id) + bool get isValid { + final segments = value.split('.'); + if (segments.length < 2) { + // It must have at least two segments (one or more dots). + return false; + } + + final isLetter = RegExp('^[a-zA-Z]'); + final isAlphanumeric = RegExp(r'^[a-zA-Z0-9_]+$'); + + for (final segment in segments) { + if (segment.isEmpty || !isLetter.hasMatch(segment[0])) { + // Each segment must start with a letter. + return false; + } + + if (!isAlphanumeric.hasMatch(segment)) { + // All characters must be alphanumeric or an underscore [a-zA-Z0-9_]. + return false; + } + } + + return true; + } +} diff --git a/bdaya_new_flutter_app/hooks/lib/src/models/android_namespace.dart b/bdaya_new_flutter_app/hooks/lib/src/models/android_namespace.dart new file mode 100644 index 0000000..0e34d9b --- /dev/null +++ b/bdaya_new_flutter_app/hooks/lib/src/models/android_namespace.dart @@ -0,0 +1,40 @@ +import 'package:bdaya_new_flutter_app/bdaya_new_flutter_app_hooks.dart'; + +/// {@template android_namespace} +/// Every Android module has a namespace, which is used as the Kotlin or Java +/// package name for its generated R and BuildConfig classes. +/// +/// The namespace can be different than the application ID, but it is usually +/// kept the same for a simpler workflow. +/// +/// The name you set for the build.gradle file's namespace property should +/// always match your project's base package name, where you keep your +/// activities and other app code. You can have other sub-packages in your +/// project, but those files must import the R class using the namespace from +/// the namespace property. +/// +/// See also: +/// +/// * [Set the namespace Android documentation](https://developer.android.com/studio/build/application-id) +/// {@endtemplate} +extension type AndroidNamespace(String value) { + /// Creates a new [AndroidNamespace] from the provided [applicationId]. + /// + /// If a specific namespace is not provided, the namespace will default to the + /// application ID. + /// + /// It is recommended to keep the namespace the same as the application ID for + /// a simpler workflow: + /// + /// From: [Set the namespace Android documentation](https://developer.android.com/build/configure-app-module#set-namespace) + /// > For a simpler workflow, keep your namespace the same as your application + /// > ID, as they are by default. + /// + /// From: [Set the application ID Android documentation](https://developer.android.com/build/configure-app-module#set-application-id): + /// > Keep the application ID the same as the namespace. The distinction + /// > between the two properties can be a bit confusing, but if you keep them + /// > the same, you have nothing to worry about. + AndroidNamespace.fromApplicationId( + AndroidApplicationId applicationId, + ) : this(applicationId.value); +} diff --git a/bdaya_new_flutter_app/hooks/lib/src/models/apple_application_id.dart b/bdaya_new_flutter_app/hooks/lib/src/models/apple_application_id.dart new file mode 100644 index 0000000..87a2b50 --- /dev/null +++ b/bdaya_new_flutter_app/hooks/lib/src/models/apple_application_id.dart @@ -0,0 +1,32 @@ +import 'package:mason/mason.dart'; + +/// {@template apple_application_id} +/// An App ID identifies your iOS/macOS app in a provisioning profile. +/// +/// An App ID is a two-part string used to identify one or more apps from a +/// single development team. A period (.) separates its parts. +/// +/// See also: +/// +/// * [Register an App ID](https://developer.apple.com/help/account/manage-identifiers/register-an-app-id/) +/// * [Apple Documentation Archive](https://developer.apple.com/library/archive/documentation/General/Conceptual/DevPedia-CocoaCore/AppID.html) +/// {@endtemplate} +extension type AppleApplicationId(String value) { + /// Creates a new [AppleApplicationId] from the provided [organizationName] + /// and [projectName]. + /// + /// This is the default fallback value for the application ID. + factory AppleApplicationId.fallback({ + required String organizationName, + required String projectName, + }) { + final parts = []; + for (final part in organizationName.split('.')) { + if (part.isEmpty) continue; + parts.add(part.paramCase); + } + parts.add(projectName.paramCase); + + return AppleApplicationId(parts.join('.')); + } +} diff --git a/bdaya_new_flutter_app/hooks/lib/src/models/bdaya_new_flutter_app_configuration.dart b/bdaya_new_flutter_app/hooks/lib/src/models/bdaya_new_flutter_app_configuration.dart new file mode 100644 index 0000000..2fcb739 --- /dev/null +++ b/bdaya_new_flutter_app/hooks/lib/src/models/bdaya_new_flutter_app_configuration.dart @@ -0,0 +1,193 @@ +import 'package:equatable/equatable.dart'; +import 'package:bdaya_new_flutter_app/bdaya_new_flutter_app_hooks.dart'; + +/// The variables specified by this hook. +/// +/// The variables can be found in the Brick's `brick.yaml` file. They are +/// initially included in the `HookContext.vars` map. +/// +/// See also: +/// +/// * [brick.yaml documentation](https://docs.brickhub.dev/brick-structure#brickyaml) +enum _BdayaNewFlutterAppConfigurationVariables { + /// {@template bdaya_new_flutter_app_configuration_variables.project_name} + /// The project name. + /// + /// Defaults to `my_app`. + /// {@endtemplate} + projectName._('project_name'), + + /// {@template bdaya_new_flutter_app_configuration_variables.organization_name} + /// The organization name. + /// + /// Defaults to `com.example`. + /// {@endtemplate} + organizationName._('org_name'), + + /// {@template bdaya_new_flutter_app_configuration_variables.application_id} + /// The application id on Android, Bundle ID on iOS and company name on + /// Windows. If omitted value will be formed by org_name + . + project_name. + /// + /// Has no default specified within the `brick.yaml`. + /// {@endtemplate} + applicationId._('application_id'), + + /// {@template bdaya_new_flutter_app_configuration_variables.description} + /// A short project description. + /// + /// Defaults to `A Bdaya App`. + /// {@endtemplate} + description._('description'); + + const _BdayaNewFlutterAppConfigurationVariables._(this.key); + + /// The key used in the `HookContext.vars` [Map]. + /// + /// This should match the variable key in the `brick.yaml`. + final String key; +} + +/// {@template bdaya_new_flutter_app_configuration} +/// Configuration for the `bdaya_new_flutter_app` brick. +/// {@endtemplate} +class BdayaNewFlutterAppConfiguration extends Equatable { + /// {@macro bdaya_new_flutter_app_configuration} + BdayaNewFlutterAppConfiguration({ + String? projectName, + String? organizationName, + String? description, + WindowsApplicationId? windowsApplicationId, + AppleApplicationId? iOsApplicationId, + AppleApplicationId? macOsApplicationId, + AndroidApplicationId? androidApplicationId, + AndroidNamespace? androidNamespace, + }) : projectName = projectName ?? 'my_app', + organizationName = organizationName ?? 'com.example', + description = description ?? 'A Bdaya App' { + this.windowsApplicationId = windowsApplicationId ?? + WindowsApplicationId.fallback( + organizationName: this.organizationName, + projectName: this.projectName, + ); + this.iOsApplicationId = iOsApplicationId ?? + AppleApplicationId.fallback( + organizationName: this.organizationName, + projectName: this.projectName, + ); + this.macOsApplicationId = macOsApplicationId ?? + AppleApplicationId.fallback( + organizationName: this.organizationName, + projectName: this.projectName, + ); + this.androidApplicationId = androidApplicationId ?? + AndroidApplicationId.fallback( + organizationName: this.organizationName, + projectName: this.projectName, + ); + if (!this.androidApplicationId.isValid) { + throw InvalidAndroidApplicationIdFormat(this.androidApplicationId); + } + + this.androidNamespace = androidNamespace ?? + AndroidNamespace.fromApplicationId(this.androidApplicationId); + } + + /// Deserializes a [BdayaNewFlutterAppConfiguration] from a `Map` + /// used to represent the configuration in the `HookContext.vars` map. + factory BdayaNewFlutterAppConfiguration.fromHookVars( + Map vars) { + final projectName = + vars[_BdayaNewFlutterAppConfigurationVariables.projectName.key]; + if (projectName is! String?) { + throw ArgumentError.value( + vars, + 'vars', + '''Expected a value for key "${_BdayaNewFlutterAppConfigurationVariables.projectName.key}" to be of type String?, got $projectName.''', + ); + } + + final organizationName = + vars[_BdayaNewFlutterAppConfigurationVariables.organizationName.key]; + if (organizationName is! String?) { + throw ArgumentError.value( + vars, + 'vars', + '''Expected a value for key "${_BdayaNewFlutterAppConfigurationVariables.organizationName.key}" to be of type String?, got $organizationName.''', + ); + } + + final applicationId = + vars[_BdayaNewFlutterAppConfigurationVariables.applicationId.key]; + if (applicationId is! String?) { + throw ArgumentError.value( + vars, + 'vars', + '''Expected a value for key "${_BdayaNewFlutterAppConfigurationVariables.applicationId.key}" to be of type String?, got $applicationId.''', + ); + } + + final description = + vars[_BdayaNewFlutterAppConfigurationVariables.description.key]; + if (description is! String?) { + throw ArgumentError.value( + vars, + 'vars', + '''Expected a value for key "${_BdayaNewFlutterAppConfigurationVariables.description.key}" to be of type String?, got $description.''', + ); + } + + return BdayaNewFlutterAppConfiguration( + projectName: projectName, + organizationName: organizationName, + iOsApplicationId: applicationId == null || applicationId.isEmpty + ? null + : AppleApplicationId(applicationId), + macOsApplicationId: applicationId == null || applicationId.isEmpty + ? null + : AppleApplicationId(applicationId), + windowsApplicationId: applicationId == null || applicationId.isEmpty + ? null + : WindowsApplicationId(applicationId), + androidApplicationId: applicationId == null || applicationId.isEmpty + ? null + : AndroidApplicationId(applicationId), + description: description, + ); + } + + /// {@macro bdaya_new_flutter_app_configuration_variables.project_name} + final String projectName; + + /// {@macro bdaya_new_flutter_app_configuration_variables.organization_name} + final String organizationName; + + /// {@macro bdaya_new_flutter_app_configuration_variables.description} + final String description; + + /// {@macro windows_application_id} + late final WindowsApplicationId windowsApplicationId; + + /// {@macro apple_application_id} + late final AppleApplicationId iOsApplicationId; + + /// {@macro apple_application_id} + late final AppleApplicationId macOsApplicationId; + + /// {@macro android_namespace} + late final AndroidNamespace androidNamespace; + + /// {@macro android_application_id} + late final AndroidApplicationId androidApplicationId; + + @override + List get props => [ + projectName, + organizationName, + description, + windowsApplicationId, + iOsApplicationId, + macOsApplicationId, + androidNamespace, + androidApplicationId, + ]; +} diff --git a/bdaya_new_flutter_app/hooks/lib/src/models/exceptions.dart b/bdaya_new_flutter_app/hooks/lib/src/models/exceptions.dart new file mode 100644 index 0000000..5d91ac5 --- /dev/null +++ b/bdaya_new_flutter_app/hooks/lib/src/models/exceptions.dart @@ -0,0 +1,54 @@ +import 'package:bdaya_new_flutter_app/bdaya_new_flutter_app_hooks.dart'; + +/// {@template bdaya_new_flutter_app_exception} +/// An exception thrown by the `bdaya_new_flutter_app` hooks. +/// {@endtemplate} +abstract class BdayaNewFlutterAppHooksException implements Exception { + /// {@macro bdaya_new_flutter_app_exception} + BdayaNewFlutterAppHooksException({ + required this.description, + required this.help, + }); + + /// A message describing the exception. + final String description; + + /// A message describing how to resolve the exception, or signposting to + /// additional resources. + final String help; + + @override + String toString() => ''' +[$BdayaNewFlutterAppHooksException] $description. + +$help +'''; +} + +/// {@template InvalidAndroidApplicationIdFormat} +/// An exception thrown when an invalid Android application ID format is +/// given. +/// {@endtemplate} +class InvalidAndroidApplicationIdFormat + extends BdayaNewFlutterAppHooksException { + /// {@macro InvalidAndroidApplicationIdFormat} + InvalidAndroidApplicationIdFormat(AndroidApplicationId applicationId) + : super( + description: + '''An invalid Android application ID (${applicationId.value}) format was provided.''', + help: ''' +Try adjusting your Android application ID (${applicationId.value}) to match the following format: + +* It must have at least two segments (one or more dots). +* Each segment must start with a letter. +* All characters must be alphanumeric or an underscore [a-zA-Z0-9_]. + +Although the application ID looks like a traditional Kotlin or Java +package name, the naming rules for the application ID are a bit more +restrictive. + +For more information, see the "Set the application ID" Android documentation: +* https://developer.android.com/build/configure-app-module#set-application-id. +''', + ); +} diff --git a/bdaya_new_flutter_app/hooks/lib/src/models/progress_args.dart b/bdaya_new_flutter_app/hooks/lib/src/models/progress_args.dart new file mode 100644 index 0000000..250ac7f --- /dev/null +++ b/bdaya_new_flutter_app/hooks/lib/src/models/progress_args.dart @@ -0,0 +1,21 @@ +/// Arguments for the [Progress.run] method. +/// +/// The [executable] argument is the executable to run. The [args] argument +/// are the arguments to pass to the executable. The [label] argument is the +/// label to display while the process is running. The +/// [workingDirectory] argument is the directory to run the process in. +/// +/// This class is immutable. +class ProgressArgs { + final String executable; + final List args; + final String label; + final String workingDirectory; + + const ProgressArgs({ + required this.executable, + required this.args, + required this.label, + required this.workingDirectory, + }); +} diff --git a/bdaya_new_flutter_app/hooks/lib/src/models/windows_application_id.dart b/bdaya_new_flutter_app/hooks/lib/src/models/windows_application_id.dart new file mode 100644 index 0000000..5498e6b --- /dev/null +++ b/bdaya_new_flutter_app/hooks/lib/src/models/windows_application_id.dart @@ -0,0 +1,26 @@ +import 'package:mason/mason.dart'; + +/// {@template windows_application_id} +/// Identifies the Windows application. +/// +/// Gets used as part of the Runner.rc company name and its copyright notice. +/// {@endtemplate} +extension type WindowsApplicationId(String value) { + /// Creates a new [WindowsApplicationId] from the provided [organizationName] + /// and [projectName]. + /// + /// This is the default fallback value for the application ID. + factory WindowsApplicationId.fallback({ + required String organizationName, + required String projectName, + }) { + final parts = []; + for (final part in organizationName.split('.')) { + if (part.isEmpty) continue; + parts.add(part.paramCase); + } + parts.add(projectName.paramCase); + + return WindowsApplicationId(parts.join('.')); + } +} diff --git a/bdaya_new_flutter_app/hooks/post_gen.dart b/bdaya_new_flutter_app/hooks/post_gen.dart new file mode 100644 index 0000000..b325a1c --- /dev/null +++ b/bdaya_new_flutter_app/hooks/post_gen.dart @@ -0,0 +1,668 @@ +import 'dart:io'; +import 'package:bdaya_new_flutter_app/src/consts.dart'; +import 'package:path/path.dart' as p; +import 'package:mason/mason.dart'; +import 'package:bdaya_new_flutter_app/bdaya_new_flutter_app_hooks.dart'; +import 'package:yaml_edit/yaml_edit.dart'; + +/// Main hook entry point for creating a new Flutter app +Future run(HookContext context) async { + final logger = context.logger; + final configuration = + BdayaNewFlutterAppConfiguration.fromHookVars(context.vars); + final projectDir = Directory(configuration.projectName.snakeCase); + final templateDirectory = _getTemplateDirectory(); + + logger.info('Creating Flutter app: ${configuration.projectName}'); + + try { + await _ensureVeryGoodCli(logger); + await _cleanupInitialFiles(projectDir, logger); + await _createFlutterApp(configuration, projectDir.path, logger); + await _setupProject(configuration, projectDir, templateDirectory, logger); + await _bootstrap(projectDir.path, configuration.projectName, logger); + + logger.success(''' +✅ Project created successfully! + Run "cd ${configuration.projectName.snakeCase}" to enter the directory. + Run "code ." to open in VSCode. +'''); + } catch (e, stackTrace) { + logger.err('Project creation failed: $e'); + logger.detail(stackTrace.toString()); + rethrow; + } +} + +/// Gets the template directory path +Directory _getTemplateDirectory() { + final scriptPath = Platform.script.toFilePath(); + final templatePath = scriptPath.split('/build/hooks')[0] + '/template/'; + return Directory(templatePath); +} + +/// Ensures very_good_cli is installed +Future _ensureVeryGoodCli(Logger logger) async { + if (!await _isVeryGoodInstalled(logger)) { + await _installVeryGoodCli(logger); + } +} + +/// Checks if very_good_cli is installed +Future _isVeryGoodInstalled(Logger logger) async { + final progress = logger.progress('Checking very_good_cli installation'); + + try { + final result = await Process.run('very_good', ['--version']); + + if (result.exitCode == 0) { + progress.complete('very_good_cli is already installed'); + logger.info('Version: ${result.stdout.trim()}'); + return true; + } + + progress.update('very_good_cli not found'); + return false; + } catch (e) { + progress.update('very_good_cli not accessible'); + return false; + } finally { + progress.cancel(); + } +} + +/// Installs very_good_cli globally +Future _installVeryGoodCli(Logger logger) async { + final progress = logger.progress('Installing very_good_cli'); + + try { + final result = await Process.run( + 'dart', ['pub', 'global', 'activate', 'very_good_cli']); + + if (result.exitCode == 0) { + progress.complete('Successfully installed very_good_cli'); + } else { + throw ProcessException( + 'dart', + ['pub', 'global', 'activate', 'very_good_cli'], + 'Installation failed: ${result.stderr}', + result.exitCode, + ); + } + } catch (e) { + progress.fail('Error installing very_good_cli: $e'); + rethrow; + } +} + +/// Cleans up initial files that shouldn't be in the project +Future _cleanupInitialFiles(Directory projectDir, Logger logger) async { + await _removeFileIfExists(p.join(projectDir.path, 'blank.md'), logger); +} + +/// Sets up the project with configuration and templates +Future _setupProject( + BdayaNewFlutterAppConfiguration configuration, + Directory projectDir, + Directory templateDirectory, + Logger logger, +) async { + await _updatePubspec(configuration, projectDir.path, logger); + await _updateAnalysisOptions(configuration, projectDir.path, logger); + await _addDependencies(configuration, projectDir.path, logger); + await _cleanupUnwantedFiles(projectDir.path, logger); + await _setupTemplateFiles(projectDir.path, templateDirectory.path, logger); + await _updateTestFiles(configuration, projectDir.path); +} + +Future _updateAnalysisOptions( + BdayaNewFlutterAppConfiguration configuration, + String projectPath, + Logger logger) async { + final progress = logger.progress( + 'Updating analysis_options.yaml for ${configuration.projectName}'); + final analysisOptionsFile = + File(p.join(projectPath, 'analysis_options.yaml')); + + try { + if (!await analysisOptionsFile.exists()) { + throw FileSystemException('analysis_options.yaml not found', projectPath); + } + + final content = await analysisOptionsFile.readAsString(); + final editor = YamlEditor(content); + + editor.update(['include'], r'package:flutter_lints/flutter.yaml'); + editor.remove(['analyzer']); + editor.remove(['linter']); + await analysisOptionsFile.writeAsString(editor.toString()); + progress.complete('Updated analysis_options.yaml'); + } catch (e) { + progress.fail('Failed to update analysis_options.yaml: $e'); + rethrow; + } +} + +/// Adds both regular and dev dependencies +Future _addDependencies( + BdayaNewFlutterAppConfiguration configuration, + String projectPath, + Logger logger, +) async { + await _addPackages(configuration, projectPath, logger, kDep, isDev: false); + await _addPackages(configuration, projectPath, logger, kDevDep, isDev: true); +} + +/// Sets up template files and directories +Future _setupTemplateFiles( + String projectPath, + String templatePath, + Logger logger, +) async { + await _replaceAppView(projectPath, templatePath); + await _copyTemplateAssets(projectPath, templatePath); + await _copyProjectFiles(projectPath, templatePath, logger, kFiles, kDirs); + await _setupLocalizationStructure(projectPath, templatePath, logger); +} + +/// Copies template assets like l10n configuration +Future _copyTemplateAssets( + String projectPath, String templatePath) async { + final assetsToSync = [ + ('l10n.yaml', 'l10n.yaml'), + ('l10n_errors.txt', 'l10n_errors.txt'), + ]; + + for (final (source, dest) in assetsToSync) { + final sourceFile = File(p.join(templatePath, source)); + final destFile = File(p.join(projectPath, dest)); + + // Only copy if source exists + if (await sourceFile.exists()) { + await _copyFile(sourceFile.path, destFile.path); + } + } +} + +/// Sets up the localization directory structure +Future _setupLocalizationStructure( + String projectPath, String templatePath, Logger logger) async { + final l10nDir = Directory(p.join(projectPath, 'lib', 'l10n')); + + // Ensure l10n directory exists + if (!await l10nDir.exists()) { + await l10nDir.create(recursive: true); + logger.info('Created lib/l10n directory'); + } + + // Copy any .arb files from template if they exist + final templateL10nDir = Directory(p.join(templatePath, 'lib', 'l10n')); + if (await templateL10nDir.exists()) { + await for (final entity in templateL10nDir.list()) { + if (entity is File && entity.path.endsWith('.arb')) { + final filename = p.basename(entity.path); + await _copyFile( + entity.path, + p.join(l10nDir.path, filename), + ); + } + } + } else { + // Create a basic app_en.arb file if template doesn't have one + await _createBasicArbFile(l10nDir.path); + } +} + +/// Creates a basic ARB file for localization +Future _createBasicArbFile(String l10nPath) async { + final arbFile = File(p.join(l10nPath, 'app_en.arb')); + const basicArbContent = ''' +{ + "@@locale": "en", + "appTitle": "Flutter App", + "@appTitle": { + "description": "The title of the application" + } +} +'''; + + await arbFile.writeAsString(basicArbContent); +} + +/// Removes unwanted folders and files +Future _cleanupUnwantedFiles(String projectPath, Logger logger) async { + // Only remove the counter folder, keep lib/l10n for now + final foldersToRemove = ['lib/counter', 'lib/l10n', 'test/counter']; + final filesToRemove = [ + p.join('lib', 'bootstrap.dart'), + ]; + + await _removeFolders(projectPath, logger, foldersToRemove); + + for (final file in filesToRemove) { + await _removeFileIfExists(p.join(projectPath, file), logger); + } +} + +/// Updates test files to work with new structure +Future _updateTestFiles( + BdayaNewFlutterAppConfiguration configuration, + String projectPath, +) async { + await _replaceInFile( + filePath: p.join(projectPath, 'test/app/view/app_test.dart'), + removeLines: [ + "expect(find.byType(CounterPage), findsOneWidget);", + "import 'package:${configuration.projectName.snakeCase}/counter/counter.dart';", + ], + ); + + await _replaceInFile( + filePath: p.join(projectPath, 'test/helpers/pump_app.dart'), + replacements: { + "import 'package:${configuration.projectName.snakeCase}/l10n/l10n.dart';": + "import 'package:${configuration.projectName.snakeCase}/gen/l10n/app_localizations.dart';", + }, + ); +} + +/// Creates Flutter app using very_good_cli +Future _createFlutterApp( + BdayaNewFlutterAppConfiguration configuration, + String workingDirectory, + Logger logger, +) async { + final progress = logger + .progress('Creating ${configuration.projectName} using very_good_cli'); + + try { + final result = await Process.run( + 'very_good', + [ + 'create', + 'flutter_app', + '.', + '--description', + configuration.description, + '--org-name', + configuration.organizationName, + '--application-id', + configuration.androidApplicationId.value, + ], + workingDirectory: workingDirectory, + ); + + if (result.exitCode != 0) { + throw ProcessException( + 'very_good', + ['create', 'flutter_app'], + 'Flutter app creation failed:\n${result.stderr}', + result.exitCode, + ); + } + + progress.complete('Successfully created Flutter app'); + logger.detail(result.stdout); + } catch (e) { + progress.fail('Error creating Flutter app: $e'); + rethrow; + } +} + +/// Updates pubspec.yaml with required configuration +Future _updatePubspec( + BdayaNewFlutterAppConfiguration configuration, + String projectPath, + Logger logger, +) async { + final progress = + logger.progress('Updating pubspec.yaml for ${configuration.projectName}'); + final pubspecFile = File(p.join(projectPath, 'pubspec.yaml')); + + try { + if (!await pubspecFile.exists()) { + throw FileSystemException('pubspec.yaml not found', projectPath); + } + + final pubspecContent = await pubspecFile.readAsString(); + final editor = YamlEditor(pubspecContent); + + _updatePubspecSections(editor); + + await pubspecFile.writeAsString(editor.toString()); + progress.complete('Updated pubspec.yaml'); + } catch (e) { + progress.fail('Failed to update pubspec.yaml: $e'); + rethrow; + } +} + +/// Updates individual sections of pubspec.yaml +void _updatePubspecSections(YamlEditor editor) { + // Helper to ensure section exists + void ensureSection(String key) { + try { + editor.parseAt([key]); + } catch (_) { + editor.update([key], {}); + } + } + + // Update environment + ensureSection('environment'); + editor.update(['environment', 'sdk'], kSdk); + editor.update(['environment', 'flutter'], kFlutter); + + // Update dependencies + ensureSection('dependencies'); + editor.update(['dependencies', 'flutter'], {'sdk': 'flutter'}); + editor.update(['dependencies', 'flutter_localizations'], {'sdk': 'flutter'}); + editor.update(['dependencies', 'intl'], 'any'); + + // Remove unwanted dependencies + final depsToRemove = ['bloc', 'flutter_bloc']; + for (final dep in depsToRemove) { + try { + editor.remove(['dependencies', dep]); + } catch (_) { + // Ignore if dependency doesn't exist + } + } + + // Update dev dependencies + ensureSection('dev_dependencies'); + editor.update(['dev_dependencies', 'flutter_test'], {'sdk': 'flutter'}); + editor.update(['dev_dependencies', 'integration_test'], {'sdk': 'flutter'}); + + // Remove unwanted dev dependencies + final devDepsToRemove = ['bloc_test', 'very_good_analysis', 'mocktail']; + for (final dep in devDepsToRemove) { + try { + editor.remove(['dev_dependencies', dep]); + } catch (_) { + // Ignore if dependency doesn't exist + } + } +} + +/// Adds packages to pubspec.yaml +Future _addPackages( + BdayaNewFlutterAppConfiguration configuration, + String projectPath, + Logger logger, + List packages, { + required bool isDev, +}) async { + if (packages.isEmpty) return; + + final progress = logger.progress( + 'Adding ${isDev ? 'dev ' : ''}dependencies: ${packages.join(', ')}'); + + try { + final result = await Process.run( + 'dart', + [ + 'pub', + 'add', + if (isDev) '--dev', + ...packages, + ], + workingDirectory: projectPath, + ); + + if (result.exitCode == 0) { + progress + .complete('Successfully added ${isDev ? 'dev ' : ''}dependencies'); + } else { + throw ProcessException( + 'dart', + ['pub', 'add'], + 'Failed to add packages: ${result.stderr}', + result.exitCode, + ); + } + } catch (e) { + progress.fail('Error adding packages: $e'); + rethrow; + } +} + +/// Replaces app view with template version +Future _replaceAppView(String projectPath, String templatePath) async { + final source = File(p.join(templatePath, 'lib', 'app', 'view', 'app.dart')); + final destination = + File(p.join(projectPath, 'lib', 'app', 'view', 'app.dart')); + + if (!await source.exists()) { + throw FileSystemException('Template app.dart not found', source.path); + } + + try { + await destination.parent.create(recursive: true); + await source.copy(destination.path); + } catch (e) { + throw FileSystemException( + 'Failed to replace app view: $e', destination.path); + } +} + +/// Copies project files from template +Future _copyProjectFiles( + String projectPath, + String templatePath, + Logger logger, + List fileNames, + List dirNames, +) async { + final progress = logger.progress('Copying template files'); + + try { + // Copy individual files + for (final fileName in fileNames) { + await _copyFile( + p.join(templatePath, 'lib', fileName), + p.join(projectPath, 'lib', fileName), + ); + } + + // Copy directories + for (final dirName in dirNames) { + await _copyDirectory( + Directory(p.join(templatePath, 'lib', dirName)), + Directory(p.join(projectPath, 'lib', dirName)), + ); + } + + progress.complete('Copied template files'); + } catch (e) { + progress.fail('Failed to copy template files: $e'); + rethrow; + } +} + +/// Recursively copies a directory +Future _copyDirectory(Directory source, Directory destination) async { + if (!await source.exists()) { + throw FileSystemException('Source directory not found', source.path); + } + + await destination.create(recursive: true); + + await for (final entity in source.list(recursive: false)) { + final newPath = p.join(destination.path, p.basename(entity.path)); + + if (entity is Directory) { + await _copyDirectory(entity, Directory(newPath)); + } else if (entity is File) { + await _copyFile(entity.path, newPath); + } + } +} + +/// Copies a single file +Future _copyFile(String sourcePath, String destinationPath) async { + final sourceFile = File(sourcePath); + + if (!await sourceFile.exists()) { + throw FileSystemException('Source file not found', sourcePath); + } + + try { + final destinationFile = File(destinationPath); + await destinationFile.parent.create(recursive: true); + await sourceFile.copy(destinationPath); + } catch (e) { + throw FileSystemException('Failed to copy file: $e', destinationPath); + } +} + +/// Removes multiple folders +Future _removeFolders( + String projectPath, Logger logger, List folders) async { + for (final folder in folders) { + await _deleteFolder(p.join(projectPath, folder), logger); + } +} + +/// Deletes a folder recursively +Future _deleteFolder(String folderPath, Logger logger) async { + final directory = Directory(folderPath); + + if (!await directory.exists()) { + return; // Already doesn't exist + } + + try { + await directory.delete(recursive: true); + logger.info('Deleted folder: $folderPath'); + } catch (e) { + logger.err('Failed to delete folder $folderPath: $e'); + rethrow; + } +} + +/// Removes a file if it exists +Future _removeFileIfExists(String filePath, Logger logger) async { + final file = File(filePath); + + if (await file.exists()) { + try { + await file.delete(); + logger.info('Removed file: $filePath'); + } catch (e) { + logger.err('Failed to remove file $filePath: $e'); + rethrow; + } + } +} + +/// Replaces content in a file +Future _replaceInFile({ + required String filePath, + Map replacements = const {}, + List removeLines = const [], +}) async { + final file = File(filePath); + + if (!await file.exists()) { + throw FileSystemException('File not found for replacement', filePath); + } + + try { + String content = await file.readAsString(); + + // Apply replacements + for (final MapEntry(:key, :value) in replacements.entries) { + content = content.replaceAll(key, value); + } + + // Remove specific lines + if (removeLines.isNotEmpty) { + final lines = content.split('\n'); + final filteredLines = lines.where((line) { + return !removeLines.any((pattern) => line.contains(pattern)); + }).toList(); + content = filteredLines.join('\n'); + } + + await file.writeAsString(content); + } catch (e) { + throw FileSystemException('Failed to update file: $e', filePath); + } +} + +/// Bootstraps the project by running necessary commands +Future _bootstrap( + String projectPath, String projectName, Logger logger) async { + final commands = [ + _BootstrapCommand( + executable: 'flutter', + args: ['pub', 'get'], + label: 'Getting dependencies', + ), + _BootstrapCommand( + executable: 'flutter', + args: ['gen-l10n'], + label: 'Generating l10n', + ), + _BootstrapCommand( + executable: 'dart', + args: ['run', 'build_runner', 'build', '--delete-conflicting-outputs'], + label: 'Running build_runner', + ), + ]; + + for (final command in commands) { + final progress = logger.progress(command.label); + + try { + final result = await Process.run( + command.executable, + command.args, + workingDirectory: projectPath, + ); + + if (result.exitCode == 0) { + progress.complete('${command.label} completed'); + } else { + throw ProcessException( + command.executable, + command.args, + '${command.label} failed: ${result.stderr}', + result.exitCode, + ); + } + } catch (e) { + progress.fail('${command.label} failed: $e'); + rethrow; + } + } +} + +/// Helper class for bootstrap commands +class _BootstrapCommand { + const _BootstrapCommand({ + required this.executable, + required this.args, + required this.label, + }); + + final String executable; + final List args; + final String label; +} + +/// Custom exception for process failures +class ProcessException implements Exception { + const ProcessException( + this.executable, this.arguments, this.message, this.exitCode); + + final String executable; + final List arguments; + final String message; + final int exitCode; + + @override + String toString() => + 'ProcessException: $executable ${arguments.join(' ')}\n$message (exit code: $exitCode)'; +} diff --git a/bdaya_new_flutter_app/hooks/pubspec.yaml b/bdaya_new_flutter_app/hooks/pubspec.yaml new file mode 100644 index 0000000..c753dcb --- /dev/null +++ b/bdaya_new_flutter_app/hooks/pubspec.yaml @@ -0,0 +1,14 @@ +name: bdaya_new_flutter_app + +environment: + sdk: ^3.6.0 + +dependencies: + clock: ^1.1.1 + equatable: ^2.0.5 + mason: ^0.1.0 + yaml_edit: ^2.2.2 + +dev_dependencies: + mocktail: ^1.0.3 + test: ^1.25.2 diff --git a/bdaya_new_flutter_app/hooks/template/.flutter-plugins-dependencies b/bdaya_new_flutter_app/hooks/template/.flutter-plugins-dependencies new file mode 100644 index 0000000..be744ce --- /dev/null +++ b/bdaya_new_flutter_app/hooks/template/.flutter-plugins-dependencies @@ -0,0 +1 @@ +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"integration_test","path":"/home/ahmed-ali/snap/flutter/common/flutter/packages/integration_test/","native_build":true,"dependencies":[],"dev_dependency":true},{"name":"shared_preferences_foundation","path":"/home/ahmed-ali/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.4/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false}],"android":[{"name":"integration_test","path":"/home/ahmed-ali/snap/flutter/common/flutter/packages/integration_test/","native_build":true,"dependencies":[],"dev_dependency":true},{"name":"shared_preferences_android","path":"/home/ahmed-ali/.pub-cache/hosted/pub.dev/shared_preferences_android-2.4.11/","native_build":true,"dependencies":[],"dev_dependency":false}],"macos":[{"name":"shared_preferences_foundation","path":"/home/ahmed-ali/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.4/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false}],"linux":[{"name":"path_provider_linux","path":"/home/ahmed-ali/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_linux","path":"/home/ahmed-ali/.pub-cache/hosted/pub.dev/shared_preferences_linux-2.4.1/","native_build":false,"dependencies":["path_provider_linux"],"dev_dependency":false}],"windows":[{"name":"path_provider_windows","path":"/home/ahmed-ali/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_windows","path":"/home/ahmed-ali/.pub-cache/hosted/pub.dev/shared_preferences_windows-2.4.1/","native_build":false,"dependencies":["path_provider_windows"],"dev_dependency":false}],"web":[{"name":"shared_preferences_web","path":"/home/ahmed-ali/.pub-cache/hosted/pub.dev/shared_preferences_web-2.4.3/","dependencies":[],"dev_dependency":false}]},"dependencyGraph":[{"name":"integration_test","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"shared_preferences","dependencies":["shared_preferences_android","shared_preferences_foundation","shared_preferences_linux","shared_preferences_web","shared_preferences_windows"]},{"name":"shared_preferences_android","dependencies":[]},{"name":"shared_preferences_foundation","dependencies":[]},{"name":"shared_preferences_linux","dependencies":["path_provider_linux"]},{"name":"shared_preferences_web","dependencies":[]},{"name":"shared_preferences_windows","dependencies":["path_provider_windows"]}],"date_created":"2025-08-25 23:44:37.706679","version":"3.32.6","swift_package_manager_enabled":{"ios":false,"macos":false}} \ No newline at end of file diff --git a/bdaya_new_flutter_app/hooks/template/l10n.yaml b/bdaya_new_flutter_app/hooks/template/l10n.yaml new file mode 100644 index 0000000..6597d10 --- /dev/null +++ b/bdaya_new_flutter_app/hooks/template/l10n.yaml @@ -0,0 +1,7 @@ +arb-dir: lib/l10n/arb +template-arb-file: app_en.arb +output-localization-file: app_localizations.dart +nullable-getter: false +untranslated-messages-file: l10n_errors.txt +output-dir: lib/gen/l10n +synthetic-package: false diff --git a/bdaya_new_flutter_app/hooks/template/l10n_errors.txt b/bdaya_new_flutter_app/hooks/template/l10n_errors.txt new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/bdaya_new_flutter_app/hooks/template/l10n_errors.txt @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/bdaya_new_flutter_app/hooks/template/lib/app/app.dart b/bdaya_new_flutter_app/hooks/template/lib/app/app.dart new file mode 100644 index 0000000..f23ab3c --- /dev/null +++ b/bdaya_new_flutter_app/hooks/template/lib/app/app.dart @@ -0,0 +1 @@ +export 'view/app.dart'; diff --git a/bdaya_new_flutter_app/hooks/template/lib/app/view/app.dart b/bdaya_new_flutter_app/hooks/template/lib/app/view/app.dart new file mode 100644 index 0000000..5733f0e --- /dev/null +++ b/bdaya_new_flutter_app/hooks/template/lib/app/view/app.dart @@ -0,0 +1,26 @@ +import 'package:go_router/go_router.dart'; +import '../../common.dart'; + +class App extends StatelessWidget { + const App({super.key}); + + @override + Widget build(BuildContext context) { + final themeService = getIt(); + final locale = themeService.locale.of(context); + final themeMode = themeService.themeMode.of(context); + + return MaterialApp.router( + builder: (context, child) => SplashScreen(child: child), + // onGenerateTitle: (context) => context.l10n.appTitle, // TODO add title to l10n + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + routerConfig: getIt(), + locale: locale, + // theme: AppColorScheme.lightThemeData(fontFamily), + themeMode: themeMode, + // darkTheme: AppColorScheme.darkThemeData(fontFamily), + // debugShowCheckedModeBanner: false, + ); + } +} diff --git a/bdaya_new_flutter_app/hooks/template/lib/bootstrap.dart b/bdaya_new_flutter_app/hooks/template/lib/bootstrap.dart new file mode 100644 index 0000000..dc4ed33 --- /dev/null +++ b/bdaya_new_flutter_app/hooks/template/lib/bootstrap.dart @@ -0,0 +1,24 @@ +import 'dart:async'; + +import 'common.dart'; + +Future bootstrap(FutureOr Function() builder) async { + WidgetsFlutterBinding.ensureInitialized(); + + // Optional, business based,: Lock device orientation to portrait mode + // await SystemChrome.setPreferredOrientations([ + // DeviceOrientation.portraitUp, + // ]); + + Logger.root.level = Level.ALL; + Logger.root.onRecord.listen( + bdayaOnRecordHandlerFactory( + showSequenceNumber: false, showTime: false, showError: true), + ); + setPathUrlStrategy(); + getIt.allowReassignment = true; + + configureDependencies(); + + runApp(SharedValue.wrapApp(await builder())); +} diff --git a/bdaya_new_flutter_app/hooks/template/lib/common.dart b/bdaya_new_flutter_app/hooks/template/lib/common.dart new file mode 100644 index 0000000..2f61606 --- /dev/null +++ b/bdaya_new_flutter_app/hooks/template/lib/common.dart @@ -0,0 +1,14 @@ +export 'package:bdaya_flutter_common/bdaya_flutter_common.dart'; +export 'package:flutter/material.dart' hide Notification; +export 'package:collection/collection.dart'; + +export 'get_it_config.dart'; +export 'routes.dart'; +export 'pages/_exports.dart'; +export 'extensions/_exports.dart'; +export 'services/_exports.dart'; +export 'gen/_exports.dart'; +export 'models/_exports.dart'; +export 'mixins/_exports.dart'; +export 'widgets/_exports.dart'; +export 'utils/_exports.dart'; diff --git a/bdaya_new_flutter_app/hooks/template/lib/extensions/_exports.dart b/bdaya_new_flutter_app/hooks/template/lib/extensions/_exports.dart new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/bdaya_new_flutter_app/hooks/template/lib/extensions/_exports.dart @@ -0,0 +1 @@ + diff --git a/bdaya_new_flutter_app/hooks/template/lib/gen/_exports.dart b/bdaya_new_flutter_app/hooks/template/lib/gen/_exports.dart new file mode 100644 index 0000000..e10b909 --- /dev/null +++ b/bdaya_new_flutter_app/hooks/template/lib/gen/_exports.dart @@ -0,0 +1 @@ +export 'l10n/_exports.dart'; diff --git a/bdaya_new_flutter_app/hooks/template/lib/gen/l10n/_exports.dart b/bdaya_new_flutter_app/hooks/template/lib/gen/l10n/_exports.dart new file mode 100644 index 0000000..c4f6eb9 --- /dev/null +++ b/bdaya_new_flutter_app/hooks/template/lib/gen/l10n/_exports.dart @@ -0,0 +1,9 @@ +import 'package:flutter/widgets.dart'; +import 'app_localizations.dart'; + +export 'app_localizations.dart'; + +// l10n +extension AppLocalizationsX on BuildContext { + AppLocalizations get l10n => AppLocalizations.of(this); +} diff --git a/bdaya_new_flutter_app/hooks/template/lib/get_it_config.dart b/bdaya_new_flutter_app/hooks/template/lib/get_it_config.dart new file mode 100644 index 0000000..c8e9b85 --- /dev/null +++ b/bdaya_new_flutter_app/hooks/template/lib/get_it_config.dart @@ -0,0 +1,23 @@ +import 'common.dart'; +import 'get_it_config.config.dart'; + +const realEnv = Environment('real'); +const testsEnv = Environment('tests'); +const currentEnvironment = String.fromEnvironment('env'); +String? getItEnvironment; + +final getIt = GetIt.instance; + +@InjectableInit( + initializerName: r'init', // default + preferRelativeImports: false, // default + asExtension: true, // default + //to avoid warnings due to https://github.com/Milad-Akarie/injectable/issues/125 + ignoreUnregisteredTypesInPackages: ['my_app/common.dart'], + externalPackageModulesBefore: [ + ExternalModule(BdayaFlutterCommonPackageModule), + ], +) +void configureDependencies() => getIt.init( + environment: getItEnvironment ?? currentEnvironment, + ); diff --git a/bdaya_new_flutter_app/hooks/template/lib/injectable_module.dart b/bdaya_new_flutter_app/hooks/template/lib/injectable_module.dart new file mode 100644 index 0000000..a4f0004 --- /dev/null +++ b/bdaya_new_flutter_app/hooks/template/lib/injectable_module.dart @@ -0,0 +1,13 @@ +import 'package:go_router/go_router.dart'; + +import 'common.dart'; + +@module +abstract class RegisterModule { + @lazySingleton + GoRouter getRouter() => GoRouter( + routes: appRoutesList(), + initialLocation: AppRouteNames.initialRoute, + redirect: mainRedirect, + ); +} diff --git a/bdaya_new_flutter_app/hooks/template/lib/l10n/arb/app_ar.arb b/bdaya_new_flutter_app/hooks/template/lib/l10n/arb/app_ar.arb new file mode 100644 index 0000000..3453f89 --- /dev/null +++ b/bdaya_new_flutter_app/hooks/template/lib/l10n/arb/app_ar.arb @@ -0,0 +1,6 @@ +{ + "@@locale": "ar", + "unknown_error": "خطأ غير معروف", + "retry": "إعادة المحاولة", + "ok": "حسنا" +} diff --git a/bdaya_new_flutter_app/hooks/template/lib/l10n/arb/app_en.arb b/bdaya_new_flutter_app/hooks/template/lib/l10n/arb/app_en.arb new file mode 100644 index 0000000..c021cc2 --- /dev/null +++ b/bdaya_new_flutter_app/hooks/template/lib/l10n/arb/app_en.arb @@ -0,0 +1,6 @@ +{ + "@@locale": "en", + "unknown_error": "Unknown error", + "retry": "Retry", + "ok": "OK" +} diff --git a/bdaya_new_flutter_app/hooks/template/lib/mixins/_exports.dart b/bdaya_new_flutter_app/hooks/template/lib/mixins/_exports.dart new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/bdaya_new_flutter_app/hooks/template/lib/mixins/_exports.dart @@ -0,0 +1 @@ + diff --git a/bdaya_new_flutter_app/hooks/template/lib/models/_exports.dart b/bdaya_new_flutter_app/hooks/template/lib/models/_exports.dart new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/bdaya_new_flutter_app/hooks/template/lib/models/_exports.dart @@ -0,0 +1 @@ + diff --git a/bdaya_new_flutter_app/hooks/template/lib/pages/_exports.dart b/bdaya_new_flutter_app/hooks/template/lib/pages/_exports.dart new file mode 100644 index 0000000..996b340 --- /dev/null +++ b/bdaya_new_flutter_app/hooks/template/lib/pages/_exports.dart @@ -0,0 +1,3 @@ +export 'splash_screen.dart'; + +export 'app_shell/_exports.dart'; diff --git a/bdaya_new_flutter_app/hooks/template/lib/pages/app_shell/_exports.dart b/bdaya_new_flutter_app/hooks/template/lib/pages/app_shell/_exports.dart new file mode 100644 index 0000000..4329264 --- /dev/null +++ b/bdaya_new_flutter_app/hooks/template/lib/pages/app_shell/_exports.dart @@ -0,0 +1,4 @@ +export 'view/view.dart'; + +export 'dashboard_shell/_exports.dart'; +export 'public_shell/_exports.dart'; diff --git a/bdaya_new_flutter_app/hooks/template/lib/pages/app_shell/dashboard_shell/_exports.dart b/bdaya_new_flutter_app/hooks/template/lib/pages/app_shell/dashboard_shell/_exports.dart new file mode 100644 index 0000000..00ffcf9 --- /dev/null +++ b/bdaya_new_flutter_app/hooks/template/lib/pages/app_shell/dashboard_shell/_exports.dart @@ -0,0 +1 @@ +export 'view/view.dart'; diff --git a/bdaya_new_flutter_app/hooks/template/lib/pages/app_shell/dashboard_shell/view/controller.dart b/bdaya_new_flutter_app/hooks/template/lib/pages/app_shell/dashboard_shell/view/controller.dart new file mode 100644 index 0000000..98a2ee2 --- /dev/null +++ b/bdaya_new_flutter_app/hooks/template/lib/pages/app_shell/dashboard_shell/view/controller.dart @@ -0,0 +1,6 @@ +import '../../../../common.dart'; + +@lazySingleton +class DashboardShellController extends BdayaCombinedController { + DashboardShellController(/*add getIt dependencies here*/); +} diff --git a/bdaya_new_flutter_app/hooks/template/lib/pages/app_shell/dashboard_shell/view/view.dart b/bdaya_new_flutter_app/hooks/template/lib/pages/app_shell/dashboard_shell/view/view.dart new file mode 100644 index 0000000..86ec840 --- /dev/null +++ b/bdaya_new_flutter_app/hooks/template/lib/pages/app_shell/dashboard_shell/view/view.dart @@ -0,0 +1,40 @@ +import '../../../../common.dart'; +import 'controller.dart'; + +class DashboardShellView extends StatelessWidget { + const DashboardShellView({ + super.key, + required this.controller, + required this.child, + }); + + static Widget hooked({ + required Widget child, + BdayaGetItHookMode hookMode = BdayaGetItHookMode.lazySingleton, + String? instanceName, + Object? param1, + Object? param2, + List? keys, + }) { + return HookBuilder( + builder: (context) => DashboardShellView( + controller: useBdayaViewController( + hookMode: hookMode, + instanceName: instanceName, + keys: keys, + param1: param1, + param2: param2, + ), + child: child, + ), + ); + } + + final DashboardShellController controller; + final Widget child; + + @override + Widget build(BuildContext context) { + return child; + } +} diff --git a/bdaya_new_flutter_app/hooks/template/lib/pages/app_shell/public_shell/_exports.dart b/bdaya_new_flutter_app/hooks/template/lib/pages/app_shell/public_shell/_exports.dart new file mode 100644 index 0000000..bd30544 --- /dev/null +++ b/bdaya_new_flutter_app/hooks/template/lib/pages/app_shell/public_shell/_exports.dart @@ -0,0 +1,2 @@ +export 'view/view.dart'; +export 'counter/view.dart'; diff --git a/bdaya_new_flutter_app/hooks/template/lib/pages/app_shell/public_shell/counter/controller.dart b/bdaya_new_flutter_app/hooks/template/lib/pages/app_shell/public_shell/counter/controller.dart new file mode 100644 index 0000000..f46e33e --- /dev/null +++ b/bdaya_new_flutter_app/hooks/template/lib/pages/app_shell/public_shell/counter/controller.dart @@ -0,0 +1,12 @@ +import '../../../../common.dart'; + +@lazySingleton +class CounterController extends BdayaCombinedController { + CounterController(/*add getIt dependencies here*/); + + final counter = SharedValue(value: 0); + + void increment() => counter.$++; + + void decrement() => counter.$--; +} diff --git a/bdaya_new_flutter_app/hooks/template/lib/pages/app_shell/public_shell/counter/view.dart b/bdaya_new_flutter_app/hooks/template/lib/pages/app_shell/public_shell/counter/view.dart new file mode 100644 index 0000000..c4b5cad --- /dev/null +++ b/bdaya_new_flutter_app/hooks/template/lib/pages/app_shell/public_shell/counter/view.dart @@ -0,0 +1,66 @@ +import '../../../../common.dart'; + +import 'controller.dart'; + +class CounterView extends StatelessWidget { + const CounterView({ + super.key, + required this.controller, + }); + + static Widget hooked({ + BdayaGetItHookMode hookMode = BdayaGetItHookMode.lazySingleton, + String? instanceName, + Object? param1, + Object? param2, + List? keys, + }) { + return HookBuilder( + builder: (context) => CounterView( + controller: useBdayaViewController( + hookMode: hookMode, + instanceName: instanceName, + keys: keys, + param1: param1, + param2: param2, + ), + ), + ); + } + + final CounterController controller; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('Counter'), + centerTitle: true, + ), + body: Builder(builder: (context) { + final counter = controller.counter.of(context); + return Center( + child: Text('value: $counter'), + ); + }), + floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, + floatingActionButton: Row( + spacing: 12, + mainAxisSize: MainAxisSize.min, + children: [ + FloatingActionButton( + onPressed: controller.increment, + child: Icon(Icons.add), + ), + Builder(builder: (context) { + final counter = controller.counter.of(context); + return FloatingActionButton( + onPressed: counter == 0 ? null : controller.decrement, + child: Icon(Icons.remove), + ); + }), + ], + ), + ); + } +} diff --git a/bdaya_new_flutter_app/hooks/template/lib/pages/app_shell/public_shell/view/controller.dart b/bdaya_new_flutter_app/hooks/template/lib/pages/app_shell/public_shell/view/controller.dart new file mode 100644 index 0000000..4fe556d --- /dev/null +++ b/bdaya_new_flutter_app/hooks/template/lib/pages/app_shell/public_shell/view/controller.dart @@ -0,0 +1,6 @@ +import '../../../../common.dart'; + +@lazySingleton +class PublicShellController extends BdayaCombinedController { + PublicShellController(/*add getIt dependencies here*/); +} diff --git a/bdaya_new_flutter_app/hooks/template/lib/pages/app_shell/public_shell/view/view.dart b/bdaya_new_flutter_app/hooks/template/lib/pages/app_shell/public_shell/view/view.dart new file mode 100644 index 0000000..f6c33a3 --- /dev/null +++ b/bdaya_new_flutter_app/hooks/template/lib/pages/app_shell/public_shell/view/view.dart @@ -0,0 +1,41 @@ +import '../../../../common.dart'; + +import 'controller.dart'; + +class PublicShellView extends StatelessWidget { + const PublicShellView({ + super.key, + required this.controller, + required this.child, + }); + + static Widget hooked({ + required Widget child, + BdayaGetItHookMode hookMode = BdayaGetItHookMode.lazySingleton, + String? instanceName, + Object? param1, + Object? param2, + List? keys, + }) { + return HookBuilder( + builder: (context) => PublicShellView( + controller: useBdayaViewController( + hookMode: hookMode, + instanceName: instanceName, + keys: keys, + param1: param1, + param2: param2, + ), + child: child, + ), + ); + } + + final PublicShellController controller; + final Widget child; + + @override + Widget build(BuildContext context) { + return child; + } +} diff --git a/bdaya_new_flutter_app/hooks/template/lib/pages/app_shell/view/controller.dart b/bdaya_new_flutter_app/hooks/template/lib/pages/app_shell/view/controller.dart new file mode 100644 index 0000000..5463705 --- /dev/null +++ b/bdaya_new_flutter_app/hooks/template/lib/pages/app_shell/view/controller.dart @@ -0,0 +1,6 @@ +import '../../../common.dart'; + +@lazySingleton +class AppShellController extends BdayaCombinedController { + AppShellController(); +} diff --git a/bdaya_new_flutter_app/hooks/template/lib/pages/app_shell/view/view.dart b/bdaya_new_flutter_app/hooks/template/lib/pages/app_shell/view/view.dart new file mode 100644 index 0000000..2df9a7a --- /dev/null +++ b/bdaya_new_flutter_app/hooks/template/lib/pages/app_shell/view/view.dart @@ -0,0 +1,40 @@ +import '../../../common.dart'; +import 'controller.dart'; + +class AppShellView extends StatelessWidget { + const AppShellView({ + super.key, + required this.controller, + required this.child, + }); + + static Widget hooked({ + required Widget child, + BdayaGetItHookMode hookMode = BdayaGetItHookMode.lazySingleton, + String? instanceName, + Object? param1, + Object? param2, + List? keys, + }) { + return HookBuilder( + builder: (context) => AppShellView( + controller: useBdayaViewController( + hookMode: hookMode, + instanceName: instanceName, + keys: keys, + param1: param1, + param2: param2, + ), + child: child, + ), + ); + } + + final AppShellController controller; + final Widget child; + + @override + Widget build(BuildContext context) { + return child; + } +} diff --git a/bdaya_new_flutter_app/hooks/template/lib/pages/splash_screen.dart b/bdaya_new_flutter_app/hooks/template/lib/pages/splash_screen.dart new file mode 100644 index 0000000..26d9fdc --- /dev/null +++ b/bdaya_new_flutter_app/hooks/template/lib/pages/splash_screen.dart @@ -0,0 +1,97 @@ +import '../common.dart'; + +/// Splash screen should be theme/locale independent +class SplashScreen extends StatefulWidget { + const SplashScreen({super.key, this.child}); + final Widget? child; + + @override + State createState() => _SplashScreenState(); +} + +class _SplashScreenState extends State { + late Future _initializationFuture; + + @override + void initState() { + super.initState(); + _startInitialization(); + } + + void _startInitialization({bool retrying = false}) { + // Use get_it to retrieve the InitService instance + _initializationFuture = getIt().init( + context, + retrying: retrying, + ); + } + + void _retry() { + setState(() => _startInitialization(retrying: true)); // Reset the future + } + + @override + Widget build(BuildContext context) { + final actualChild = widget.child ?? const SizedBox.shrink(); + return FutureBuilder( + future: _initializationFuture, + builder: (context, snapshot) { + if (snapshot.hasError) { + return ErrorScreen( + snapshot: snapshot, + onRetry: _retry, + ); + } + if (snapshot.connectionState != ConnectionState.done) { + //if not done, show loading + return const Scaffold( + body: Center( + child: CircularProgressIndicator.adaptive(), + ), + ); + } + return actualChild; + }, + ); + } +} + +class ErrorScreen extends StatelessWidget { + final VoidCallback onRetry; + final AsyncSnapshot snapshot; + const ErrorScreen({ + super.key, + required this.onRetry, + required this.snapshot, + }); + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + return Scaffold( + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // const AppLogo(), + if (snapshot.connectionState != ConnectionState.waiting) + Text( + l10n.unknown_error, + style: TextStyle( + fontSize: 18, color: Theme.of(context).colorScheme.error), + ), + const SizedBox(height: 20), + + if (snapshot.connectionState == ConnectionState.waiting) + const CircularProgressIndicator.adaptive() + else + ElevatedButton( + onPressed: onRetry, + child: Text(l10n.retry), + ), + ], + ), + ), + ); + } +} diff --git a/bdaya_new_flutter_app/hooks/template/lib/routes.dart b/bdaya_new_flutter_app/hooks/template/lib/routes.dart new file mode 100644 index 0000000..b4583e7 --- /dev/null +++ b/bdaya_new_flutter_app/hooks/template/lib/routes.dart @@ -0,0 +1,100 @@ +import 'dart:async'; + +import 'package:go_router/go_router.dart'; + +import 'common.dart'; + +final appShellNavigatorKey = GlobalKey( + debugLabel: 'appShell', +); +final dashboardShellNavigatorKey = GlobalKey( + debugLabel: 'dashboardShell', +); +final publicShellNavigatorKey = GlobalKey( + debugLabel: 'publicShell', +); + +class AppRouteNames { + static const initialRoute = '/'; + + // static const kPublicHome = 'public_home'; + // static const kAuth = 'auth'; + // static const kSignIn = 'sign_in'; + // static const kSignUp = 'sign_up'; + // static const kSignUpVerification = 'sign_up_verification'; + // static const kForgetPassword = 'forget_password'; + // static const kResetPasswordVerification = 'reset_password_verification'; + // static const kNewPassword = 'new_password'; + static const kCounter = 'counter'; + // static const kDashboard = 'dashboard'; +} + +FutureOr mainRedirect(BuildContext context, GoRouterState state) { + // TODO: implement mainRedirect + return null; +} + +List appRoutesList() => [ + ShellRoute( + navigatorKey: appShellNavigatorKey, + builder: (context, state, child) => Scaffold( + body: AppShellView.hooked(child: child), + ), + routes: [ + // + GoRoute( + path: '/', + name: AppRouteNames.initialRoute, + redirect: (context, state) { + if (state.uri.path == '/') { + return state.namedLocation( + AppRouteNames.kCounter, + queryParameters: state.uri.queryParameters, + ); + } + return null; + }, + ), + //public pages + ShellRoute( + navigatorKey: publicShellNavigatorKey, + builder: (context, state, child) => + PublicShellView.hooked(child: child), + routes: [ + GoRoute( + path: '/${AppRouteNames.kCounter}', + name: AppRouteNames.kCounter, + builder: (context, state) => CounterView.hooked(), + ) + ], + ), + + // //auth page + // GoRoute( + // path: '/auth', + // name: AppRouteNames.kAuth, + // redirect: (context, state) async { + // // TODO: implement auth redirect + // throw UnimplementedError(); + // }, + // builder: (context, state) => throw UnimplementedError(), + // ), + + // // dashboard pages + // ShellRoute( + // navigatorKey: dashboardShellNavigatorKey, + // builder: (context, state, child) { + // return HookBuilder( + // builder: (context) => DashboardShellView( + // controller: useBdayaViewController(), + // child: child, + // ), + // ); + // }, + // routes: [ + // // + // ], + // ), + ], + ), + ]; diff --git a/bdaya_new_flutter_app/hooks/template/lib/services/_exports.dart b/bdaya_new_flutter_app/hooks/template/lib/services/_exports.dart new file mode 100644 index 0000000..e022cf2 --- /dev/null +++ b/bdaya_new_flutter_app/hooks/template/lib/services/_exports.dart @@ -0,0 +1,4 @@ +export 'init_service.dart'; +export 'app_config_service.dart'; +export 'localizations_service.dart'; +export 'grpc_service.dart'; diff --git a/bdaya_new_flutter_app/hooks/template/lib/services/app_config_service.dart b/bdaya_new_flutter_app/hooks/template/lib/services/app_config_service.dart new file mode 100644 index 0000000..c521d6b --- /dev/null +++ b/bdaya_new_flutter_app/hooks/template/lib/services/app_config_service.dart @@ -0,0 +1,54 @@ +import '../common.dart'; + +@lazySingleton +class AppConfigService { + final BdayaAppThemeServiceBase themeService; + AppConfigService(this.themeService); + + final logger = Logger('app-config-service'); + + Future init( + BuildContext context, Locale l, Brightness brightness) async { + logger.fine('Initializing App configs service...'); + // ensure locale is initialized! + if (themeService.locale.$ == null) { + await themeService.setLocale(const Locale('ar')); + await Future.delayed(const Duration(milliseconds: 300)); + if (context.mounted) { + getIt().setCurrentL10n(context.l10n); + } + logger.fine( + 'Locale initialization ensured, locale initial value: ${themeService.locale.$}.'); + } + // ensure theme mode is initialized! + if (themeService.themeMode.$ == null) { + // final sysThemeMode = brightness == Brightness.dark + // ? ThemeMode.dark + // : brightness == Brightness.light + // ? ThemeMode.light + // : ThemeMode.system; + themeService.setThemeMode(ThemeMode.light); + logger.fine( + 'Theme mode initialization ensured, theme mode initial value: ${themeService.themeMode.$}.'); + } + + // await Future.wait([ + // showOnboarding.load(), + // chooseLanguage.load(), + // ]); + // logger.fine( + // 'App Configs Service Initialization Done, show_onboarding: ${showOnboarding.$}, show_choose_lang: ${chooseLanguage.$}.'); // TODO replace value with dollar sign + } + + // final showOnboarding = SharedValue( + // key: 'show_onboarding', + // value: true, + // autosave: true, + // ); + + // final chooseLanguage = SharedValue( + // key: 'choose_language', + // value: true, + // autosave: true, + // ); +} diff --git a/bdaya_new_flutter_app/hooks/template/lib/services/grpc_service.dart b/bdaya_new_flutter_app/hooks/template/lib/services/grpc_service.dart new file mode 100644 index 0000000..fbc3d47 --- /dev/null +++ b/bdaya_new_flutter_app/hooks/template/lib/services/grpc_service.dart @@ -0,0 +1,121 @@ +import 'package:grpc/grpc_or_grpcweb.dart'; +import 'package:grpc/service_api.dart'; +import '../common.dart'; + +const kAcceptLanguageHeader = 'Accept-Language'; +const grpcServerUrl = String.fromEnvironment( + 'grpc-url', + defaultValue: 'default.server.com', +); + +const grpcServerPort = int.fromEnvironment('grpc-port', defaultValue: 443); + +const grpcServerTransportSecure = + bool.fromEnvironment('grpc-secure', defaultValue: true); + +class LoggingInterceptor extends ClientInterceptor { + final Logger _logger = Logger('LoggingInterceptor'); + + @override + ResponseFuture interceptUnary( + ClientMethod method, + Q request, + CallOptions options, + ClientUnaryInvoker invoker, + ) { + _logger.info('Request: ${method.path}, request: $request'); + final response = invoker(method, request, options); + response.headers.then((r) { + _logger.info('Response: $r'); + }); + + response.trailers.then((r) { + _logger.info('Response: $r'); + }); + return response; + } + + @override + ResponseStream interceptStreaming( + ClientMethod method, + Stream requests, + CallOptions options, + ClientStreamingInvoker invoker, + ) { + _logger.info('Request Stream: ${method.path}'); + requests.listen((request) { + _logger.info('Request: $request'); + }); + + final responseStream = invoker(method, requests, options); + responseStream.listen((response) { + _logger.info('Response: $response'); + }); + return responseStream; + } +} + +@realEnv +@lazySingleton +class GrpcService { + Iterable get defaultInterceptors => [ + MyClientInterceptor(), + LoggingInterceptor(), + ]; + + GrpcOrGrpcWebClientChannel get appDefaultChannel { + return GrpcOrGrpcWebClientChannel.toSingleEndpoint( + host: grpcServerUrl, + port: grpcServerPort, + transportSecure: grpcServerTransportSecure, + ); + } + + // InvoicesSummaryReportsServiceClient get invoicesSummaryReportsServiceClient => + // InvoicesSummaryReportsServiceClient( + // invoicesChannel, + // interceptors: { + // MyClientInterceptor(), + // LoggingInterceptor(), + // }, + // ); +} + +/// +/// response interceptor +class MyClientInterceptor extends ClientInterceptor { + CallOptions getNewOptions(CallOptions options) { + final l = getIt().locale.$; + // final authService = getIt(); + // final token = authService.getToken(); + // final tenantId = authService.tenantIdRx.$; + return options.mergedWith( + CallOptions( + metadata: { + if (l != null) kAcceptLanguageHeader: l.languageCode, + // if (token != null) kAuthorizationHeader: 'Bearer ' + token, + // if (token == null && tenantId != null) kTenantHeader: tenantId, + }, + ), + ); + } + + @override + ResponseFuture interceptUnary(ClientMethod method, Q request, + CallOptions options, ClientUnaryInvoker invoker) { + final newOptions = getNewOptions(options); + + return invoker(method, request, newOptions); + } + + @override + ResponseStream interceptStreaming( + ClientMethod method, + Stream requests, + CallOptions options, + ClientStreamingInvoker invoker) { + final newOptions = getNewOptions(options); + + return invoker(method, requests, newOptions); + } +} diff --git a/bdaya_new_flutter_app/hooks/template/lib/services/init_service.dart b/bdaya_new_flutter_app/hooks/template/lib/services/init_service.dart new file mode 100644 index 0000000..0e6c748 --- /dev/null +++ b/bdaya_new_flutter_app/hooks/template/lib/services/init_service.dart @@ -0,0 +1,53 @@ +import '../common.dart'; +import 'package:async/async.dart'; + +@lazySingleton +class InitService { + var initMemo = AsyncMemoizer(); + + InitService(); + + Future _reset() async { + initMemo = AsyncMemoizer(); + } + + Future init(BuildContext context, {bool retrying = false}) async { + if (retrying) { + await _reset(); + } + await initMemo.runOnce(() async { + await getIt().init(); + await Future.delayed(const Duration(milliseconds: 300)); + await Future.wait([ + //parallel init + + //delay for 3 seconds to get a chance to display animations,logo,etc... + Future.delayed(const Duration(seconds: 3)), + //actual init sequence` + if (context.mounted) _sequentialInit(context), + ]); + }); + } + + Future _sequentialInit(BuildContext context) async { + final l = Localizations.localeOf(context); + final brightness = MediaQuery.of(context).platformBrightness; + final l10n = context.l10n; + final localizationsService = getIt(); + localizationsService.setCurrentL10n(l10n); + + try { + if (context.mounted) { + await getIt().init(context, l, brightness); + } + // TODO: initialize other services + } catch (e, st) { + Logger("init-service").severe( + l10n.unknown_error, + e, + st, + ); + rethrow; + } + } +} diff --git a/bdaya_new_flutter_app/hooks/template/lib/services/localizations_service.dart b/bdaya_new_flutter_app/hooks/template/lib/services/localizations_service.dart new file mode 100644 index 0000000..800d546 --- /dev/null +++ b/bdaya_new_flutter_app/hooks/template/lib/services/localizations_service.dart @@ -0,0 +1,20 @@ +import '../common.dart'; + +@lazySingleton +class LocalizationsService { + AppLocalizations? _l10n; + + AppLocalizations get l10n { + assert(_l10n != null, + 'LocalizationsService not initialized. Call setCurrentL10n() first.'); + return _l10n!; + } + + final _logger = Logger('LocalizationsService'); + + void setCurrentL10n(AppLocalizations l10n) { + _l10n = l10n; + final localeName = l10n.localeName; + _logger.info('Current localization set to: $localeName'); + } +} diff --git a/bdaya_new_flutter_app/hooks/template/lib/utils/_exports.dart b/bdaya_new_flutter_app/hooks/template/lib/utils/_exports.dart new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/bdaya_new_flutter_app/hooks/template/lib/utils/_exports.dart @@ -0,0 +1 @@ + diff --git a/bdaya_new_flutter_app/hooks/template/lib/widgets/_exports.dart b/bdaya_new_flutter_app/hooks/template/lib/widgets/_exports.dart new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/bdaya_new_flutter_app/hooks/template/lib/widgets/_exports.dart @@ -0,0 +1 @@ + diff --git a/bdaya_new_flutter_app/hooks/test/pre_gen_test.dart b/bdaya_new_flutter_app/hooks/test/pre_gen_test.dart new file mode 100644 index 0000000..38fd73b --- /dev/null +++ b/bdaya_new_flutter_app/hooks/test/pre_gen_test.dart @@ -0,0 +1,52 @@ +import 'package:clock/clock.dart'; +import 'package:mason/mason.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:test/test.dart'; + +import '../post_gen.dart' as pre_gen; + +class _MockHookContext extends Mock implements HookContext {} + +void main() { + group('pre_gen', () { + late HookContext context; + + setUp(() { + context = _MockHookContext(); + }); + + test('populates variables', () { + withClock(Clock.fixed(DateTime(2020)), () { + final vars = { + 'project_name': 'my_app', + 'org_name': 'com.example', + 'application_id': 'app.id', + 'description': 'A new Flutter project.', + }; + when(() => context.vars).thenReturn(vars); + + pre_gen.run(context); + + final newVars = verify(() => context.vars = captureAny()).captured.last + as Map; + + expect( + newVars, + equals( + { + 'project_name': 'my_app', + 'org_name': 'com.example', + 'description': 'A new Flutter project.', + 'android_namespace': 'app.id', + 'android_application_id': 'app.id', + 'ios_application_id': 'app.id', + 'macos_application_id': 'app.id', + 'windows_application_id': 'app.id', + 'current_year': '2020', + }, + ), + ); + }); + }); + }); +} diff --git a/bdaya_new_flutter_app/hooks/test/src/models/android_application_id_test.dart b/bdaya_new_flutter_app/hooks/test/src/models/android_application_id_test.dart new file mode 100644 index 0000000..bc44b37 --- /dev/null +++ b/bdaya_new_flutter_app/hooks/test/src/models/android_application_id_test.dart @@ -0,0 +1,60 @@ +import 'package:test/test.dart'; +import 'package:bdaya_new_flutter_app/bdaya_new_flutter_app_hooks.dart'; + +void main() { + group('$AndroidApplicationId', () { + group('fallback', () { + test( + 'concatenates organization name with project name in snake case', + () { + const organizationName = 'com.example.hello-world'; + const projectName = 'my app'; + final androidApplicationId = AndroidApplicationId.fallback( + organizationName: organizationName, + projectName: projectName, + ); + expect(androidApplicationId.value, 'com.example.hello_world.my_app'); + }, + ); + + test( + 'ignores empty segments', + () { + const organizationName = 'com..example..hello-world'; + const projectName = 'my app'; + final androidApplicationId = AndroidApplicationId.fallback( + organizationName: organizationName, + projectName: projectName, + ); + expect(androidApplicationId.value, 'com.example.hello_world.my_app'); + }, + ); + }); + + group('isValid', () { + group('returns true', () { + test('when Android ID is valid', () { + final androidApplicationId = AndroidApplicationId('com.example.app'); + expect(androidApplicationId.isValid, isTrue); + }); + }); + + group('returns false', () { + test('when Android ID has less than two segments', () { + final androidApplicationId = AndroidApplicationId('com'); + expect(androidApplicationId.isValid, isFalse); + }); + + test('when Android ID has a segment that starts with a non-letter', () { + final androidApplicationId = AndroidApplicationId('1com.example.app'); + expect(androidApplicationId.isValid, isFalse); + }); + + test('when Android ID has a segment with a special character', () { + final androidApplicationId = AndroidApplicationId('com.example.app!'); + expect(androidApplicationId.isValid, isFalse); + }); + }); + }); + }); +} diff --git a/bdaya_new_flutter_app/hooks/test/src/models/apple_application_id_test.dart b/bdaya_new_flutter_app/hooks/test/src/models/apple_application_id_test.dart new file mode 100644 index 0000000..c9d590c --- /dev/null +++ b/bdaya_new_flutter_app/hooks/test/src/models/apple_application_id_test.dart @@ -0,0 +1,34 @@ +import 'package:test/test.dart'; +import 'package:bdaya_new_flutter_app/bdaya_new_flutter_app_hooks.dart'; + +void main() { + group('$AppleApplicationId', () { + group('fallback', () { + test( + 'concatenates organization name with project name in param case', + () { + const organizationName = 'com.example.hello-world'; + const projectName = 'my app'; + final appleApplicationId = AppleApplicationId.fallback( + organizationName: organizationName, + projectName: projectName, + ); + expect(appleApplicationId.value, 'com.example.hello-world.my-app'); + }, + ); + + test( + 'ignores empty parts', + () { + const organizationName = 'com.example.hello_world'; + const projectName = 'my app'; + final appleApplicationId = AppleApplicationId.fallback( + organizationName: organizationName, + projectName: projectName, + ); + expect(appleApplicationId.value, 'com.example.hello-world.my-app'); + }, + ); + }); + }); +} diff --git a/bdaya_new_flutter_app/hooks/test/src/models/very_good_core_configuration_test.dart b/bdaya_new_flutter_app/hooks/test/src/models/very_good_core_configuration_test.dart new file mode 100644 index 0000000..dae9fdd --- /dev/null +++ b/bdaya_new_flutter_app/hooks/test/src/models/very_good_core_configuration_test.dart @@ -0,0 +1,193 @@ +import 'package:test/test.dart'; +import 'package:bdaya_new_flutter_app/bdaya_new_flutter_app_hooks.dart'; + +void main() { + group('$BdayaNewFlutterAppConfiguration', () { + group('defaults', () { + test('project_name to "my_app"', () { + final configuration = BdayaNewFlutterAppConfiguration(); + expect(configuration.projectName, 'my_app'); + }); + + test('organization_name to "com.example"', () { + final configuration = BdayaNewFlutterAppConfiguration(); + expect(configuration.organizationName, 'com.example'); + }); + + test('description to "A Bdaya Flutter App"', () { + final configuration = BdayaNewFlutterAppConfiguration(); + expect(configuration.description, 'A Bdaya Flutter App'); + }); + + test('windowsApplicationId to "com.example.my-app"', () { + final configuration = BdayaNewFlutterAppConfiguration(); + expect(configuration.windowsApplicationId.value, 'com.example.my-app'); + }); + + test('iOsApplicationId to "com.example.my-app"', () { + final configuration = BdayaNewFlutterAppConfiguration(); + expect(configuration.iOsApplicationId.value, 'com.example.my-app'); + }); + + test('macOsApplicationId to "com.example.my-app"', () { + final configuration = BdayaNewFlutterAppConfiguration(); + expect(configuration.macOsApplicationId.value, 'com.example.my-app'); + }); + + test('androidApplicationId to "com.example.my_app"', () { + final configuration = BdayaNewFlutterAppConfiguration(); + expect(configuration.androidApplicationId.value, 'com.example.my_app'); + }); + }); + + group('throws', () { + group('a $InvalidAndroidApplicationIdFormat when Android ID', () { + test('has special characters', () { + expect( + () => BdayaNewFlutterAppConfiguration( + androidApplicationId: AndroidApplicationId('com.example.my_app!'), + ), + throwsA(isA()), + ); + }); + + test('parts start with numeric character', () { + expect( + () => BdayaNewFlutterAppConfiguration( + androidApplicationId: + AndroidApplicationId('1com.1example.1my_app'), + ), + throwsA(isA()), + ); + }); + + test('has a single part', () { + expect( + () => BdayaNewFlutterAppConfiguration( + androidApplicationId: AndroidApplicationId('com'), + ), + throwsA(isA()), + ); + }); + }); + }); + + group('fromHookVars', () { + test('decodes as expected', () { + final vars = { + 'project_name': 'bdaya flutter app', + 'org_name': 'com.bdaya', + 'application_id': 'com.bdaya.bdaya_app', + 'description': 'A Bdaya Flutter App', + }; + + final configuration = + BdayaNewFlutterAppConfiguration.fromHookVars(vars); + expect( + configuration, + equals( + BdayaNewFlutterAppConfiguration( + projectName: 'bdaya flutter app', + organizationName: 'com.bdaya', + description: 'A Bdaya Flutter App', + windowsApplicationId: WindowsApplicationId('com.bdaya.bdaya_app'), + iOsApplicationId: AppleApplicationId('com.bdaya.bdaya_app'), + macOsApplicationId: AppleApplicationId('com.bdaya.bdaya_app'), + androidApplicationId: AndroidApplicationId('com.bdaya.bdaya_app'), + androidNamespace: AndroidNamespace('com.bdaya.bdaya_app'), + ), + ), + ); + }); + + test('defaults id when empty', () { + final vars = { + 'project_name': 'bdaya flutter app', + 'org_name': 'com.bdaya', + 'application_id': '', + 'description': 'A Bdaya Flutter App', + }; + + final configuration = + BdayaNewFlutterAppConfiguration.fromHookVars(vars); + expect( + configuration, + equals( + BdayaNewFlutterAppConfiguration( + projectName: 'bdaya flutter app', + organizationName: 'com.bdaya', + description: 'A Bdaya Flutter App', + windowsApplicationId: WindowsApplicationId('com.bdaya.bdaya-app'), + iOsApplicationId: AppleApplicationId('com.bdaya.bdaya-app'), + macOsApplicationId: AppleApplicationId('com.bdaya.bdaya-app'), + androidApplicationId: AndroidApplicationId('com.bdaya.bdaya_app'), + androidNamespace: AndroidNamespace('com.bdaya.bdaya_app'), + ), + ), + ); + }); + + group('throws $ArgumentError', () { + test('when "project_name" is not a String?', () { + final vars = {'project_name': 42}; + + expect( + () => BdayaNewFlutterAppConfiguration.fromHookVars(vars), + throwsA( + isA().having( + (error) => error.message, + 'message', + '''Expected a value for key "project_name" to be of type String?, got 42.''', + ), + ), + ); + }); + + test('when "org_name" is not a String?', () { + final vars = {'org_name': 42}; + + expect( + () => BdayaNewFlutterAppConfiguration.fromHookVars(vars), + throwsA( + isA().having( + (error) => error.message, + 'message', + '''Expected a value for key "org_name" to be of type String?, got 42.''', + ), + ), + ); + }); + + test('when "application_id" is not a String?', () { + final vars = {'application_id': 42}; + + expect( + () => BdayaNewFlutterAppConfiguration.fromHookVars(vars), + throwsA( + isA().having( + (error) => error.message, + 'message', + '''Expected a value for key "application_id" to be of type String?, got 42.''', + ), + ), + ); + }); + + test('when "description" is not a String?', () { + final vars = {'description': 42}; + + expect( + () => BdayaNewFlutterAppConfiguration.fromHookVars(vars), + throwsA( + isA().having( + (error) => error.message, + 'message', + '''Expected a value for key "description" to be of type String?, got 42.''', + ), + ), + ); + }); + }); + }); + }); +} diff --git a/bdaya_new_flutter_app/hooks/test/src/models/windows_application_id_test.dart b/bdaya_new_flutter_app/hooks/test/src/models/windows_application_id_test.dart new file mode 100644 index 0000000..73fc1d5 --- /dev/null +++ b/bdaya_new_flutter_app/hooks/test/src/models/windows_application_id_test.dart @@ -0,0 +1,34 @@ +import 'package:test/test.dart'; +import 'package:bdaya_new_flutter_app/bdaya_new_flutter_app_hooks.dart'; + +void main() { + group('$WindowsApplicationId', () { + group('fallback', () { + test( + 'concatenates organization name with project name in param case', + () { + const organizationName = 'com.example.hello-world'; + const projectName = 'my app'; + final windowsApplicationId = WindowsApplicationId.fallback( + organizationName: organizationName, + projectName: projectName, + ); + expect(windowsApplicationId.value, 'com.example.hello-world.my-app'); + }, + ); + + test( + 'ignores empty parts', + () { + const organizationName = 'com.example.hello_world'; + const projectName = 'my app'; + final windowsApplicationId = WindowsApplicationId.fallback( + organizationName: organizationName, + projectName: projectName, + ); + expect(windowsApplicationId.value, 'com.example.hello-world.my-app'); + }, + ); + }); + }); +} diff --git a/test_app/pubspec.lock b/test_app/pubspec.lock index 66323d3..317e1a4 100644 --- a/test_app/pubspec.lock +++ b/test_app/pubspec.lock @@ -165,10 +165,10 @@ packages: dependency: transitive description: name: collection - sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" + sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf url: "https://pub.dev" source: hosted - version: "1.17.1" + version: "1.19.0" convert: dependency: transitive description: @@ -360,6 +360,30 @@ packages: url: "https://pub.dev" source: hosted version: "4.8.1" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" + url: "https://pub.dev" + source: hosted + version: "10.0.7" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" + url: "https://pub.dev" + source: hosted + version: "3.0.8" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" lints: dependency: transitive description: @@ -380,26 +404,26 @@ packages: dependency: transitive description: name: matcher - sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb url: "https://pub.dev" source: hosted - version: "0.12.15" + version: "0.12.16+1" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.15.0" mime: dependency: transitive description: @@ -420,10 +444,10 @@ packages: dependency: transitive description: name: path - sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" url: "https://pub.dev" source: hosted - version: "1.8.3" + version: "1.9.0" path_provider_linux: dependency: transitive description: @@ -612,7 +636,7 @@ packages: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" source_gen: dependency: transitive description: @@ -625,26 +649,26 @@ packages: dependency: transitive description: name: source_span - sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.10.0" stack_trace: dependency: transitive description: name: stack_trace - sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.12.0" stream_channel: dependency: transitive description: name: stream_channel - sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" stream_transform: dependency: transitive description: @@ -657,10 +681,10 @@ packages: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" term_glyph: dependency: transitive description: @@ -673,10 +697,10 @@ packages: dependency: transitive description: name: test_api - sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb + sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "0.7.3" timing: dependency: transitive description: @@ -717,6 +741,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b + url: "https://pub.dev" + source: hosted + version: "14.3.0" watcher: dependency: transitive description: @@ -758,5 +790,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.0.1 <4.0.0" - flutter: ">=3.7.0" + dart: ">=3.4.0 <4.0.0" + flutter: ">=3.18.0-18.0.pre.54"