├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ └── bug_report.md ├── CODEOWNERS ├── workflows │ ├── ci.yaml │ ├── flutterprint.yaml │ └── generate_flutterprint.yaml ├── dependabot.yaml └── PULL_REQUEST_TEMPLATE.md ├── analysis_options.yaml ├── src └── my_app │ ├── lib │ ├── widgets │ │ ├── widgets.dart │ │ ├── backend_env_banner.dart │ │ └── countdown_trigger.dart │ ├── services │ │ ├── rest_api_service │ │ │ ├── domain │ │ │ │ ├── token_type.dart │ │ │ │ ├── api_method.dart │ │ │ │ ├── api_status.dart │ │ │ │ └── api_failure.dart │ │ │ ├── rest_api_service.dart │ │ │ └── data │ │ │ │ └── rest_api_service.dart │ │ ├── local_storage │ │ │ ├── local_storage.dart │ │ │ └── data │ │ │ │ ├── fake_secure_storage_repository.dart │ │ │ │ └── shared_preference_repository.dart │ │ └── snackbar │ │ │ └── app_snackbar_repository.dart │ ├── features │ │ ├── start_up │ │ │ ├── start_up.dart │ │ │ ├── data │ │ │ │ └── start_up_repository.dart │ │ │ └── presentation │ │ │ │ └── splash_page.dart │ │ ├── backend_environment │ │ │ ├── backend_environment.dart │ │ │ ├── domain │ │ │ │ ├── endpoints.dart │ │ │ │ ├── backend_env.dart │ │ │ │ └── host.dart │ │ │ └── application │ │ │ │ └── host_controller.dart │ │ ├── authentication │ │ │ ├── authentication.dart │ │ │ ├── domain │ │ │ │ ├── auth_state.dart │ │ │ │ └── auth_info.dart │ │ │ └── data │ │ │ │ ├── fake_auth_repository.dart │ │ │ │ └── auth_repository.dart │ │ ├── theme_mode │ │ │ └── application │ │ │ │ └── theme_mode_controller.dart │ │ └── home │ │ │ └── presentation │ │ │ └── home_page.dart │ ├── utils │ │ ├── utils.dart │ │ └── validators │ │ │ ├── local_host_validator_mixin.dart │ │ │ ├── account_password_validators_mixin.dart │ │ │ └── string_validator.dart │ ├── constant │ │ ├── regex_pattern.dart │ │ ├── theme │ │ │ ├── theme.dart │ │ │ └── color_schemes.dart │ │ └── app_sizes.dart │ ├── l10n │ │ ├── l10n.dart │ │ └── arb │ │ │ ├── app_zh.arb │ │ │ └── app_en.arb │ └── routing │ │ └── not_found_page.dart │ ├── ios │ ├── Runner │ │ ├── Runner-Bridging-Header.h │ │ ├── Assets.xcassets │ │ │ ├── LaunchImage.imageset │ │ │ │ ├── LaunchImage.png │ │ │ │ ├── LaunchImage@2x.png │ │ │ │ ├── LaunchImage@3x.png │ │ │ │ ├── README.md │ │ │ │ └── Contents.json │ │ │ └── AppIcon.appiconset │ │ │ │ ├── Icon-App-20x20@1x.png │ │ │ │ ├── Icon-App-20x20@2x.png │ │ │ │ ├── Icon-App-20x20@3x.png │ │ │ │ ├── Icon-App-29x29@1x.png │ │ │ │ ├── Icon-App-29x29@2x.png │ │ │ │ ├── Icon-App-29x29@3x.png │ │ │ │ ├── Icon-App-40x40@1x.png │ │ │ │ ├── Icon-App-40x40@2x.png │ │ │ │ ├── Icon-App-40x40@3x.png │ │ │ │ ├── Icon-App-60x60@2x.png │ │ │ │ ├── Icon-App-60x60@3x.png │ │ │ │ ├── Icon-App-76x76@1x.png │ │ │ │ ├── Icon-App-76x76@2x.png │ │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ │ └── Icon-App-83.5x83.5@2x.png │ │ ├── AppDelegate.swift │ │ ├── Base.lproj │ │ │ ├── Main.storyboard │ │ │ └── LaunchScreen.storyboard │ │ └── Info.plist │ ├── Flutter │ │ ├── Debug.xcconfig │ │ ├── Release.xcconfig │ │ └── AppFrameworkInfo.plist │ ├── Runner.xcodeproj │ │ └── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ └── IDEWorkspaceChecks.plist │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ └── IDEWorkspaceChecks.plist │ ├── .gitignore │ └── Podfile │ ├── web │ ├── favicon.png │ ├── icons │ │ ├── Icon-192.png │ │ ├── Icon-512.png │ │ └── favicon.png │ └── manifest.json │ ├── android │ ├── gradle.properties │ ├── app │ │ └── src │ │ │ ├── main │ │ │ ├── res │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── drawable │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── drawable-v21 │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── values │ │ │ │ │ └── styles.xml │ │ │ │ └── values-night │ │ │ │ │ └── styles.xml │ │ │ ├── kotlin │ │ │ │ └── com │ │ │ │ │ └── example │ │ │ │ │ └── flutterprint │ │ │ │ │ └── MainActivity.kt │ │ │ └── AndroidManifest.xml │ │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ │ └── profile │ │ │ └── AndroidManifest.xml │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ ├── .gitignore │ ├── settings.gradle │ └── build.gradle │ ├── l10n.yaml │ ├── scripts │ ├── remove_gen_files_in_lcov.sh │ ├── test.sh │ └── import_files_coverage.sh │ ├── test │ ├── fake_model.dart │ ├── mocks.dart │ ├── utils │ │ └── validators │ │ │ └── string_validator_test.dart │ ├── robot.dart │ └── features │ │ ├── authentication │ │ ├── auth_robot.dart │ │ └── auth_flow_test.dart │ │ └── backend_environment │ │ └── backend_robot.dart │ ├── .github │ ├── dependabot.yaml │ ├── workflows │ │ └── main.yaml │ └── PULL_REQUEST_TEMPLATE.md │ ├── .vscode │ └── extensions.json │ ├── .idea │ └── runConfigurations │ │ └── main_dart.xml │ ├── .metadata │ ├── LICENSE │ ├── coverage_badge.svg │ └── analysis_options.yaml ├── brick ├── __brick__ │ └── {{project_name.snakeCase()}} │ │ ├── lib │ │ ├── widgets │ │ │ ├── widgets.dart │ │ │ ├── backend_env_banner.dart │ │ │ └── countdown_trigger.dart │ │ ├── services │ │ │ ├── rest_api_service │ │ │ │ ├── domain │ │ │ │ │ ├── token_type.dart │ │ │ │ │ ├── api_method.dart │ │ │ │ │ ├── api_status.dart │ │ │ │ │ └── api_failure.dart │ │ │ │ ├── rest_api_service.dart │ │ │ │ └── data │ │ │ │ │ └── rest_api_service.dart │ │ │ ├── local_storage │ │ │ │ ├── local_storage.dart │ │ │ │ └── data │ │ │ │ │ ├── fake_secure_storage_repository.dart │ │ │ │ │ └── shared_preference_repository.dart │ │ │ └── snackbar │ │ │ │ └── app_snackbar_repository.dart │ │ ├── features │ │ │ ├── start_up │ │ │ │ ├── start_up.dart │ │ │ │ ├── data │ │ │ │ │ └── start_up_repository.dart │ │ │ │ └── presentation │ │ │ │ │ └── splash_page.dart │ │ │ ├── backend_environment │ │ │ │ ├── backend_environment.dart │ │ │ │ ├── domain │ │ │ │ │ ├── endpoints.dart │ │ │ │ │ ├── backend_env.dart │ │ │ │ │ └── host.dart │ │ │ │ └── application │ │ │ │ │ └── host_controller.dart │ │ │ ├── authentication │ │ │ │ ├── authentication.dart │ │ │ │ ├── domain │ │ │ │ │ ├── auth_state.dart │ │ │ │ │ └── auth_info.dart │ │ │ │ └── data │ │ │ │ │ ├── fake_auth_repository.dart │ │ │ │ │ └── auth_repository.dart │ │ │ ├── theme_mode │ │ │ │ └── application │ │ │ │ │ └── theme_mode_controller.dart │ │ │ └── home │ │ │ │ └── presentation │ │ │ │ └── home_page.dart │ │ ├── utils │ │ │ ├── utils.dart │ │ │ └── validators │ │ │ │ ├── local_host_validator_mixin.dart │ │ │ │ ├── account_password_validators_mixin.dart │ │ │ │ └── string_validator.dart │ │ ├── constant │ │ │ ├── regex_pattern.dart │ │ │ ├── theme │ │ │ │ ├── theme.dart │ │ │ │ └── color_schemes.dart │ │ │ └── app_sizes.dart │ │ ├── l10n │ │ │ ├── l10n.dart │ │ │ └── arb │ │ │ │ ├── app_zh.arb │ │ │ │ └── app_en.arb │ │ └── routing │ │ │ └── not_found_page.dart │ │ ├── ios │ │ ├── Runner │ │ │ ├── Runner-Bridging-Header.h │ │ │ ├── Assets.xcassets │ │ │ │ ├── LaunchImage.imageset │ │ │ │ │ ├── LaunchImage.png │ │ │ │ │ ├── LaunchImage@2x.png │ │ │ │ │ ├── LaunchImage@3x.png │ │ │ │ │ ├── README.md │ │ │ │ │ └── Contents.json │ │ │ │ └── AppIcon.appiconset │ │ │ │ │ ├── Icon-App-20x20@1x.png │ │ │ │ │ ├── Icon-App-20x20@2x.png │ │ │ │ │ ├── Icon-App-20x20@3x.png │ │ │ │ │ ├── Icon-App-29x29@1x.png │ │ │ │ │ ├── Icon-App-29x29@2x.png │ │ │ │ │ ├── Icon-App-29x29@3x.png │ │ │ │ │ ├── Icon-App-40x40@1x.png │ │ │ │ │ ├── Icon-App-40x40@2x.png │ │ │ │ │ ├── Icon-App-40x40@3x.png │ │ │ │ │ ├── Icon-App-60x60@2x.png │ │ │ │ │ ├── Icon-App-60x60@3x.png │ │ │ │ │ ├── Icon-App-76x76@1x.png │ │ │ │ │ ├── Icon-App-76x76@2x.png │ │ │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ │ │ └── Icon-App-83.5x83.5@2x.png │ │ │ ├── AppDelegate.swift │ │ │ ├── Base.lproj │ │ │ │ ├── Main.storyboard │ │ │ │ └── LaunchScreen.storyboard │ │ │ └── Info.plist │ │ ├── Flutter │ │ │ ├── Debug.xcconfig │ │ │ ├── Release.xcconfig │ │ │ └── AppFrameworkInfo.plist │ │ ├── Runner.xcodeproj │ │ │ └── project.xcworkspace │ │ │ │ ├── contents.xcworkspacedata │ │ │ │ └── xcshareddata │ │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ │ └── IDEWorkspaceChecks.plist │ │ ├── Runner.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ │ └── IDEWorkspaceChecks.plist │ │ ├── .gitignore │ │ └── Podfile │ │ ├── android │ │ ├── gradle.properties │ │ ├── app │ │ │ └── src │ │ │ │ ├── main │ │ │ │ ├── res │ │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── drawable │ │ │ │ │ │ └── launch_background.xml │ │ │ │ │ ├── drawable-v21 │ │ │ │ │ │ └── launch_background.xml │ │ │ │ │ ├── values │ │ │ │ │ │ └── styles.xml │ │ │ │ │ └── values-night │ │ │ │ │ │ └── styles.xml │ │ │ │ ├── kotlin │ │ │ │ │ └── {{org_name.pathCase()}} │ │ │ │ │ │ └── MainActivity.kt │ │ │ │ └── AndroidManifest.xml │ │ │ │ ├── debug │ │ │ │ └── AndroidManifest.xml │ │ │ │ └── profile │ │ │ │ └── AndroidManifest.xml │ │ ├── gradle │ │ │ └── wrapper │ │ │ │ └── gradle-wrapper.properties │ │ ├── .gitignore │ │ ├── settings.gradle │ │ └── build.gradle │ │ ├── web │ │ ├── favicon.png │ │ ├── icons │ │ │ ├── favicon.png │ │ │ ├── Icon-192.png │ │ │ └── Icon-512.png │ │ └── manifest.json │ │ ├── l10n.yaml │ │ ├── scripts │ │ ├── remove_gen_files_in_lcov.sh │ │ ├── test.sh │ │ └── import_files_coverage.sh │ │ ├── test │ │ ├── fake_model.dart │ │ ├── mocks.dart │ │ ├── utils │ │ │ └── validators │ │ │ │ └── string_validator_test.dart │ │ ├── features │ │ │ ├── authentication │ │ │ │ ├── auth_robot.dart │ │ │ │ └── auth_flow_test.dart │ │ │ └── backend_environment │ │ │ │ └── backend_robot.dart │ │ └── robot.dart │ │ ├── .github │ │ ├── dependabot.yaml │ │ ├── workflows │ │ │ └── main.yaml │ │ └── PULL_REQUEST_TEMPLATE.md │ │ ├── .vscode │ │ └── extensions.json │ │ ├── .idea │ │ └── runConfigurations │ │ │ └── main_dart.xml │ │ ├── .metadata │ │ ├── coverage_badge.svg │ │ └── analysis_options.yaml ├── hooks │ ├── pubspec.yaml │ ├── pre_gen.dart │ └── post_gen.dart ├── CHANGELOG.md ├── brick.yaml └── LICENSE ├── images └── flutterprint_preview.gif ├── tool └── generator │ ├── static │ └── MainActivity.kt │ ├── pubspec.yaml │ └── .gitignore └── .gitignore /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | analyzer: 2 | exclude: [brick/__brick__/**] 3 | -------------------------------------------------------------------------------- /src/my_app/lib/widgets/widgets.dart: -------------------------------------------------------------------------------- 1 | export 'backend_env_banner.dart'; 2 | -------------------------------------------------------------------------------- /src/my_app/ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Every request must be reviewed and accepted by: 2 | 3 | * @ycchuang305 -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/lib/widgets/widgets.dart: -------------------------------------------------------------------------------- 1 | export 'backend_env_banner.dart'; 2 | -------------------------------------------------------------------------------- /src/my_app/web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/src/my_app/web/favicon.png -------------------------------------------------------------------------------- /src/my_app/lib/services/rest_api_service/domain/token_type.dart: -------------------------------------------------------------------------------- 1 | enum TokenType { 2 | none, 3 | account, 4 | } 5 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /images/flutterprint_preview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/images/flutterprint_preview.gif -------------------------------------------------------------------------------- /src/my_app/web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/src/my_app/web/icons/Icon-192.png -------------------------------------------------------------------------------- /src/my_app/web/icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/src/my_app/web/icons/Icon-512.png -------------------------------------------------------------------------------- /src/my_app/web/icons/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/src/my_app/web/icons/favicon.png -------------------------------------------------------------------------------- /src/my_app/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /src/my_app/lib/features/start_up/start_up.dart: -------------------------------------------------------------------------------- 1 | export 'data/start_up_repository.dart'; 2 | export 'presentation/splash_page.dart'; 3 | -------------------------------------------------------------------------------- /src/my_app/lib/utils/utils.dart: -------------------------------------------------------------------------------- 1 | export 'validators/account_password_validators_mixin.dart'; 2 | export 'validators/string_validator.dart'; 3 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/lib/services/rest_api_service/domain/token_type.dart: -------------------------------------------------------------------------------- 1 | enum TokenType { 2 | none, 3 | account, 4 | } 5 | -------------------------------------------------------------------------------- /src/my_app/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /brick/hooks/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutterprint_hooks 2 | 3 | environment: 4 | sdk: ">=2.17.0 <3.0.0" 5 | 6 | dependencies: 7 | mason: ^0.1.0-dev.40 8 | -------------------------------------------------------------------------------- /src/my_app/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /src/my_app/lib/constant/regex_pattern.dart: -------------------------------------------------------------------------------- 1 | class RegexPattern { 2 | static const ipAddressPort = r'^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}):(\d+)$'; 3 | } 4 | -------------------------------------------------------------------------------- /src/my_app/l10n.yaml: -------------------------------------------------------------------------------- 1 | arb-dir: lib/l10n/arb 2 | template-arb-file: app_en.arb 3 | output-localization-file: app_localizations.dart 4 | nullable-getter: false 5 | -------------------------------------------------------------------------------- /src/my_app/lib/services/local_storage/local_storage.dart: -------------------------------------------------------------------------------- 1 | export 'data/secure_storage_repository.dart'; 2 | export 'data/shared_preference_repository.dart'; 3 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/lib/features/start_up/start_up.dart: -------------------------------------------------------------------------------- 1 | export 'data/start_up_repository.dart'; 2 | export 'presentation/splash_page.dart'; 3 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/lib/utils/utils.dart: -------------------------------------------------------------------------------- 1 | export 'validators/account_password_validators_mixin.dart'; 2 | export 'validators/string_validator.dart'; 3 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/brick/__brick__/{{project_name.snakeCase()}}/web/favicon.png -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /src/my_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/src/my_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /src/my_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/src/my_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /src/my_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/src/my_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/lib/constant/regex_pattern.dart: -------------------------------------------------------------------------------- 1 | class RegexPattern { 2 | static const ipAddressPort = r'^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}):(\d+)$'; 3 | } 4 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/lib/services/local_storage/local_storage.dart: -------------------------------------------------------------------------------- 1 | export 'data/secure_storage_repository.dart'; 2 | export 'data/shared_preference_repository.dart'; 3 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/web/icons/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/brick/__brick__/{{project_name.snakeCase()}}/web/icons/favicon.png -------------------------------------------------------------------------------- /src/my_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/src/my_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /src/my_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/src/my_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/l10n.yaml: -------------------------------------------------------------------------------- 1 | arb-dir: lib/l10n/arb 2 | template-arb-file: app_en.arb 3 | output-localization-file: app_localizations.dart 4 | nullable-getter: false 5 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/brick/__brick__/{{project_name.snakeCase()}}/web/icons/Icon-192.png -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/web/icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/brick/__brick__/{{project_name.snakeCase()}}/web/icons/Icon-512.png -------------------------------------------------------------------------------- /src/my_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/src/my_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /src/my_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/src/my_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /src/my_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/src/my_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /src/my_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/src/my_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /src/my_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/src/my_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /src/my_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/src/my_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /src/my_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/src/my_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /src/my_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/src/my_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /src/my_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/src/my_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /src/my_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/src/my_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /src/my_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/src/my_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /src/my_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/src/my_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /src/my_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/src/my_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /src/my_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/src/my_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /src/my_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/src/my_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /src/my_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/src/my_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /src/my_app/scripts/remove_gen_files_in_lcov.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | lcov --remove coverage/lcov.info 'lib/*/*.freezed.dart' 'lib/*/*.g.dart' 'lib/*/*.config.g.dart' 'lib/*/*.part.dart' 'lib/*/*.gr.dart' -o coverage/lcov.info -------------------------------------------------------------------------------- /tool/generator/static/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package {{org_name.dotCase()}}.{{project_name.snakeCase()}} 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /src/my_app/lib/features/backend_environment/backend_environment.dart: -------------------------------------------------------------------------------- 1 | export 'application/host_controller.dart'; 2 | export 'domain/backend_env.dart'; 3 | export 'domain/endpoints.dart'; 4 | export 'domain/host.dart'; 5 | -------------------------------------------------------------------------------- /src/my_app/test/fake_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutterprint/features/authentication/authentication.dart'; 2 | 3 | class FakeModel { 4 | static const authInfo = AuthInfo( 5 | accessToken: 'foo', 6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /src/my_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/src/my_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /src/my_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/src/my_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .atom/ 3 | .idea/* 4 | .vscode/* 5 | 6 | # Files and directories created by pub 7 | .dart_tool/ 8 | .packages 9 | pubspec.lock 10 | 11 | # Conventional directory for build outputs 12 | build/ 13 | -------------------------------------------------------------------------------- /src/my_app/android/app/src/main/kotlin/com/example/flutterprint/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.flutterPrint 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /tool/generator/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: generator 2 | description: A template generator for Very Good CLI. 3 | publish_to: none 4 | 5 | environment: 6 | sdk: ">=2.12.0-0 <3.0.0" 7 | 8 | dependencies: 9 | path: ^1.8.0 10 | -------------------------------------------------------------------------------- /src/my_app/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build: 10 | uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/semantic_pull_request.yml@v1 11 | -------------------------------------------------------------------------------- /src/my_app/lib/constant/theme/theme.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | export 'color_schemes.dart'; 4 | 5 | extension ColorSchemesX on BuildContext { 6 | ColorScheme get colorScheme => Theme.of(this).colorScheme; 7 | } 8 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/scripts/remove_gen_files_in_lcov.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | lcov --remove coverage/lcov.info 'lib/*/*.freezed.dart' 'lib/*/*.g.dart' 'lib/*/*.config.g.dart' 'lib/*/*.part.dart' 'lib/*/*.gr.dart' -o coverage/lcov.info -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/brick/__brick__/{{project_name.snakeCase()}}/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/brick/__brick__/{{project_name.snakeCase()}}/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/brick/__brick__/{{project_name.snakeCase()}}/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/brick/__brick__/{{project_name.snakeCase()}}/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/lib/features/backend_environment/backend_environment.dart: -------------------------------------------------------------------------------- 1 | export 'application/host_controller.dart'; 2 | export 'domain/backend_env.dart'; 3 | export 'domain/endpoints.dart'; 4 | export 'domain/host.dart'; 5 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/brick/__brick__/{{project_name.snakeCase()}}/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /src/my_app/lib/features/authentication/authentication.dart: -------------------------------------------------------------------------------- 1 | export 'domain/auth_info.dart'; 2 | export 'domain/auth_state.dart'; 3 | export 'data/auth_repository.dart'; 4 | export 'application/auth_controller.dart'; 5 | export 'presentation/sign_in_page.dart'; 6 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/test/fake_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:{{project_name.snakeCase()}}/features/authentication/authentication.dart'; 2 | 3 | class FakeModel { 4 | static const authInfo = AuthInfo( 5 | accessToken: 'foo', 6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/lib/constant/theme/theme.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | export 'color_schemes.dart'; 4 | 5 | extension ColorSchemesX on BuildContext { 6 | ColorScheme get colorScheme => Theme.of(this).colorScheme; 7 | } 8 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /src/my_app/.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | - package-ecosystem: "pub" 8 | directory: "/" 9 | schedule: 10 | interval: "daily" 11 | -------------------------------------------------------------------------------- /src/my_app/lib/features/backend_environment/domain/endpoints.dart: -------------------------------------------------------------------------------- 1 | enum Endpoint { 2 | // TODO: Manage your api endpoints 3 | signIn('path/to/your/api/endpoint'), 4 | refreshToken('path/to/your/api/endpoint'); 5 | 6 | const Endpoint(this.path); 7 | final String path; 8 | } 9 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/android/app/src/main/kotlin/{{org_name.pathCase()}}/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package {{org_name.dotCase()}}.{{project_name.snakeCase()}} 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/lib/features/authentication/authentication.dart: -------------------------------------------------------------------------------- 1 | export 'domain/auth_info.dart'; 2 | export 'domain/auth_state.dart'; 3 | export 'data/auth_repository.dart'; 4 | export 'application/auth_controller.dart'; 5 | export 'presentation/sign_in_page.dart'; 6 | -------------------------------------------------------------------------------- /src/my_app/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip 7 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | - package-ecosystem: "pub" 8 | directory: "/" 9 | schedule: 10 | interval: "daily" 11 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/lib/features/backend_environment/domain/endpoints.dart: -------------------------------------------------------------------------------- 1 | enum Endpoint { 2 | // TODO: Manage your api endpoints 3 | signIn('path/to/your/api/endpoint'), 4 | refreshToken('path/to/your/api/endpoint'); 5 | 6 | const Endpoint(this.path); 7 | final String path; 8 | } 9 | -------------------------------------------------------------------------------- /src/my_app/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "dart-code.dart-code", 6 | "dart-code.flutter", 7 | "robert-brunhage.flutter-riverpod-snippets" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /src/my_app/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/my_app/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/my_app/lib/l10n/l10n.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 3 | 4 | export 'package:flutter_gen/gen_l10n/app_localizations.dart'; 5 | 6 | extension AppLocalizationsX on BuildContext { 7 | AppLocalizations get l10n => AppLocalizations.of(this); 8 | } 9 | -------------------------------------------------------------------------------- /src/my_app/.idea/runConfigurations/main_dart.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /src/my_app/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip 7 | -------------------------------------------------------------------------------- /src/my_app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /tool/generator/.gitignore: -------------------------------------------------------------------------------- 1 | # Files and directories created by pub 2 | .dart_tool/ 3 | .packages 4 | pubspec.lock 5 | 6 | # Conventional directory for build outputs 7 | build/ 8 | 9 | # Directory created by dartdoc 10 | doc/api/ 11 | 12 | # Temporary Files 13 | .tmp/ 14 | 15 | # Files generated during tests 16 | .test_coverage.dart 17 | coverage/ -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "dart-code.dart-code", 6 | "dart-code.flutter", 7 | "robert-brunhage.flutter-riverpod-snippets" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /src/my_app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/lib/l10n/l10n.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 3 | 4 | export 'package:flutter_gen/gen_l10n/app_localizations.dart'; 5 | 6 | extension AppLocalizationsX on BuildContext { 7 | AppLocalizations get l10n => AppLocalizations.of(this); 8 | } 9 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/.idea/runConfigurations/main_dart.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/my_app/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 9b2d32b605630f28625709ebd9d78ab3016b2bf6 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /src/my_app/android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 9b2d32b605630f28625709ebd9d78ab3016b2bf6 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /src/my_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /src/my_app/.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | name: flutterprint 2 | 3 | on: [pull_request, push] 4 | 5 | jobs: 6 | semantic-pull-request: 7 | uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/semantic_pull_request.yml@v1 8 | 9 | build: 10 | uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 11 | with: 12 | flutter_channel: stable 13 | flutter_version: 3.7.2 14 | min_coverage: 80 15 | -------------------------------------------------------------------------------- /src/my_app/lib/services/rest_api_service/domain/api_method.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'api_method.freezed.dart'; 4 | 5 | @freezed 6 | class ApiMethod with _$ApiMethod { 7 | const factory ApiMethod.get() = _Get; 8 | const factory ApiMethod.post() = _Post; 9 | const factory ApiMethod.put() = _Put; 10 | const factory ApiMethod.patch() = _Patch; 11 | const factory ApiMethod.delete() = _Delete; 12 | } 13 | -------------------------------------------------------------------------------- /src/my_app/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/my_app/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/lib/services/rest_api_service/domain/api_method.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'api_method.freezed.dart'; 4 | 5 | @freezed 6 | class ApiMethod with _$ApiMethod { 7 | const factory ApiMethod.get() = _Get; 8 | const factory ApiMethod.post() = _Post; 9 | const factory ApiMethod.put() = _Put; 10 | const factory ApiMethod.patch() = _Patch; 11 | const factory ApiMethod.delete() = _Delete; 12 | } 13 | -------------------------------------------------------------------------------- /src/my_app/ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/my_app/lib/services/rest_api_service/domain/api_status.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutterprint/services/rest_api_service/domain/api_failure.dart'; 2 | import 'package:freezed_annotation/freezed_annotation.dart'; 3 | 4 | part 'api_status.freezed.dart'; 5 | 6 | @freezed 7 | class ApiStatus with _$ApiStatus { 8 | const factory ApiStatus.loading() = _Loading; 9 | const factory ApiStatus.success() = _Success; 10 | const factory ApiStatus.failure(ApiFailure apiFailure) = _ApiFailure; 11 | } 12 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | - package-ecosystem: "pub" 8 | directory: "/brick/hooks" 9 | schedule: 10 | interval: "daily" 11 | - package-ecosystem: "pub" 12 | directory: "/tool/generator" 13 | schedule: 14 | interval: "daily" 15 | - package-ecosystem: "pub" 16 | directory: "/src/my_app" 17 | schedule: 18 | interval: "daily" 19 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | name: {{project_name.snakeCase()}} 2 | 3 | on: [pull_request, push] 4 | 5 | jobs: 6 | semantic-pull-request: 7 | uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/semantic_pull_request.yml@v1 8 | 9 | build: 10 | uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 11 | with: 12 | flutter_channel: stable 13 | flutter_version: 3.7.2 14 | min_coverage: 80 15 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/my_app/android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 4 | def properties = new Properties() 5 | 6 | assert localPropertiesFile.exists() 7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 8 | 9 | def flutterSdkPath = properties.getProperty("flutter.sdk") 10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 12 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/lib/services/rest_api_service/domain/api_status.dart: -------------------------------------------------------------------------------- 1 | import 'package:{{project_name.snakeCase()}}/services/rest_api_service/domain/api_failure.dart'; 2 | import 'package:freezed_annotation/freezed_annotation.dart'; 3 | 4 | part 'api_status.freezed.dart'; 5 | 6 | @freezed 7 | class ApiStatus with _$ApiStatus { 8 | const factory ApiStatus.loading() = _Loading; 9 | const factory ApiStatus.success() = _Success; 10 | const factory ApiStatus.failure(ApiFailure apiFailure) = _ApiFailure; 11 | } 12 | -------------------------------------------------------------------------------- /src/my_app/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/my_app/android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /src/my_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 4 | def properties = new Properties() 5 | 6 | assert localPropertiesFile.exists() 7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 8 | 9 | def flutterSdkPath = properties.getProperty("flutter.sdk") 10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 12 | -------------------------------------------------------------------------------- /src/my_app/scripts/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # create a helper file to make coverage work for all dart files except the generated files 3 | sh scripts/import_files_coverage.sh flutterprint 4 | 5 | # run tests and generate a coverage file (at /coverage/lcov.info) 6 | flutter test --coverage --test-randomize-ordering-seed random 7 | 8 | # remove all generated files in lcov.info 9 | sh scripts/remove_gen_files_in_lcov.sh 10 | 11 | # generate coverage info 12 | genhtml -o coverage coverage/lcov.info 13 | 14 | # open to see coverage info 15 | open coverage/index.html 16 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/scripts/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # create a helper file to make coverage work for all dart files except the generated files 3 | sh scripts/import_files_coverage.sh {{project_name.snakeCase()}} 4 | 5 | # run tests and generate a coverage file (at /coverage/lcov.info) 6 | flutter test --coverage --test-randomize-ordering-seed random 7 | 8 | # remove all generated files in lcov.info 9 | sh scripts/remove_gen_files_in_lcov.sh 10 | 11 | # generate coverage info 12 | genhtml -o coverage coverage/lcov.info 13 | 14 | # open to see coverage info 15 | open coverage/index.html 16 | -------------------------------------------------------------------------------- /src/my_app/lib/features/authentication/domain/auth_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutterprint/services/rest_api_service/domain/api_failure.dart'; 2 | import 'package:freezed_annotation/freezed_annotation.dart'; 3 | 4 | part 'auth_state.freezed.dart'; 5 | 6 | @freezed 7 | class AuthState with _$AuthState { 8 | const factory AuthState.authenticating() = _Authenticating; 9 | const factory AuthState.unauthenticated() = _Unauthenticated; 10 | const factory AuthState.unknown() = _Unknown; 11 | const factory AuthState.authenticated() = _Authenticated; 12 | const factory AuthState.failure(ApiFailure failure) = _Failure; 13 | } 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Create a report to help us improve 4 | title: "fix: " 5 | labels: bug 6 | --- 7 | 8 | **Description** 9 | A clear and concise description of what the bug is. 10 | 11 | **Steps To Reproduce** 12 | 13 | 1. Go to '...' 14 | 2. Click on '....' 15 | 3. Scroll down to '....' 16 | 4. See error 17 | 18 | **Expected Behavior** 19 | A clear and concise description of what you expected to happen. 20 | 21 | **Screenshots** 22 | If applicable, add screenshots to help explain your problem. 23 | 24 | **Additional Context** 25 | Add any other context about the problem here. 26 | -------------------------------------------------------------------------------- /brick/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.1.5 2 | 3 | - feat: upgrade to Flutter 3.7.2 4 | - fix: debug error log not caught 5 | 6 | # 0.1.4 7 | 8 | - feat: upgrade to Flutter 3.7.0 9 | 10 | # 0.1.3 11 | 12 | - chore(brick): upgrade to mason v0.1.0-dev.40 13 | - feat: run flutter pub get on template generated 14 | 15 | # 0.1.2 16 | 17 | - chore: remove git ignored files in brick 18 | 19 | # 0.1.1 20 | 21 | - docs: update readme 22 | 23 | # 0.1.0 24 | 25 | - feat: add startup feature 26 | - feat: upgrade to Flutter 3.3.10 27 | - refactor: move snackbar under services 28 | - refactor: rename router to routing 29 | 30 | # 0.0.1 31 | 32 | - feat: initial release 🎉 33 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/lib/features/authentication/domain/auth_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:{{project_name.snakeCase()}}/services/rest_api_service/domain/api_failure.dart'; 2 | import 'package:freezed_annotation/freezed_annotation.dart'; 3 | 4 | part 'auth_state.freezed.dart'; 5 | 6 | @freezed 7 | class AuthState with _$AuthState { 8 | const factory AuthState.authenticating() = _Authenticating; 9 | const factory AuthState.unauthenticated() = _Unauthenticated; 10 | const factory AuthState.unknown() = _Unknown; 11 | const factory AuthState.authenticated() = _Authenticated; 12 | const factory AuthState.failure(ApiFailure failure) = _Failure; 13 | } 14 | -------------------------------------------------------------------------------- /src/my_app/web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Flutterprint", 3 | "short_name": "Flutterprint", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "background_color": "#0175C2", 7 | "theme_color": "#0175C2", 8 | "description": "A new Flutter project.", 9 | "orientation": "portrait-primary", 10 | "prefer_related_applications": false, 11 | "icons": [{ 12 | "src": "icons/Icon-192.png", 13 | "sizes": "192x192", 14 | "type": "image/png" 15 | }, 16 | { 17 | "src": "icons/Icon-512.png", 18 | "sizes": "512x512", 19 | "type": "image/png" 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /src/my_app/ios/.gitignore: -------------------------------------------------------------------------------- 1 | *.mode1v3 2 | *.mode2v3 3 | *.moved-aside 4 | *.pbxuser 5 | *.perspectivev3 6 | **/*sync/ 7 | .sconsign.dblite 8 | .tags* 9 | **/.vagrant/ 10 | **/DerivedData/ 11 | Icon? 12 | **/Pods/ 13 | **/.symlinks/ 14 | profile 15 | xcuserdata 16 | **/.generated/ 17 | Flutter/App.framework 18 | Flutter/Flutter.framework 19 | Flutter/Flutter.podspec 20 | Flutter/Generated.xcconfig 21 | Flutter/app.flx 22 | Flutter/app.zip 23 | Flutter/flutter_assets/ 24 | Flutter/flutter_export_environment.sh 25 | ServiceDefinitions.json 26 | Runner/GeneratedPluginRegistrant.* 27 | 28 | # Exceptions to above rules. 29 | !default.mode1v3 30 | !default.mode2v3 31 | !default.pbxuser 32 | !default.perspectivev3 33 | -------------------------------------------------------------------------------- /src/my_app/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.6.10' 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:7.1.2' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | mavenCentral() 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | project.evaluationDependsOn(':app') 25 | } 26 | 27 | task clean(type: Delete) { 28 | delete rootProject.buildDir 29 | } 30 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/ios/.gitignore: -------------------------------------------------------------------------------- 1 | *.mode1v3 2 | *.mode2v3 3 | *.moved-aside 4 | *.pbxuser 5 | *.perspectivev3 6 | **/*sync/ 7 | .sconsign.dblite 8 | .tags* 9 | **/.vagrant/ 10 | **/DerivedData/ 11 | Icon? 12 | **/Pods/ 13 | **/.symlinks/ 14 | profile 15 | xcuserdata 16 | **/.generated/ 17 | Flutter/App.framework 18 | Flutter/Flutter.framework 19 | Flutter/Flutter.podspec 20 | Flutter/Generated.xcconfig 21 | Flutter/app.flx 22 | Flutter/app.zip 23 | Flutter/flutter_assets/ 24 | Flutter/flutter_export_environment.sh 25 | ServiceDefinitions.json 26 | Runner/GeneratedPluginRegistrant.* 27 | 28 | # Exceptions to above rules. 29 | !default.mode1v3 30 | !default.mode2v3 31 | !default.pbxuser 32 | !default.perspectivev3 33 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "{{project_name.titleCase()}}", 3 | "short_name": "{{project_name.titleCase()}}", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "background_color": "#0175C2", 7 | "theme_color": "#0175C2", 8 | "description": "{{{description}}}", 9 | "orientation": "portrait-primary", 10 | "prefer_related_applications": false, 11 | "icons": [{ 12 | "src": "icons/Icon-192.png", 13 | "sizes": "192x192", 14 | "type": "image/png" 15 | }, 16 | { 17 | "src": "icons/Icon-512.png", 18 | "sizes": "512x512", 19 | "type": "image/png" 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.6.10' 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:7.1.2' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | mavenCentral() 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | project.evaluationDependsOn(':app') 25 | } 26 | 27 | task clean(type: Delete) { 28 | delete rootProject.buildDir 29 | } 30 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 8 | 9 | ## Description 10 | 11 | 12 | 13 | ## Type of Change 14 | 15 | 16 | 17 | - [ ] ✨ New feature (non-breaking change which adds functionality) 18 | - [ ] 🛠️ Bug fix (non-breaking change which fixes an issue) 19 | - [ ] ❌ Breaking change (fix or feature that would cause existing functionality to change) 20 | - [ ] 🧹 Code refactor 21 | - [ ] ✅ Build configuration change 22 | - [ ] 📝 Documentation 23 | - [ ] 🗑️ Chore 24 | -------------------------------------------------------------------------------- /src/my_app/.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 8 | 9 | ## Description 10 | 11 | 12 | 13 | ## Type of Change 14 | 15 | 16 | 17 | - [ ] ✨ New feature (non-breaking change which adds functionality) 18 | - [ ] 🛠️ Bug fix (non-breaking change which fixes an issue) 19 | - [ ] ❌ Breaking change (fix or feature that would cause existing functionality to change) 20 | - [ ] 🧹 Code refactor 21 | - [ ] ✅ Build configuration change 22 | - [ ] 📝 Documentation 23 | - [ ] 🗑️ Chore 24 | -------------------------------------------------------------------------------- /.github/workflows/flutterprint.yaml: -------------------------------------------------------------------------------- 1 | name: flutterprint 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - ".github/workflows/flutterprint.yaml" 7 | - "src/my_app/lib/**" 8 | - "src/my_app/test/**" 9 | - "src/my_app/pubspec.yaml" 10 | - "tool/generator/**" 11 | push: 12 | branches: 13 | - main 14 | paths: 15 | - ".github/workflows/flutterprint.yaml" 16 | - "src/my_app/lib/**" 17 | - "src/my_app/test/**" 18 | - "src/my_app/pubspec.yaml" 19 | - "tool/generator/**" 20 | 21 | jobs: 22 | build: 23 | uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 24 | with: 25 | flutter_channel: stable 26 | flutter_version: 3.7.2 27 | min_coverage: 80 28 | working_directory: src/my_app 29 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 8 | 9 | ## Description 10 | 11 | 12 | 13 | ## Type of Change 14 | 15 | 16 | 17 | - [ ] ✨ New feature (non-breaking change which adds functionality) 18 | - [ ] 🛠️ Bug fix (non-breaking change which fixes an issue) 19 | - [ ] ❌ Breaking change (fix or feature that would cause existing functionality to change) 20 | - [ ] 🧹 Code refactor 21 | - [ ] ✅ Build configuration change 22 | - [ ] 📝 Documentation 23 | - [ ] 🗑️ Chore 24 | -------------------------------------------------------------------------------- /brick/hooks/pre_gen.dart: -------------------------------------------------------------------------------- 1 | import 'package:mason/mason.dart'; 2 | 3 | enum Platform { 4 | android, 5 | ios, 6 | } 7 | 8 | void run(HookContext context) { 9 | context.vars['application_id_android'] = 10 | _appId(context, platform: Platform.android); 11 | context.vars['application_id'] = _appId(context); 12 | } 13 | 14 | String _appId(HookContext context, {Platform? platform}) { 15 | final orgName = context.vars['org_name'] as String; 16 | final projectName = context.vars['project_name'] as String; 17 | 18 | var applicationId = context.vars['application_id'] as String?; 19 | applicationId = (applicationId?.isNotEmpty ?? false) 20 | ? applicationId 21 | : '''$orgName.${platform == Platform.android ? projectName.snakeCase : projectName.paramCase}'''; 22 | 23 | return applicationId!; 24 | } 25 | -------------------------------------------------------------------------------- /src/my_app/lib/services/local_storage/data/fake_secure_storage_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutterprint/features/authentication/domain/auth_info.dart'; 2 | import 'package:flutterprint/services/local_storage/data/secure_storage_repository.dart'; 3 | 4 | class FakeSecureStorageRepository implements SecureRepository { 5 | const FakeSecureStorageRepository(); 6 | 7 | @override 8 | Future deleteAll() => Future.value(null); 9 | 10 | @override 11 | Future deleteAuthInfo() => Future.value(null); 12 | 13 | @override 14 | Future getAuthInfo() => Future.value(const AuthInfo( 15 | accessToken: 'foo', 16 | )); 17 | 18 | @override 19 | Future saveAuthInfo(AuthInfo authInfo) => Future.value(null); 20 | 21 | @override 22 | Future validateSecureStorage() => Future.value(null); 23 | } 24 | -------------------------------------------------------------------------------- /src/my_app/lib/features/theme_mode/application/theme_mode_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart' show ThemeMode; 2 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 | import 'package:flutterprint/services/local_storage/data/shared_preference_repository.dart'; 4 | 5 | final themeModeControllerProvider = 6 | AutoDisposeNotifierProvider( 7 | ThemeModeController.new); 8 | 9 | class ThemeModeController extends AutoDisposeNotifier { 10 | @override 11 | ThemeMode build() => ref.read(sharedPreferenceRepoProvider).getThemeMode(); 12 | 13 | Future toggleAndSaveThemeMode() async { 14 | final themeMode = 15 | state == ThemeMode.light ? ThemeMode.dark : ThemeMode.light; 16 | state = themeMode; 17 | return await ref.read(sharedPreferenceRepoProvider).setThemeMode(themeMode); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/my_app/lib/utils/validators/local_host_validator_mixin.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutterprint/constant/regex_pattern.dart'; 2 | import 'package:flutterprint/l10n/l10n.dart'; 3 | import 'package:flutterprint/utils/utils.dart'; 4 | 5 | mixin LocalHostValidatorMixin { 6 | // Use a regular expression to check if the input string follows the pattern 7 | // of an IP address (four groups of digits separated by periods) followed by a 8 | // colon and a port number (one or more digits) 9 | final localHostVaidator = StringValidator.regex(RegexPattern.ipAddressPort); 10 | 11 | bool canSubmitLocalHost(String localHost) => 12 | localHostVaidator.isValid(localHost); 13 | 14 | String? localHostErrorText(String account, AppLocalizations l10n) { 15 | final bool showErrorText = !canSubmitLocalHost(account); 16 | return showErrorText ? l10n.incorrectInputFormat : null; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/lib/services/local_storage/data/fake_secure_storage_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:{{project_name.snakeCase()}}/features/authentication/domain/auth_info.dart'; 2 | import 'package:{{project_name.snakeCase()}}/services/local_storage/data/secure_storage_repository.dart'; 3 | 4 | class FakeSecureStorageRepository implements SecureRepository { 5 | const FakeSecureStorageRepository(); 6 | 7 | @override 8 | Future deleteAll() => Future.value(null); 9 | 10 | @override 11 | Future deleteAuthInfo() => Future.value(null); 12 | 13 | @override 14 | Future getAuthInfo() => Future.value(const AuthInfo( 15 | accessToken: 'foo', 16 | )); 17 | 18 | @override 19 | Future saveAuthInfo(AuthInfo authInfo) => Future.value(null); 20 | 21 | @override 22 | Future validateSecureStorage() => Future.value(null); 23 | } 24 | -------------------------------------------------------------------------------- /src/my_app/test/mocks.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_secure_storage/flutter_secure_storage.dart'; 2 | import 'package:http/http.dart' as http; 3 | import 'package:flutterprint/features/authentication/authentication.dart'; 4 | import 'package:flutterprint/services/local_storage/data/secure_storage_repository.dart'; 5 | import 'package:mocktail/mocktail.dart'; 6 | 7 | // A generic Listener class, used to keep track of when a provider notifies its listeners 8 | class Listener extends Mock { 9 | void call(T? previous, T next); 10 | } 11 | 12 | class MockAuthRepository extends Mock implements AuthRepository {} 13 | 14 | class MockSecureRepository extends Mock implements SecureRepository {} 15 | 16 | class MockFlutterSecureStorage extends Mock implements FlutterSecureStorage {} 17 | 18 | class MockHttpClient extends Mock implements http.Client {} 19 | 20 | class MockResponse extends Mock implements http.Response {} 21 | -------------------------------------------------------------------------------- /src/my_app/lib/features/backend_environment/domain/backend_env.dart: -------------------------------------------------------------------------------- 1 | /// [hostKey] is a key used to stored in shared preference 2 | /// [baseUrl] is the base url/domain name of your server 3 | /// [appBanner] is a label that indicates the current backend environment 4 | enum BackendEnv { 5 | // TODO: Manage your backend environments configurations 6 | release( 7 | hostKey: '@release', 8 | baseUrl: 'release.example.com', 9 | appBanner: '', 10 | ), 11 | demo( 12 | hostKey: '@demo', 13 | baseUrl: 'demo.example.com', 14 | appBanner: 'demo', 15 | ), 16 | develop( 17 | hostKey: '@develop', 18 | baseUrl: 'develop.example.com', 19 | appBanner: 'develop', 20 | ); 21 | 22 | final String hostKey; 23 | final String baseUrl; 24 | final String appBanner; 25 | const BackendEnv({ 26 | required this.hostKey, 27 | required this.baseUrl, 28 | required this.appBanner, 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/lib/features/theme_mode/application/theme_mode_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart' show ThemeMode; 2 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 | import 'package:{{project_name.snakeCase()}}/services/local_storage/data/shared_preference_repository.dart'; 4 | 5 | final themeModeControllerProvider = 6 | AutoDisposeNotifierProvider( 7 | ThemeModeController.new); 8 | 9 | class ThemeModeController extends AutoDisposeNotifier { 10 | @override 11 | ThemeMode build() => ref.read(sharedPreferenceRepoProvider).getThemeMode(); 12 | 13 | Future toggleAndSaveThemeMode() async { 14 | final themeMode = 15 | state == ThemeMode.light ? ThemeMode.dark : ThemeMode.light; 16 | state = themeMode; 17 | return await ref.read(sharedPreferenceRepoProvider).setThemeMode(themeMode); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/my_app/ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 11.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/my_app/lib/widgets/backend_env_banner.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 | import 'package:flutterprint/features/backend_environment/application/host_controller.dart'; 4 | 5 | /// Show a debug banner that indicates the current backend environment 6 | class BackendEnvBanner extends ConsumerWidget { 7 | final Widget child; 8 | const BackendEnvBanner({ 9 | super.key, 10 | required this.child, 11 | }); 12 | 13 | @override 14 | Widget build(BuildContext context, WidgetRef ref) { 15 | final bannerName = 16 | ref.watch(hostControllerProvider.select((state) => state.bannerName)); 17 | return bannerName.isEmpty 18 | ? child 19 | : Banner( 20 | key: Key(bannerName), 21 | location: BannerLocation.bottomEnd, 22 | message: bannerName, 23 | child: child, 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/lib/features/backend_environment/domain/backend_env.dart: -------------------------------------------------------------------------------- 1 | /// [hostKey] is a key used to stored in shared preference 2 | /// [baseUrl] is the base url/domain name of your server 3 | /// [appBanner] is a label that indicates the current backend environment 4 | enum BackendEnv { 5 | // TODO: Manage your backend environments configurations 6 | release( 7 | hostKey: '@release', 8 | baseUrl: 'release.example.com', 9 | appBanner: '', 10 | ), 11 | demo( 12 | hostKey: '@demo', 13 | baseUrl: 'demo.example.com', 14 | appBanner: 'demo', 15 | ), 16 | develop( 17 | hostKey: '@develop', 18 | baseUrl: 'develop.example.com', 19 | appBanner: 'develop', 20 | ); 21 | 22 | final String hostKey; 23 | final String baseUrl; 24 | final String appBanner; 25 | const BackendEnv({ 26 | required this.hostKey, 27 | required this.baseUrl, 28 | required this.appBanner, 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/lib/utils/validators/local_host_validator_mixin.dart: -------------------------------------------------------------------------------- 1 | import 'package:{{project_name.snakeCase()}}/constant/regex_pattern.dart'; 2 | import 'package:{{project_name.snakeCase()}}/l10n/l10n.dart'; 3 | import 'package:{{project_name.snakeCase()}}/utils/utils.dart'; 4 | 5 | mixin LocalHostValidatorMixin { 6 | // Use a regular expression to check if the input string follows the pattern 7 | // of an IP address (four groups of digits separated by periods) followed by a 8 | // colon and a port number (one or more digits) 9 | final localHostVaidator = StringValidator.regex(RegexPattern.ipAddressPort); 10 | 11 | bool canSubmitLocalHost(String localHost) => 12 | localHostVaidator.isValid(localHost); 13 | 14 | String? localHostErrorText(String account, AppLocalizations l10n) { 15 | final bool showErrorText = !canSubmitLocalHost(account); 16 | return showErrorText ? l10n.incorrectInputFormat : null; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 11.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/test/mocks.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_secure_storage/flutter_secure_storage.dart'; 2 | import 'package:http/http.dart' as http; 3 | import 'package:{{project_name.snakeCase()}}/features/authentication/authentication.dart'; 4 | import 'package:{{project_name.snakeCase()}}/services/local_storage/data/secure_storage_repository.dart'; 5 | import 'package:mocktail/mocktail.dart'; 6 | 7 | // A generic Listener class, used to keep track of when a provider notifies its listeners 8 | class Listener extends Mock { 9 | void call(T? previous, T next); 10 | } 11 | 12 | class MockAuthRepository extends Mock implements AuthRepository {} 13 | 14 | class MockSecureRepository extends Mock implements SecureRepository {} 15 | 16 | class MockFlutterSecureStorage extends Mock implements FlutterSecureStorage {} 17 | 18 | class MockHttpClient extends Mock implements http.Client {} 19 | 20 | class MockResponse extends Mock implements http.Response {} 21 | -------------------------------------------------------------------------------- /src/my_app/lib/services/rest_api_service/domain/api_failure.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'api_failure.freezed.dart'; 4 | 5 | @freezed 6 | class ApiFailure with _$ApiFailure { 7 | const ApiFailure._(); 8 | const factory ApiFailure.apiAgreementConflict() = _ApiAgreementConflict; 9 | const factory ApiFailure.badRequest({String? message}) = _BadRequest; 10 | const factory ApiFailure.forbidden({String? message}) = _Forbidden; 11 | const factory ApiFailure.socket({String? message}) = _Socket; 12 | const factory ApiFailure.notFound({String? message}) = _NotFound; 13 | const factory ApiFailure.serviceUnavailable({String? message}) = 14 | _ServiceUnavailable; 15 | const factory ApiFailure.timeout({String? message}) = _Timeout; 16 | const factory ApiFailure.unauthorized({String? message}) = _Unauthorized; 17 | const factory ApiFailure.undefined( 18 | {required int statusCode, String? message}) = _Undefined; 19 | } 20 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/lib/widgets/backend_env_banner.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 | import 'package:{{project_name.snakeCase()}}/features/backend_environment/application/host_controller.dart'; 4 | 5 | /// Show a debug banner that indicates the current backend environment 6 | class BackendEnvBanner extends ConsumerWidget { 7 | final Widget child; 8 | const BackendEnvBanner({ 9 | super.key, 10 | required this.child, 11 | }); 12 | 13 | @override 14 | Widget build(BuildContext context, WidgetRef ref) { 15 | final bannerName = 16 | ref.watch(hostControllerProvider.select((state) => state.bannerName)); 17 | return bannerName.isEmpty 18 | ? child 19 | : Banner( 20 | key: Key(bannerName), 21 | location: BannerLocation.bottomEnd, 22 | message: bannerName, 23 | child: child, 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/my_app/scripts/import_files_coverage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | file=test/coverage_helper_test.dart 3 | printf "// Helper file to make coverage work for all dart files\n" > $file 4 | printf "// **************************************************************************\n" >> $file 5 | printf "// Because of this: https://github.com/flutter/flutter/issues/27997#issue-410722816\n" >> $file 6 | printf "// DO NOT EDIT THIS FILE USE: sh scripts/import_files_coverage.sh flutterprint\n" >> $file 7 | printf "// **************************************************************************\n" >> $file 8 | printf "\n" >> $file 9 | printf "// ignore_for_file: unused_import\n" >> $file 10 | find lib -type f \( -iname "*.dart" ! -iname '*_event.dart' ! -iname '*_state.dart' ! -iname "*.g.dart" ! -iname "*.config.g.dart" ! -iname "*.gr.dart" ! -iname "*.freezed.dart" ! -iname "generated_plugin_registrant.dart" \) | cut -c4- | awk -v package="$1" '{printf "import '\''package:%s%s'\'';\n", package, $1}' >> $file 11 | printf "\nvoid main(){}" >> $file -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/lib/services/rest_api_service/domain/api_failure.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'api_failure.freezed.dart'; 4 | 5 | @freezed 6 | class ApiFailure with _$ApiFailure { 7 | const ApiFailure._(); 8 | const factory ApiFailure.apiAgreementConflict() = _ApiAgreementConflict; 9 | const factory ApiFailure.badRequest({String? message}) = _BadRequest; 10 | const factory ApiFailure.forbidden({String? message}) = _Forbidden; 11 | const factory ApiFailure.socket({String? message}) = _Socket; 12 | const factory ApiFailure.notFound({String? message}) = _NotFound; 13 | const factory ApiFailure.serviceUnavailable({String? message}) = 14 | _ServiceUnavailable; 15 | const factory ApiFailure.timeout({String? message}) = _Timeout; 16 | const factory ApiFailure.unauthorized({String? message}) = _Unauthorized; 17 | const factory ApiFailure.undefined( 18 | {required int statusCode, String? message}) = _Undefined; 19 | } 20 | -------------------------------------------------------------------------------- /src/my_app/lib/utils/validators/account_password_validators_mixin.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutterprint/utils/validators/string_validator.dart'; 2 | import 'package:flutterprint/l10n/l10n.dart'; 3 | 4 | /// Mixin class to be used for client-side account & password validation 5 | mixin AccountAndPasswordValidatorsMixin { 6 | final nonEmptyValidator = StringValidator.nonEmpty(); 7 | 8 | bool canSubmitAccount(String account) { 9 | return nonEmptyValidator.isValid(account); 10 | } 11 | 12 | bool canSubmitPassword(String password) { 13 | return nonEmptyValidator.isValid(password); 14 | } 15 | 16 | String? accountErrorText(String account, AppLocalizations l10n) { 17 | final bool showErrorText = !canSubmitAccount(account); 18 | return showErrorText ? l10n.accountCanNotBeEmpty : null; 19 | } 20 | 21 | String? passwordErrorText(String password, AppLocalizations l10n) { 22 | final bool showErrorText = !canSubmitPassword(password); 23 | return showErrorText ? l10n.passwordCanNotBeEmpty : null; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /brick/brick.yaml: -------------------------------------------------------------------------------- 1 | name: flutterprint 2 | description: A starter app for building maintainable & scalable Flutter apps, streamlining development with commonly used features. 3 | repository: https://github.com/ycchuang305/flutterprint 4 | version: 0.1.5 5 | 6 | environment: 7 | mason: ">=0.1.0-dev.40 <0.1.0" 8 | 9 | vars: 10 | project_name: 11 | type: string 12 | description: The project name 13 | default: my_app 14 | prompt: What is the project name? 15 | org_name: 16 | type: string 17 | description: The organization name 18 | default: com.example 19 | prompt: What is the organization name? 20 | application_id: 21 | type: string 22 | 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. 23 | prompt: What is the application id? 24 | description: 25 | type: string 26 | description: A short project description 27 | default: A Flutter blueprint 28 | prompt: What is the project description? 29 | -------------------------------------------------------------------------------- /src/my_app/lib/features/authentication/data/fake_auth_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutterprint/features/authentication/authentication.dart'; 2 | import 'package:flutterprint/services/rest_api_service/rest_api_service.dart'; 3 | 4 | class FakeAuthRepository implements AuthRepository { 5 | const FakeAuthRepository({this.addDelay = true}); 6 | final bool addDelay; 7 | final fakeAuthInfo = const AuthInfo( 8 | accessToken: 'foo', 9 | ); 10 | 11 | @override 12 | Future> refreshToken() async { 13 | if (addDelay) { 14 | await Future.delayed(const Duration(seconds: 1)); 15 | } 16 | return Right(fakeAuthInfo); 17 | } 18 | 19 | @override 20 | Future> signIn({ 21 | required String username, 22 | required String password, 23 | }) async { 24 | if (addDelay) { 25 | await Future.delayed(const Duration(seconds: 1)); 26 | } 27 | return Right(fakeAuthInfo); 28 | // return left(const ApiFailure.forbidden()); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/scripts/import_files_coverage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | file=test/coverage_helper_test.dart 3 | printf "// Helper file to make coverage work for all dart files\n" > $file 4 | printf "// **************************************************************************\n" >> $file 5 | printf "// Because of this: https://github.com/flutter/flutter/issues/27997#issue-410722816\n" >> $file 6 | printf "// DO NOT EDIT THIS FILE USE: sh scripts/import_files_coverage.sh {{project_name.snakeCase()}}\n" >> $file 7 | printf "// **************************************************************************\n" >> $file 8 | printf "\n" >> $file 9 | printf "// ignore_for_file: unused_import\n" >> $file 10 | find lib -type f \( -iname "*.dart" ! -iname '*_event.dart' ! -iname '*_state.dart' ! -iname "*.g.dart" ! -iname "*.config.g.dart" ! -iname "*.gr.dart" ! -iname "*.freezed.dart" ! -iname "generated_plugin_registrant.dart" \) | cut -c4- | awk -v package="$1" '{printf "import '\''package:%s%s'\'';\n", package, $1}' >> $file 11 | printf "\nvoid main(){}" >> $file -------------------------------------------------------------------------------- /src/my_app/lib/features/start_up/data/start_up_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 2 | import 'package:flutterprint/features/authentication/authentication.dart'; 3 | import 'package:flutterprint/features/backend_environment/backend_environment.dart'; 4 | import 'package:flutterprint/services/local_storage/local_storage.dart'; 5 | 6 | final startUpRepositoryProvider = 7 | Provider.autoDispose((ref) => StartUpRepository(ref)); 8 | 9 | class StartUpRepository { 10 | const StartUpRepository(this.ref); 11 | final Ref ref; 12 | 13 | Future prepareAuthentication() async { 14 | // Considering add more setup method here only if it has to be done before authenticating 15 | 16 | // Validate secure storage 17 | await ref.read(secureRepositoryProvider).validateSecureStorage(); 18 | 19 | // Setup backend host environment 20 | ref.read(hostControllerProvider.notifier).setUpHost(); 21 | 22 | // Start authentication 23 | ref.read(authControllerProvider.notifier).authentication(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/lib/utils/validators/account_password_validators_mixin.dart: -------------------------------------------------------------------------------- 1 | import 'package:{{project_name.snakeCase()}}/utils/validators/string_validator.dart'; 2 | import 'package:{{project_name.snakeCase()}}/l10n/l10n.dart'; 3 | 4 | /// Mixin class to be used for client-side account & password validation 5 | mixin AccountAndPasswordValidatorsMixin { 6 | final nonEmptyValidator = StringValidator.nonEmpty(); 7 | 8 | bool canSubmitAccount(String account) { 9 | return nonEmptyValidator.isValid(account); 10 | } 11 | 12 | bool canSubmitPassword(String password) { 13 | return nonEmptyValidator.isValid(password); 14 | } 15 | 16 | String? accountErrorText(String account, AppLocalizations l10n) { 17 | final bool showErrorText = !canSubmitAccount(account); 18 | return showErrorText ? l10n.accountCanNotBeEmpty : null; 19 | } 20 | 21 | String? passwordErrorText(String password, AppLocalizations l10n) { 22 | final bool showErrorText = !canSubmitPassword(password); 23 | return showErrorText ? l10n.passwordCanNotBeEmpty : null; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/my_app/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/lib/features/authentication/data/fake_auth_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:{{project_name.snakeCase()}}/features/authentication/authentication.dart'; 2 | import 'package:{{project_name.snakeCase()}}/services/rest_api_service/rest_api_service.dart'; 3 | 4 | class FakeAuthRepository implements AuthRepository { 5 | const FakeAuthRepository({this.addDelay = true}); 6 | final bool addDelay; 7 | final fakeAuthInfo = const AuthInfo( 8 | accessToken: 'foo', 9 | ); 10 | 11 | @override 12 | Future> refreshToken() async { 13 | if (addDelay) { 14 | await Future.delayed(const Duration(seconds: 1)); 15 | } 16 | return Right(fakeAuthInfo); 17 | } 18 | 19 | @override 20 | Future> signIn({ 21 | required String username, 22 | required String password, 23 | }) async { 24 | if (addDelay) { 25 | await Future.delayed(const Duration(seconds: 1)); 26 | } 27 | return Right(fakeAuthInfo); 28 | // return left(const ApiFailure.forbidden()); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/my_app/android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/lib/features/start_up/data/start_up_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 2 | import 'package:{{project_name.snakeCase()}}/features/authentication/authentication.dart'; 3 | import 'package:{{project_name.snakeCase()}}/features/backend_environment/backend_environment.dart'; 4 | import 'package:{{project_name.snakeCase()}}/services/local_storage/local_storage.dart'; 5 | 6 | final startUpRepositoryProvider = 7 | Provider.autoDispose((ref) => StartUpRepository(ref)); 8 | 9 | class StartUpRepository { 10 | const StartUpRepository(this.ref); 11 | final Ref ref; 12 | 13 | Future prepareAuthentication() async { 14 | // Considering add more setup method here only if it has to be done before authenticating 15 | 16 | // Validate secure storage 17 | await ref.read(secureRepositoryProvider).validateSecureStorage(); 18 | 19 | // Setup backend host environment 20 | ref.read(hostControllerProvider.notifier).setUpHost(); 21 | 22 | // Start authentication 23 | ref.read(authControllerProvider.notifier).authentication(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /brick/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Danny Chuang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /src/my_app/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Danny Chuang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/my_app/coverage_badge.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | coverage 16 | coverage 17 | 100% 18 | 100% 19 | 20 | -------------------------------------------------------------------------------- /src/my_app/lib/features/authentication/domain/auth_info.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert' as convert; 2 | import 'package:freezed_annotation/freezed_annotation.dart'; 3 | 4 | part 'auth_info.freezed.dart'; 5 | 6 | @freezed 7 | class AuthInfo with _$AuthInfo { 8 | const AuthInfo._(); 9 | const factory AuthInfo({ 10 | required String accessToken, 11 | }) = _AuthInfo; 12 | 13 | bool get hasToken => accessToken.isNotEmpty; 14 | 15 | factory AuthInfo.empty() => const AuthInfo(accessToken: ''); 16 | 17 | factory AuthInfo.fromJson(Map json) { 18 | if (json['_data'] != null) { 19 | var accessToken = json['_data']['access_token'] as String; 20 | return AuthInfo(accessToken: accessToken); 21 | } 22 | return AuthInfo.empty(); 23 | } 24 | 25 | factory AuthInfo.fromSecureStorage(String value) { 26 | var json = convert.jsonDecode(value) as Map; 27 | return AuthInfo( 28 | accessToken: json['accessToken'] as String, 29 | ); 30 | } 31 | 32 | String get toSecureStorage { 33 | final Map json = { 34 | 'accessToken': accessToken, 35 | }; 36 | return convert.jsonEncode(json); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/coverage_badge.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | coverage 16 | coverage 17 | 100% 18 | 100% 19 | 20 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/lib/features/authentication/domain/auth_info.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert' as convert; 2 | import 'package:freezed_annotation/freezed_annotation.dart'; 3 | 4 | part 'auth_info.freezed.dart'; 5 | 6 | @freezed 7 | class AuthInfo with _$AuthInfo { 8 | const AuthInfo._(); 9 | const factory AuthInfo({ 10 | required String accessToken, 11 | }) = _AuthInfo; 12 | 13 | bool get hasToken => accessToken.isNotEmpty; 14 | 15 | factory AuthInfo.empty() => const AuthInfo(accessToken: ''); 16 | 17 | factory AuthInfo.fromJson(Map json) { 18 | if (json['_data'] != null) { 19 | var accessToken = json['_data']['access_token'] as String; 20 | return AuthInfo(accessToken: accessToken); 21 | } 22 | return AuthInfo.empty(); 23 | } 24 | 25 | factory AuthInfo.fromSecureStorage(String value) { 26 | var json = convert.jsonDecode(value) as Map; 27 | return AuthInfo( 28 | accessToken: json['accessToken'] as String, 29 | ); 30 | } 31 | 32 | String get toSecureStorage { 33 | final Map json = { 34 | 'accessToken': accessToken, 35 | }; 36 | return convert.jsonEncode(json); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/my_app/lib/utils/validators/string_validator.dart: -------------------------------------------------------------------------------- 1 | /// This file contains some helper functions used for string validation. 2 | abstract class StringValidator { 3 | bool isValid(String value); 4 | 5 | factory StringValidator.regex(String regexSource) => 6 | RegexValidator(regexSource: regexSource); 7 | 8 | factory StringValidator.nonEmpty() => NonEmptyStringValidator(); 9 | 10 | factory StringValidator.minLength(int minLength) => 11 | MinLengthStringValidator(minLength); 12 | } 13 | 14 | class RegexValidator implements StringValidator { 15 | RegexValidator({required this.regexSource}); 16 | final String regexSource; 17 | 18 | @override 19 | bool isValid(String value) { 20 | final RegExp regex = RegExp(regexSource); 21 | return regex.hasMatch(value); 22 | } 23 | } 24 | 25 | class NonEmptyStringValidator implements StringValidator { 26 | @override 27 | bool isValid(String value) { 28 | return value.isNotEmpty; 29 | } 30 | } 31 | 32 | class MinLengthStringValidator implements StringValidator { 33 | MinLengthStringValidator(this.minLength); 34 | final int minLength; 35 | 36 | @override 37 | bool isValid(String value) { 38 | return value.length >= minLength; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/lib/utils/validators/string_validator.dart: -------------------------------------------------------------------------------- 1 | /// This file contains some helper functions used for string validation. 2 | abstract class StringValidator { 3 | bool isValid(String value); 4 | 5 | factory StringValidator.regex(String regexSource) => 6 | RegexValidator(regexSource: regexSource); 7 | 8 | factory StringValidator.nonEmpty() => NonEmptyStringValidator(); 9 | 10 | factory StringValidator.minLength(int minLength) => 11 | MinLengthStringValidator(minLength); 12 | } 13 | 14 | class RegexValidator implements StringValidator { 15 | RegexValidator({required this.regexSource}); 16 | final String regexSource; 17 | 18 | @override 19 | bool isValid(String value) { 20 | final RegExp regex = RegExp(regexSource); 21 | return regex.hasMatch(value); 22 | } 23 | } 24 | 25 | class NonEmptyStringValidator implements StringValidator { 26 | @override 27 | bool isValid(String value) { 28 | return value.isNotEmpty; 29 | } 30 | } 31 | 32 | class MinLengthStringValidator implements StringValidator { 33 | MinLengthStringValidator(this.minLength); 34 | final int minLength; 35 | 36 | @override 37 | bool isValid(String value) { 38 | return value.length >= minLength; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/my_app/lib/features/backend_environment/domain/host.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutterprint/features/backend_environment/backend_environment.dart'; 2 | import 'package:freezed_annotation/freezed_annotation.dart'; 3 | 4 | part 'host.freezed.dart'; 5 | 6 | @freezed 7 | class Host with _$Host { 8 | const Host._(); 9 | const factory Host({ 10 | BackendEnv? backendEnv, 11 | @Default('') String localHost, 12 | }) = _Host; 13 | 14 | String get baseUrl => 15 | isLocal ? localHost : backendEnv?.baseUrl ?? BackendEnv.release.baseUrl; 16 | bool get isLocal => localHost.isNotEmpty; 17 | 18 | Uri getUri( 19 | Endpoint endpoint, { 20 | Map? queryParameters, 21 | String? path, 22 | }) { 23 | var selectedbaseUrl = isLocal ? baseUrl.split(':').first : baseUrl; 24 | var port = isLocal ? int.tryParse(baseUrl.split(':').last) ?? 8000 : null; 25 | return Uri( 26 | scheme: isLocal ? 'http' : 'https', 27 | host: selectedbaseUrl, 28 | port: port, 29 | path: path == null ? endpoint.path : '${endpoint.path}/$path', 30 | queryParameters: queryParameters, 31 | ); 32 | } 33 | 34 | String get bannerName { 35 | if (isLocal) { 36 | return 'local'; 37 | } 38 | return backendEnv?.appBanner ?? ''; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/my_app/test/utils/validators/string_validator_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutterprint/constant/regex_pattern.dart'; 2 | import 'package:flutterprint/utils/utils.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | 5 | void main() { 6 | group( 7 | '[StringValidator]:', 8 | () { 9 | test( 10 | 'IP address with port number regex validator', 11 | () { 12 | final ipAddressHostValidator = 13 | StringValidator.regex(RegexPattern.ipAddressPort); 14 | expect(ipAddressHostValidator.isValid('192.168.0.233'), false); 15 | expect(ipAddressHostValidator.isValid('192.168.0.233:8000'), true); 16 | }, 17 | ); 18 | 19 | test( 20 | 'Non empty validator', 21 | () { 22 | final nonEmptyValidator = StringValidator.nonEmpty(); 23 | expect(nonEmptyValidator.isValid(''), false); 24 | expect(nonEmptyValidator.isValid('123'), true); 25 | }, 26 | ); 27 | 28 | test( 29 | 'Minimum length validator', 30 | () { 31 | final minLengthValidator = StringValidator.minLength(8); 32 | expect(minLengthValidator.isValid('123'), false); 33 | expect(minLengthValidator.isValid('12345678'), true); 34 | }, 35 | ); 36 | }, 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/lib/features/backend_environment/domain/host.dart: -------------------------------------------------------------------------------- 1 | import 'package:{{project_name.snakeCase()}}/features/backend_environment/backend_environment.dart'; 2 | import 'package:freezed_annotation/freezed_annotation.dart'; 3 | 4 | part 'host.freezed.dart'; 5 | 6 | @freezed 7 | class Host with _$Host { 8 | const Host._(); 9 | const factory Host({ 10 | BackendEnv? backendEnv, 11 | @Default('') String localHost, 12 | }) = _Host; 13 | 14 | String get baseUrl => 15 | isLocal ? localHost : backendEnv?.baseUrl ?? BackendEnv.release.baseUrl; 16 | bool get isLocal => localHost.isNotEmpty; 17 | 18 | Uri getUri( 19 | Endpoint endpoint, { 20 | Map? queryParameters, 21 | String? path, 22 | }) { 23 | var selectedbaseUrl = isLocal ? baseUrl.split(':').first : baseUrl; 24 | var port = isLocal ? int.tryParse(baseUrl.split(':').last) ?? 8000 : null; 25 | return Uri( 26 | scheme: isLocal ? 'http' : 'https', 27 | host: selectedbaseUrl, 28 | port: port, 29 | path: path == null ? endpoint.path : '${endpoint.path}/$path', 30 | queryParameters: queryParameters, 31 | ); 32 | } 33 | 34 | String get bannerName { 35 | if (isLocal) { 36 | return 'local'; 37 | } 38 | return backendEnv?.appBanner ?? ''; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /.github/workflows/generate_flutterprint.yaml: -------------------------------------------------------------------------------- 1 | name: generate_flutterprint 2 | 3 | on: 4 | push: 5 | paths: 6 | - tool/generator/** 7 | - src/** 8 | branches: 9 | - main 10 | workflow_dispatch: 11 | 12 | jobs: 13 | build: 14 | runs-on: macos-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | 18 | - uses: dart-lang/setup-dart@v1 19 | 20 | - name: Install Dependencies 21 | working-directory: tool/generator 22 | run: dart pub get 23 | 24 | - name: Generate Template 25 | run: dart ./tool/generator/main.dart 26 | 27 | - name: Config Git User 28 | run: | 29 | git config user.name VGV Bot 30 | git config user.email vgvbot@users.noreply.github.com 31 | 32 | - name: Create Pull Request 33 | uses: peter-evans/create-pull-request@v4.2.3 34 | with: 35 | base: main 36 | branch: chore/generate-flutterprint 37 | commit-message: "chore: generate flutterprint template" 38 | title: "chore: generate flutterprint template" 39 | body: Please squash and merge me! 40 | labels: bot 41 | author: VGV Bot 42 | assignees: vgvbot 43 | reviewers: ycchuang305 44 | committer: VGV Bot 45 | -------------------------------------------------------------------------------- /src/my_app/lib/constant/app_sizes.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | /// Constant sizes to be used in the app (paddings, gaps, rounded corners etc.) 4 | class Sizes { 5 | static const p4 = 4.0; 6 | static const p8 = 8.0; 7 | static const p12 = 12.0; 8 | static const p16 = 16.0; 9 | static const p20 = 20.0; 10 | static const p24 = 24.0; 11 | static const p32 = 32.0; 12 | static const p48 = 48.0; 13 | static const p64 = 64.0; 14 | } 15 | 16 | /// Constant gap widths 17 | const gapW4 = SizedBox(width: Sizes.p4); 18 | const gapW8 = SizedBox(width: Sizes.p8); 19 | const gapW12 = SizedBox(width: Sizes.p12); 20 | const gapW16 = SizedBox(width: Sizes.p16); 21 | const gapW20 = SizedBox(width: Sizes.p20); 22 | const gapW24 = SizedBox(width: Sizes.p24); 23 | const gapW32 = SizedBox(width: Sizes.p32); 24 | const gapW48 = SizedBox(width: Sizes.p48); 25 | const gapW64 = SizedBox(width: Sizes.p64); 26 | 27 | /// Constant gap heights 28 | const gapH4 = SizedBox(height: Sizes.p4); 29 | const gapH8 = SizedBox(height: Sizes.p8); 30 | const gapH12 = SizedBox(height: Sizes.p12); 31 | const gapH16 = SizedBox(height: Sizes.p16); 32 | const gapH20 = SizedBox(height: Sizes.p20); 33 | const gapH24 = SizedBox(height: Sizes.p24); 34 | const gapH32 = SizedBox(height: Sizes.p32); 35 | const gapH48 = SizedBox(height: Sizes.p48); 36 | const gapH64 = SizedBox(height: Sizes.p64); 37 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/test/utils/validators/string_validator_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:{{project_name.snakeCase()}}/constant/regex_pattern.dart'; 2 | import 'package:{{project_name.snakeCase()}}/utils/utils.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | 5 | void main() { 6 | group( 7 | '[StringValidator]:', 8 | () { 9 | test( 10 | 'IP address with port number regex validator', 11 | () { 12 | final ipAddressHostValidator = 13 | StringValidator.regex(RegexPattern.ipAddressPort); 14 | expect(ipAddressHostValidator.isValid('192.168.0.233'), false); 15 | expect(ipAddressHostValidator.isValid('192.168.0.233:8000'), true); 16 | }, 17 | ); 18 | 19 | test( 20 | 'Non empty validator', 21 | () { 22 | final nonEmptyValidator = StringValidator.nonEmpty(); 23 | expect(nonEmptyValidator.isValid(''), false); 24 | expect(nonEmptyValidator.isValid('123'), true); 25 | }, 26 | ); 27 | 28 | test( 29 | 'Minimum length validator', 30 | () { 31 | final minLengthValidator = StringValidator.minLength(8); 32 | expect(minLengthValidator.isValid('123'), false); 33 | expect(minLengthValidator.isValid('12345678'), true); 34 | }, 35 | ); 36 | }, 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/lib/constant/app_sizes.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | /// Constant sizes to be used in the app (paddings, gaps, rounded corners etc.) 4 | class Sizes { 5 | static const p4 = 4.0; 6 | static const p8 = 8.0; 7 | static const p12 = 12.0; 8 | static const p16 = 16.0; 9 | static const p20 = 20.0; 10 | static const p24 = 24.0; 11 | static const p32 = 32.0; 12 | static const p48 = 48.0; 13 | static const p64 = 64.0; 14 | } 15 | 16 | /// Constant gap widths 17 | const gapW4 = SizedBox(width: Sizes.p4); 18 | const gapW8 = SizedBox(width: Sizes.p8); 19 | const gapW12 = SizedBox(width: Sizes.p12); 20 | const gapW16 = SizedBox(width: Sizes.p16); 21 | const gapW20 = SizedBox(width: Sizes.p20); 22 | const gapW24 = SizedBox(width: Sizes.p24); 23 | const gapW32 = SizedBox(width: Sizes.p32); 24 | const gapW48 = SizedBox(width: Sizes.p48); 25 | const gapW64 = SizedBox(width: Sizes.p64); 26 | 27 | /// Constant gap heights 28 | const gapH4 = SizedBox(height: Sizes.p4); 29 | const gapH8 = SizedBox(height: Sizes.p8); 30 | const gapH12 = SizedBox(height: Sizes.p12); 31 | const gapH16 = SizedBox(height: Sizes.p16); 32 | const gapH20 = SizedBox(height: Sizes.p20); 33 | const gapH24 = SizedBox(height: Sizes.p24); 34 | const gapH32 = SizedBox(height: Sizes.p32); 35 | const gapH48 = SizedBox(height: Sizes.p48); 36 | const gapH64 = SizedBox(height: Sizes.p64); 37 | -------------------------------------------------------------------------------- /src/my_app/lib/features/backend_environment/application/host_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 2 | import 'package:collection/collection.dart'; 3 | import 'package:flutterprint/features/backend_environment/backend_environment.dart'; 4 | import 'package:flutterprint/services/local_storage/local_storage.dart'; 5 | 6 | final hostControllerProvider = 7 | StateNotifierProvider((ref) { 8 | final sharedPreferenceRepository = ref.watch(sharedPreferenceRepoProvider); 9 | return HostController(sharedPreferenceRepository); 10 | }); 11 | 12 | class HostController extends StateNotifier { 13 | HostController(this.sharedPreferenceRepository) : super(const Host()); 14 | 15 | final SharedPreferenceRepository sharedPreferenceRepository; 16 | 17 | void setUpHost() => state = _getHost(sharedPreferenceRepository.hostKey); 18 | 19 | Future update({required String hostKey}) async { 20 | if (hostKey.isNotEmpty) { 21 | await sharedPreferenceRepository.updateHostKey(hostKey); 22 | } else { 23 | await sharedPreferenceRepository.removeHostKey(); 24 | } 25 | state = _getHost(hostKey); 26 | } 27 | 28 | Host _getHost(String key) { 29 | final backendEnv = 30 | BackendEnv.values.firstWhereOrNull((env) => env.hostKey == key); 31 | return Host( 32 | backendEnv: backendEnv, 33 | localHost: backendEnv == null ? key : '', 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/lib/features/backend_environment/application/host_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 2 | import 'package:collection/collection.dart'; 3 | import 'package:{{project_name.snakeCase()}}/features/backend_environment/backend_environment.dart'; 4 | import 'package:{{project_name.snakeCase()}}/services/local_storage/local_storage.dart'; 5 | 6 | final hostControllerProvider = 7 | StateNotifierProvider((ref) { 8 | final sharedPreferenceRepository = ref.watch(sharedPreferenceRepoProvider); 9 | return HostController(sharedPreferenceRepository); 10 | }); 11 | 12 | class HostController extends StateNotifier { 13 | HostController(this.sharedPreferenceRepository) : super(const Host()); 14 | 15 | final SharedPreferenceRepository sharedPreferenceRepository; 16 | 17 | void setUpHost() => state = _getHost(sharedPreferenceRepository.hostKey); 18 | 19 | Future update({required String hostKey}) async { 20 | if (hostKey.isNotEmpty) { 21 | await sharedPreferenceRepository.updateHostKey(hostKey); 22 | } else { 23 | await sharedPreferenceRepository.removeHostKey(); 24 | } 25 | state = _getHost(hostKey); 26 | } 27 | 28 | Host _getHost(String key) { 29 | final backendEnv = 30 | BackendEnv.values.firstWhereOrNull((env) => env.hostKey == key); 31 | return Host( 32 | backendEnv: backendEnv, 33 | localHost: backendEnv == null ? key : '', 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/my_app/ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '11.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | end 36 | 37 | post_install do |installer| 38 | installer.pods_project.targets.each do |target| 39 | flutter_additional_ios_build_settings(target) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /src/my_app/lib/services/rest_api_service/rest_api_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 | import 'package:flutterprint/l10n/l10n.dart'; 4 | import 'package:flutterprint/services/rest_api_service/domain/api_failure.dart'; 5 | import 'package:flutterprint/services/snackbar/app_snackbar_repository.dart'; 6 | 7 | export 'data/rest_api_service.dart'; 8 | export 'data/rest_api_service_impl.dart'; 9 | export 'domain/token_type.dart'; 10 | export 'domain/api_failure.dart'; 11 | export 'domain/api_method.dart'; 12 | export 'domain/api_status.dart'; 13 | export 'package:dartz/dartz.dart'; 14 | 15 | extension ApiFailureX on ApiFailure { 16 | void showPredefinedSnackbar({ 17 | required BuildContext context, 18 | required WidgetRef ref, 19 | }) => 20 | ref.read(appSnackBarRepoProvider).showSnackbar( 21 | context, 22 | message: when( 23 | apiAgreementConflict: () => '', 24 | badRequest: (_) => context.l10n.badRequest, 25 | forbidden: (_) => context.l10n.forbidden, 26 | socket: (_) => context.l10n.socket, 27 | notFound: (_) => context.l10n.notFound, 28 | serviceUnavailable: (_) => context.l10n.serviceUnavailable, 29 | timeout: (_) => context.l10n.timeout, 30 | unauthorized: (_) => context.l10n.unauthorized, 31 | undefined: (statusCode, _) => context.l10n.unknown(statusCode), 32 | ), 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '11.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | end 36 | 37 | post_install do |installer| 38 | installer.pods_project.targets.each do |target| 39 | flutter_additional_ios_build_settings(target) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /src/my_app/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at 17 | # https://dart-lang.github.io/linter/lints/index.html. 18 | # 19 | # Instead of disabling a lint rule for the entire project in the 20 | # section below, it can also be suppressed for a single line of code 21 | # or a specific dart file by using the `// ignore: name_of_lint` and 22 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 23 | # producing the lint. 24 | rules: 25 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 26 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 27 | - use_super_parameters 28 | 29 | # Additional information about this file can be found at 30 | # https://dart.dev/guides/language/analysis-options -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/lib/services/rest_api_service/rest_api_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 | import 'package:{{project_name.snakeCase()}}/l10n/l10n.dart'; 4 | import 'package:{{project_name.snakeCase()}}/services/rest_api_service/domain/api_failure.dart'; 5 | import 'package:{{project_name.snakeCase()}}/services/snackbar/app_snackbar_repository.dart'; 6 | 7 | export 'data/rest_api_service.dart'; 8 | export 'data/rest_api_service_impl.dart'; 9 | export 'domain/token_type.dart'; 10 | export 'domain/api_failure.dart'; 11 | export 'domain/api_method.dart'; 12 | export 'domain/api_status.dart'; 13 | export 'package:dartz/dartz.dart'; 14 | 15 | extension ApiFailureX on ApiFailure { 16 | void showPredefinedSnackbar({ 17 | required BuildContext context, 18 | required WidgetRef ref, 19 | }) => 20 | ref.read(appSnackBarRepoProvider).showSnackbar( 21 | context, 22 | message: when( 23 | apiAgreementConflict: () => '', 24 | badRequest: (_) => context.l10n.badRequest, 25 | forbidden: (_) => context.l10n.forbidden, 26 | socket: (_) => context.l10n.socket, 27 | notFound: (_) => context.l10n.notFound, 28 | serviceUnavailable: (_) => context.l10n.serviceUnavailable, 29 | timeout: (_) => context.l10n.timeout, 30 | unauthorized: (_) => context.l10n.unauthorized, 31 | undefined: (statusCode, _) => context.l10n.unknown(statusCode), 32 | ), 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /src/my_app/lib/l10n/arb/app_zh.arb: -------------------------------------------------------------------------------- 1 | { 2 | "@@locale": "zh", 3 | "account": "帳號", 4 | "@account": { 5 | "description": "用於帳號欄位的標題文字" 6 | }, 7 | "password": "密碼", 8 | "@password": { 9 | "description": "用於密碼欄位的標題文字" 10 | }, 11 | "signinAppBarTitle": "登入", 12 | 13 | "pageNotFound": "404 - 找不到頁面", 14 | 15 | "homePageAppBarTitle": "首頁", 16 | "signOut": "登出", 17 | "staySignedin": "保持登入", 18 | "submit": "送出", 19 | "authenticating": "驗證中...", 20 | "accountCanNotBeEmpty": "帳號不可為空白", 21 | "passwordCanNotBeEmpty": "密碼不可為空白", 22 | "incorrectAccountOrPassword": "帳號或密碼錯誤", 23 | 24 | "apiAgreementConflict": "API 協議衝突", 25 | "badRequest": "錯誤請求", 26 | "forbidden": "權限不足", 27 | "socket": "連線異常", 28 | "notFound": "未找到", 29 | "serviceUnavailable": "服務無法使用", 30 | "timeout": "請求逾時", 31 | "unauthorized": "帳戶未授權", 32 | "unknown": "錯誤碼 {statusCode}", 33 | 34 | "backendEnvEnableText": "再按 {value} 次進入後端環境設定頁", 35 | "@backendEnvEnableText": { 36 | "placeholders": { 37 | "value": { 38 | "type": "int" 39 | } 40 | } 41 | }, 42 | "backendEnvAppBarTitle": "後端環境設定", 43 | "backendEnvRelease": "Release", 44 | "backendEnvDemo": "Demo", 45 | "backendEnvDevelop": "Develop", 46 | "backendEnvLocal": "Local", 47 | "backendEnvLocalDescription": "Enter the local IP address followed by a port number.\nFor example, you can access the host at http://192.168.0.1:8000 by entering 192.168.0.1", 48 | "save": "儲存", 49 | 50 | "incorrectInputFormat": "輸入格式錯誤" 51 | } -------------------------------------------------------------------------------- /src/my_app/lib/routing/not_found_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 | import 'package:flutterprint/constant/app_sizes.dart'; 4 | import 'package:flutterprint/l10n/l10n.dart'; 5 | import 'package:flutterprint/routing/app_router.dart'; 6 | 7 | class NotFoundPage extends ConsumerWidget { 8 | const NotFoundPage({super.key}); 9 | 10 | @override 11 | Widget build(BuildContext context, WidgetRef ref) { 12 | final l10n = context.l10n; 13 | return Scaffold( 14 | body: Padding( 15 | padding: const EdgeInsets.all(Sizes.p16), 16 | child: Center( 17 | child: Column( 18 | mainAxisSize: MainAxisSize.min, 19 | crossAxisAlignment: CrossAxisAlignment.center, 20 | children: [ 21 | Text( 22 | l10n.pageNotFound, 23 | style: Theme.of(context).textTheme.headlineMedium, 24 | textAlign: TextAlign.center, 25 | ), 26 | gapH32, 27 | TextButton.icon( 28 | onPressed: () => 29 | ref.read(appRouterProvider).go(AppRoute.home.path), 30 | icon: const Icon( 31 | Icons.home_outlined, 32 | size: Sizes.p24, 33 | ), 34 | label: Text( 35 | l10n.homePageAppBarTitle, 36 | style: const TextStyle(fontSize: Sizes.p20), 37 | ), 38 | ), 39 | ], 40 | ), 41 | ), 42 | ), 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/my_app/lib/features/start_up/presentation/splash_page.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:after_layout/after_layout.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 6 | import 'package:flutterprint/constant/theme/theme.dart'; 7 | import 'package:flutterprint/features/authentication/authentication.dart'; 8 | import 'package:flutterprint/features/start_up/data/start_up_repository.dart'; 9 | import 'package:flutterprint/services/rest_api_service/rest_api_service.dart'; 10 | 11 | class SplashPage extends ConsumerStatefulWidget { 12 | const SplashPage({super.key}); 13 | 14 | @override 15 | ConsumerState createState() => _SplashPageState(); 16 | } 17 | 18 | class _SplashPageState extends ConsumerState 19 | with AfterLayoutMixin { 20 | @override 21 | FutureOr afterFirstLayout(BuildContext context) => 22 | ref.read(startUpRepositoryProvider).prepareAuthentication(); 23 | 24 | @override 25 | Widget build(BuildContext context) { 26 | ref.listen( 27 | authControllerProvider, 28 | (_, state) { 29 | state.whenOrNull( 30 | failure: (apiFailure) => 31 | apiFailure.showPredefinedSnackbar(context: context, ref: ref), 32 | ); 33 | }, 34 | ); 35 | final colorScheme = context.colorScheme; 36 | return Scaffold( 37 | backgroundColor: colorScheme.background, 38 | body: const Center( 39 | child: FlutterLogo( 40 | size: 64, 41 | ), 42 | ), 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at 17 | # https://dart-lang.github.io/linter/lints/index.html. 18 | # 19 | # Instead of disabling a lint rule for the entire project in the 20 | # section below, it can also be suppressed for a single line of code 21 | # or a specific dart file by using the `// ignore: name_of_lint` and 22 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 23 | # producing the lint. 24 | rules: 25 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 26 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 27 | - use_super_parameters 28 | 29 | # Additional information about this file can be found at 30 | # https://dart.dev/guides/language/analysis-options -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/lib/l10n/arb/app_zh.arb: -------------------------------------------------------------------------------- 1 | { 2 | "@@locale": "zh", 3 | "account": "帳號", 4 | "@account": { 5 | "description": "用於帳號欄位的標題文字" 6 | }, 7 | "password": "密碼", 8 | "@password": { 9 | "description": "用於密碼欄位的標題文字" 10 | }, 11 | "signinAppBarTitle": "登入", 12 | 13 | "pageNotFound": "404 - 找不到頁面", 14 | 15 | "homePageAppBarTitle": "首頁", 16 | "signOut": "登出", 17 | "staySignedin": "保持登入", 18 | "submit": "送出", 19 | "authenticating": "驗證中...", 20 | "accountCanNotBeEmpty": "帳號不可為空白", 21 | "passwordCanNotBeEmpty": "密碼不可為空白", 22 | "incorrectAccountOrPassword": "帳號或密碼錯誤", 23 | 24 | "apiAgreementConflict": "API 協議衝突", 25 | "badRequest": "錯誤請求", 26 | "forbidden": "權限不足", 27 | "socket": "連線異常", 28 | "notFound": "未找到", 29 | "serviceUnavailable": "服務無法使用", 30 | "timeout": "請求逾時", 31 | "unauthorized": "帳戶未授權", 32 | "unknown": "錯誤碼 {statusCode}", 33 | 34 | "backendEnvEnableText": "再按 {value} 次進入後端環境設定頁", 35 | "@backendEnvEnableText": { 36 | "placeholders": { 37 | "value": { 38 | "type": "int" 39 | } 40 | } 41 | }, 42 | "backendEnvAppBarTitle": "後端環境設定", 43 | "backendEnvRelease": "Release", 44 | "backendEnvDemo": "Demo", 45 | "backendEnvDevelop": "Develop", 46 | "backendEnvLocal": "Local", 47 | "backendEnvLocalDescription": "Enter the local IP address followed by a port number.\nFor example, you can access the host at http://192.168.0.1:8000 by entering 192.168.0.1", 48 | "save": "儲存", 49 | 50 | "incorrectInputFormat": "輸入格式錯誤" 51 | } -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/lib/routing/not_found_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 | import 'package:{{project_name.snakeCase()}}/constant/app_sizes.dart'; 4 | import 'package:{{project_name.snakeCase()}}/l10n/l10n.dart'; 5 | import 'package:{{project_name.snakeCase()}}/routing/app_router.dart'; 6 | 7 | class NotFoundPage extends ConsumerWidget { 8 | const NotFoundPage({super.key}); 9 | 10 | @override 11 | Widget build(BuildContext context, WidgetRef ref) { 12 | final l10n = context.l10n; 13 | return Scaffold( 14 | body: Padding( 15 | padding: const EdgeInsets.all(Sizes.p16), 16 | child: Center( 17 | child: Column( 18 | mainAxisSize: MainAxisSize.min, 19 | crossAxisAlignment: CrossAxisAlignment.center, 20 | children: [ 21 | Text( 22 | l10n.pageNotFound, 23 | style: Theme.of(context).textTheme.headlineMedium, 24 | textAlign: TextAlign.center, 25 | ), 26 | gapH32, 27 | TextButton.icon( 28 | onPressed: () => 29 | ref.read(appRouterProvider).go(AppRoute.home.path), 30 | icon: const Icon( 31 | Icons.home_outlined, 32 | size: Sizes.p24, 33 | ), 34 | label: Text( 35 | l10n.homePageAppBarTitle, 36 | style: const TextStyle(fontSize: Sizes.p20), 37 | ), 38 | ), 39 | ], 40 | ), 41 | ), 42 | ), 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/lib/features/start_up/presentation/splash_page.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:after_layout/after_layout.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 6 | import 'package:{{project_name.snakeCase()}}/constant/theme/theme.dart'; 7 | import 'package:{{project_name.snakeCase()}}/features/authentication/authentication.dart'; 8 | import 'package:{{project_name.snakeCase()}}/features/start_up/data/start_up_repository.dart'; 9 | import 'package:{{project_name.snakeCase()}}/services/rest_api_service/rest_api_service.dart'; 10 | 11 | class SplashPage extends ConsumerStatefulWidget { 12 | const SplashPage({super.key}); 13 | 14 | @override 15 | ConsumerState createState() => _SplashPageState(); 16 | } 17 | 18 | class _SplashPageState extends ConsumerState 19 | with AfterLayoutMixin { 20 | @override 21 | FutureOr afterFirstLayout(BuildContext context) => 22 | ref.read(startUpRepositoryProvider).prepareAuthentication(); 23 | 24 | @override 25 | Widget build(BuildContext context) { 26 | ref.listen( 27 | authControllerProvider, 28 | (_, state) { 29 | state.whenOrNull( 30 | failure: (apiFailure) => 31 | apiFailure.showPredefinedSnackbar(context: context, ref: ref), 32 | ); 33 | }, 34 | ); 35 | final colorScheme = context.colorScheme; 36 | return Scaffold( 37 | backgroundColor: colorScheme.background, 38 | body: const Center( 39 | child: FlutterLogo( 40 | size: 64, 41 | ), 42 | ), 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/my_app/ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/my_app/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 15 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | 30 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/my_app/lib/features/home/presentation/home_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 | import 'package:flutterprint/constant/app_sizes.dart'; 4 | import 'package:flutterprint/constant/theme/theme.dart'; 5 | import 'package:flutterprint/features/authentication/authentication.dart'; 6 | import 'package:flutterprint/features/theme_mode/application/theme_mode_controller.dart'; 7 | import 'package:flutterprint/l10n/l10n.dart'; 8 | 9 | class HomePage extends ConsumerWidget { 10 | const HomePage({super.key}); 11 | static const signOutKey = Key('signOut'); 12 | @override 13 | Widget build(BuildContext context, WidgetRef ref) { 14 | final l10n = context.l10n; 15 | final theme = ref.watch(themeModeControllerProvider); 16 | final colorScheme = context.colorScheme; 17 | return Scaffold( 18 | backgroundColor: colorScheme.background, 19 | appBar: AppBar( 20 | title: Text(l10n.homePageAppBarTitle), 21 | actions: [ 22 | IconButton( 23 | onPressed: () => ref 24 | .read(themeModeControllerProvider.notifier) 25 | .toggleAndSaveThemeMode(), 26 | icon: Icon( 27 | theme == ThemeMode.dark ? Icons.light_mode : Icons.dark_mode, 28 | ), 29 | ), 30 | ], 31 | ), 32 | body: Center( 33 | child: TextButton.icon( 34 | key: HomePage.signOutKey, 35 | onPressed: () => ref.read(authControllerProvider.notifier).signOut(), 36 | icon: const Icon( 37 | Icons.logout_outlined, 38 | size: Sizes.p24, 39 | ), 40 | label: Text( 41 | l10n.signOut, 42 | style: const TextStyle(fontSize: Sizes.p20), 43 | ), 44 | ), 45 | ), 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 15 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | 30 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/lib/features/home/presentation/home_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 | import 'package:{{project_name.snakeCase()}}/constant/app_sizes.dart'; 4 | import 'package:{{project_name.snakeCase()}}/constant/theme/theme.dart'; 5 | import 'package:{{project_name.snakeCase()}}/features/authentication/authentication.dart'; 6 | import 'package:{{project_name.snakeCase()}}/features/theme_mode/application/theme_mode_controller.dart'; 7 | import 'package:{{project_name.snakeCase()}}/l10n/l10n.dart'; 8 | 9 | class HomePage extends ConsumerWidget { 10 | const HomePage({super.key}); 11 | static const signOutKey = Key('signOut'); 12 | @override 13 | Widget build(BuildContext context, WidgetRef ref) { 14 | final l10n = context.l10n; 15 | final theme = ref.watch(themeModeControllerProvider); 16 | final colorScheme = context.colorScheme; 17 | return Scaffold( 18 | backgroundColor: colorScheme.background, 19 | appBar: AppBar( 20 | title: Text(l10n.homePageAppBarTitle), 21 | actions: [ 22 | IconButton( 23 | onPressed: () => ref 24 | .read(themeModeControllerProvider.notifier) 25 | .toggleAndSaveThemeMode(), 26 | icon: Icon( 27 | theme == ThemeMode.dark ? Icons.light_mode : Icons.dark_mode, 28 | ), 29 | ), 30 | ], 31 | ), 32 | body: Center( 33 | child: TextButton.icon( 34 | key: HomePage.signOutKey, 35 | onPressed: () => ref.read(authControllerProvider.notifier).signOut(), 36 | icon: const Icon( 37 | Icons.logout_outlined, 38 | size: Sizes.p24, 39 | ), 40 | label: Text( 41 | l10n.signOut, 42 | style: const TextStyle(fontSize: Sizes.p20), 43 | ), 44 | ), 45 | ), 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/my_app/test/robot.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 2 | import 'package:flutterprint/features/authentication/authentication.dart'; 3 | import 'package:flutterprint/features/authentication/data/fake_auth_repository.dart'; 4 | import 'package:flutterprint/main.dart'; 5 | import 'package:flutterprint/services/local_storage/data/fake_secure_storage_repository.dart'; 6 | import 'package:flutterprint/services/local_storage/local_storage.dart'; 7 | import 'package:flutter_test/flutter_test.dart'; 8 | import 'package:shared_preferences/shared_preferences.dart'; 9 | 10 | import 'features/authentication/auth_robot.dart'; 11 | import 'features/backend_environment/backend_robot.dart'; 12 | 13 | class Robot { 14 | Robot(this.tester) 15 | : authRobot = AuthRobot(tester), 16 | backendRobot = BackendRobot(tester); 17 | final WidgetTester tester; 18 | final AuthRobot authRobot; 19 | final BackendRobot backendRobot; 20 | 21 | Future pumpAppWidget( 22 | {Map initialSharedPreferenceValues = const {}}) async { 23 | const secureStorageRepository = FakeSecureStorageRepository(); 24 | const authRepository = FakeAuthRepository(addDelay: false); 25 | SharedPreferences.setMockInitialValues(initialSharedPreferenceValues); 26 | final sharedPreferences = await SharedPreferences.getInstance(); 27 | final sharedPrefRepository = SharedPreferenceRepository(sharedPreferences); 28 | // Create ProviderContainer with any required overrides 29 | final container = ProviderContainer( 30 | overrides: [ 31 | secureRepositoryProvider.overrideWithValue(secureStorageRepository), 32 | authRepoProvider.overrideWithValue(authRepository), 33 | sharedPreferenceRepoProvider.overrideWithValue(sharedPrefRepository), 34 | ], 35 | ); 36 | await tester.pumpWidget( 37 | UncontrolledProviderScope( 38 | container: container, 39 | child: const AppWidget(), 40 | ), 41 | ); 42 | await tester.pumpAndSettle(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/my_app/test/features/authentication/auth_robot.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutterprint/features/authentication/authentication.dart'; 2 | import 'package:flutterprint/features/home/presentation/home_page.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | 5 | class AuthRobot { 6 | const AuthRobot(this.tester); 7 | final WidgetTester tester; 8 | 9 | Future enterAccount(String value) async { 10 | final accountField = find.byKey(SignInPage.accountTextFieldKey); 11 | expect(accountField, findsOneWidget); 12 | await tester.enterText(accountField, value); 13 | } 14 | 15 | Future enterPassword(String value) async { 16 | final passwordField = find.byKey(SignInPage.passwordTextFieldKey); 17 | expect(passwordField, findsOneWidget); 18 | await tester.enterText(passwordField, value); 19 | } 20 | 21 | Future tapEmailAndPasswordSubmitButton() async { 22 | final submitButton = find.byKey(SignInPage.submitButtonKey); 23 | expect(submitButton, findsOneWidget); 24 | await tester.tap(submitButton); 25 | await tester.pumpAndSettle(); 26 | } 27 | 28 | Future enterAndSubmitAccountAndPassword() async { 29 | await enterAccount('tester'); 30 | await tester.pump(); 31 | await enterPassword('123456'); 32 | await tapEmailAndPasswordSubmitButton(); 33 | } 34 | 35 | Future tapSignOutButton() async { 36 | final signOutButton = find.byKey(HomePage.signOutKey); 37 | expect(signOutButton, findsOneWidget); 38 | await tester.tap(signOutButton); 39 | await tester.pumpAndSettle(); 40 | } 41 | 42 | void expectAccountAndPasswordFieldsFound() { 43 | final accountField = find.byKey(SignInPage.accountTextFieldKey); 44 | expect(accountField, findsOneWidget); 45 | final passwordField = find.byKey(SignInPage.passwordTextFieldKey); 46 | expect(passwordField, findsOneWidget); 47 | } 48 | 49 | void expectSignOutButtonFound() { 50 | final signOutBtn = find.byKey(HomePage.signOutKey); 51 | expect(signOutBtn, findsOneWidget); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/my_app/ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | Flutterprint 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | Flutterprint 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(FLUTTER_BUILD_NUMBER) 25 | LSRequiresIPhoneOS 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | UIViewControllerBasedStatusBarAppearance 45 | 46 | CADisableMinimumFrameDurationOnPhone 47 | 48 | UIApplicationSupportsIndirectInputEvents 49 | 50 | CFBundleLocalizations 51 | 52 | en 53 | zh 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /src/my_app/lib/services/local_storage/data/shared_preference_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart' show ThemeMode; 2 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 | import 'package:flutterprint/features/backend_environment/backend_environment.dart'; 4 | import 'package:shared_preferences/shared_preferences.dart'; 5 | 6 | final sharedPreferenceRepoProvider = 7 | Provider((ref) { 8 | // The value from this repository will be overriden in main method 9 | // after instantiating Shared Preferences 10 | throw UnimplementedError(); 11 | }); 12 | 13 | /// Develop enviornment repository 14 | class SharedPreferenceRepository { 15 | final SharedPreferences _pref; 16 | 17 | const SharedPreferenceRepository(this._pref); 18 | 19 | static const prefHostKey = 'hostKey'; 20 | static const prefValidSecureStorageKey = 'validSecureStorage'; 21 | static const prefStaySignedInKey = 'staySignedIn'; 22 | static const prefAppThemeMode = 'appThemeMode'; 23 | 24 | String get hostKey => 25 | _pref.getString(prefHostKey)?.trim() ?? BackendEnv.release.hostKey; 26 | 27 | Future updateHostKey(String host) => _pref.setString(prefHostKey, host); 28 | 29 | Future removeHostKey() => _pref.remove(prefHostKey); 30 | 31 | bool isSecureStorageValid() => 32 | _pref.getBool(prefValidSecureStorageKey) ?? false; 33 | 34 | Future setSecureStorageAsValid() => 35 | _pref.setBool(prefValidSecureStorageKey, true); 36 | 37 | Future setStaySignedIn(bool value) => 38 | _pref.setBool(prefStaySignedInKey, value); 39 | 40 | bool getStaySignedIn() => _pref.getBool(prefStaySignedInKey) ?? false; 41 | 42 | Future setThemeMode(ThemeMode themeMode) async { 43 | return await _pref.setString(prefAppThemeMode, themeMode.name); 44 | } 45 | 46 | ThemeMode getThemeMode() { 47 | final themeName = _pref.getString(prefAppThemeMode); 48 | return ThemeMode.values.firstWhere( 49 | (mode) => mode.name == themeName, 50 | orElse: () => ThemeMode.light, 51 | ); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/test/features/authentication/auth_robot.dart: -------------------------------------------------------------------------------- 1 | import 'package:{{project_name.snakeCase()}}/features/authentication/authentication.dart'; 2 | import 'package:{{project_name.snakeCase()}}/features/home/presentation/home_page.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | 5 | class AuthRobot { 6 | const AuthRobot(this.tester); 7 | final WidgetTester tester; 8 | 9 | Future enterAccount(String value) async { 10 | final accountField = find.byKey(SignInPage.accountTextFieldKey); 11 | expect(accountField, findsOneWidget); 12 | await tester.enterText(accountField, value); 13 | } 14 | 15 | Future enterPassword(String value) async { 16 | final passwordField = find.byKey(SignInPage.passwordTextFieldKey); 17 | expect(passwordField, findsOneWidget); 18 | await tester.enterText(passwordField, value); 19 | } 20 | 21 | Future tapEmailAndPasswordSubmitButton() async { 22 | final submitButton = find.byKey(SignInPage.submitButtonKey); 23 | expect(submitButton, findsOneWidget); 24 | await tester.tap(submitButton); 25 | await tester.pumpAndSettle(); 26 | } 27 | 28 | Future enterAndSubmitAccountAndPassword() async { 29 | await enterAccount('tester'); 30 | await tester.pump(); 31 | await enterPassword('123456'); 32 | await tapEmailAndPasswordSubmitButton(); 33 | } 34 | 35 | Future tapSignOutButton() async { 36 | final signOutButton = find.byKey(HomePage.signOutKey); 37 | expect(signOutButton, findsOneWidget); 38 | await tester.tap(signOutButton); 39 | await tester.pumpAndSettle(); 40 | } 41 | 42 | void expectAccountAndPasswordFieldsFound() { 43 | final accountField = find.byKey(SignInPage.accountTextFieldKey); 44 | expect(accountField, findsOneWidget); 45 | final passwordField = find.byKey(SignInPage.passwordTextFieldKey); 46 | expect(passwordField, findsOneWidget); 47 | } 48 | 49 | void expectSignOutButtonFound() { 50 | final signOutBtn = find.byKey(HomePage.signOutKey); 51 | expect(signOutBtn, findsOneWidget); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | {{project_name.titleCase()}} 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | {{project_name.titleCase()}} 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(FLUTTER_BUILD_NUMBER) 25 | LSRequiresIPhoneOS 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | UIViewControllerBasedStatusBarAppearance 45 | 46 | CADisableMinimumFrameDurationOnPhone 47 | 48 | UIApplicationSupportsIndirectInputEvents 49 | 50 | CFBundleLocalizations 51 | 52 | en 53 | zh 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/lib/services/local_storage/data/shared_preference_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart' show ThemeMode; 2 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 | import 'package:{{project_name.snakeCase()}}/features/backend_environment/backend_environment.dart'; 4 | import 'package:shared_preferences/shared_preferences.dart'; 5 | 6 | final sharedPreferenceRepoProvider = 7 | Provider((ref) { 8 | // The value from this repository will be overriden in main method 9 | // after instantiating Shared Preferences 10 | throw UnimplementedError(); 11 | }); 12 | 13 | /// Develop enviornment repository 14 | class SharedPreferenceRepository { 15 | final SharedPreferences _pref; 16 | 17 | const SharedPreferenceRepository(this._pref); 18 | 19 | static const prefHostKey = 'hostKey'; 20 | static const prefValidSecureStorageKey = 'validSecureStorage'; 21 | static const prefStaySignedInKey = 'staySignedIn'; 22 | static const prefAppThemeMode = 'appThemeMode'; 23 | 24 | String get hostKey => 25 | _pref.getString(prefHostKey)?.trim() ?? BackendEnv.release.hostKey; 26 | 27 | Future updateHostKey(String host) => _pref.setString(prefHostKey, host); 28 | 29 | Future removeHostKey() => _pref.remove(prefHostKey); 30 | 31 | bool isSecureStorageValid() => 32 | _pref.getBool(prefValidSecureStorageKey) ?? false; 33 | 34 | Future setSecureStorageAsValid() => 35 | _pref.setBool(prefValidSecureStorageKey, true); 36 | 37 | Future setStaySignedIn(bool value) => 38 | _pref.setBool(prefStaySignedInKey, value); 39 | 40 | bool getStaySignedIn() => _pref.getBool(prefStaySignedInKey) ?? false; 41 | 42 | Future setThemeMode(ThemeMode themeMode) async { 43 | return await _pref.setString(prefAppThemeMode, themeMode.name); 44 | } 45 | 46 | ThemeMode getThemeMode() { 47 | final themeName = _pref.getString(prefAppThemeMode); 48 | return ThemeMode.values.firstWhere( 49 | (mode) => mode.name == themeName, 50 | orElse: () => ThemeMode.light, 51 | ); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/my_app/lib/widgets/countdown_trigger.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:flutter/material.dart'; 3 | 4 | /// A countdown trigger is a widget that starts counting down from a specified number [startFrom], 5 | /// and decreases by 1 every time the child widget is pressed, the [onUpdated] callback is triggered each time the count decreases. 6 | /// When the count reaches 0, the [onFinished] callback is triggered and the count will reset back to [startFrom]. 7 | /// 8 | /// If there are no press events within [resetDurationInSec] seconds, 9 | /// the count will reset back to [startFrom]. 10 | class CountdownTrigger extends StatefulWidget { 11 | final Widget child; 12 | final int startFrom; 13 | final int resetDurationInSec; 14 | final void Function(int)? onUpdated; 15 | final VoidCallback? onFinished; 16 | const CountdownTrigger({ 17 | super.key, 18 | required this.startFrom, 19 | required this.child, 20 | this.resetDurationInSec = 3, 21 | this.onUpdated, 22 | this.onFinished, 23 | }) : assert(startFrom > 0), 24 | assert(resetDurationInSec > 0); 25 | 26 | @override 27 | State createState() => _CountdownTriggerState(); 28 | } 29 | 30 | class _CountdownTriggerState extends State { 31 | Timer? timer; 32 | late int count; 33 | 34 | @override 35 | void initState() { 36 | super.initState(); 37 | count = widget.startFrom; 38 | } 39 | 40 | @override 41 | void dispose() { 42 | timer?.cancel(); 43 | super.dispose(); 44 | } 45 | 46 | @override 47 | Widget build(BuildContext context) { 48 | return GestureDetector( 49 | onTap: () { 50 | timer?.cancel(); 51 | timer = Timer( 52 | Duration(seconds: widget.resetDurationInSec), 53 | () => count = widget.startFrom, 54 | ); 55 | count--; 56 | if (count > 0) { 57 | widget.onUpdated?.call(count); 58 | } else { 59 | timer?.cancel(); 60 | count = widget.startFrom; 61 | widget.onFinished?.call(); 62 | } 63 | }, 64 | child: widget.child, 65 | ); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/test/robot.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 2 | import 'package:{{project_name.snakeCase()}}/features/authentication/authentication.dart'; 3 | import 'package:{{project_name.snakeCase()}}/features/authentication/data/fake_auth_repository.dart'; 4 | import 'package:{{project_name.snakeCase()}}/main.dart'; 5 | import 'package:{{project_name.snakeCase()}}/services/local_storage/data/fake_secure_storage_repository.dart'; 6 | import 'package:{{project_name.snakeCase()}}/services/local_storage/local_storage.dart'; 7 | import 'package:flutter_test/flutter_test.dart'; 8 | import 'package:shared_preferences/shared_preferences.dart'; 9 | 10 | import 'features/authentication/auth_robot.dart'; 11 | import 'features/backend_environment/backend_robot.dart'; 12 | 13 | class Robot { 14 | Robot(this.tester) 15 | : authRobot = AuthRobot(tester), 16 | backendRobot = BackendRobot(tester); 17 | final WidgetTester tester; 18 | final AuthRobot authRobot; 19 | final BackendRobot backendRobot; 20 | 21 | Future pumpAppWidget( 22 | {Map initialSharedPreferenceValues = const {}}) async { 23 | const secureStorageRepository = FakeSecureStorageRepository(); 24 | const authRepository = FakeAuthRepository(addDelay: false); 25 | SharedPreferences.setMockInitialValues(initialSharedPreferenceValues); 26 | final sharedPreferences = await SharedPreferences.getInstance(); 27 | final sharedPrefRepository = SharedPreferenceRepository(sharedPreferences); 28 | // Create ProviderContainer with any required overrides 29 | final container = ProviderContainer( 30 | overrides: [ 31 | secureRepositoryProvider.overrideWithValue(secureStorageRepository), 32 | authRepoProvider.overrideWithValue(authRepository), 33 | sharedPreferenceRepoProvider.overrideWithValue(sharedPrefRepository), 34 | ], 35 | ); 36 | await tester.pumpWidget( 37 | UncontrolledProviderScope( 38 | container: container, 39 | child: const AppWidget(), 40 | ), 41 | ); 42 | await tester.pumpAndSettle(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/lib/widgets/countdown_trigger.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:flutter/material.dart'; 3 | 4 | /// A countdown trigger is a widget that starts counting down from a specified number [startFrom], 5 | /// and decreases by 1 every time the child widget is pressed, the [onUpdated] callback is triggered each time the count decreases. 6 | /// When the count reaches 0, the [onFinished] callback is triggered and the count will reset back to [startFrom]. 7 | /// 8 | /// If there are no press events within [resetDurationInSec] seconds, 9 | /// the count will reset back to [startFrom]. 10 | class CountdownTrigger extends StatefulWidget { 11 | final Widget child; 12 | final int startFrom; 13 | final int resetDurationInSec; 14 | final void Function(int)? onUpdated; 15 | final VoidCallback? onFinished; 16 | const CountdownTrigger({ 17 | super.key, 18 | required this.startFrom, 19 | required this.child, 20 | this.resetDurationInSec = 3, 21 | this.onUpdated, 22 | this.onFinished, 23 | }) : assert(startFrom > 0), 24 | assert(resetDurationInSec > 0); 25 | 26 | @override 27 | State createState() => _CountdownTriggerState(); 28 | } 29 | 30 | class _CountdownTriggerState extends State { 31 | Timer? timer; 32 | late int count; 33 | 34 | @override 35 | void initState() { 36 | super.initState(); 37 | count = widget.startFrom; 38 | } 39 | 40 | @override 41 | void dispose() { 42 | timer?.cancel(); 43 | super.dispose(); 44 | } 45 | 46 | @override 47 | Widget build(BuildContext context) { 48 | return GestureDetector( 49 | onTap: () { 50 | timer?.cancel(); 51 | timer = Timer( 52 | Duration(seconds: widget.resetDurationInSec), 53 | () => count = widget.startFrom, 54 | ); 55 | count--; 56 | if (count > 0) { 57 | widget.onUpdated?.call(count); 58 | } else { 59 | timer?.cancel(); 60 | count = widget.startFrom; 61 | widget.onFinished?.call(); 62 | } 63 | }, 64 | child: widget.child, 65 | ); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/my_app/lib/features/authentication/data/auth_repository.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 4 | import 'package:flutterprint/features/authentication/authentication.dart'; 5 | import 'package:flutterprint/features/authentication/data/fake_auth_repository.dart'; 6 | import 'package:flutterprint/features/backend_environment/backend_environment.dart'; 7 | import 'package:flutterprint/services/rest_api_service/rest_api_service.dart'; 8 | 9 | final authRepoProvider = Provider((ref) { 10 | return const FakeAuthRepository(); 11 | // TODO: Replace the repository with AuthRepositoryImpl for real implementation 12 | // final restApiService = ref.watch(restApiServiceProvider); 13 | // return AuthRepositoryImpl(restApiService, ref); 14 | }); 15 | 16 | abstract class AuthRepository { 17 | Future> signIn({ 18 | required String username, 19 | required String password, 20 | }); 21 | 22 | Future> refreshToken(); 23 | } 24 | 25 | class AuthRepositoryImpl implements AuthRepository { 26 | const AuthRepositoryImpl(this.restApiService, this.ref); 27 | 28 | final RestApiService restApiService; 29 | final Ref ref; 30 | 31 | @override 32 | Future> signIn({ 33 | required String username, 34 | required String password, 35 | }) => 36 | restApiService.post( 37 | url: ref.read(hostControllerProvider).getUri(Endpoint.signIn), 38 | tokenType: TokenType.none, 39 | body: jsonEncode({ 40 | 'username': username, 41 | 'password': password, 42 | }), 43 | dataParser: _authInfoParser, 44 | ); 45 | 46 | @override 47 | Future> refreshToken() => restApiService.put( 48 | url: ref.read(hostControllerProvider).getUri(Endpoint.refreshToken), 49 | tokenType: TokenType.account, 50 | dataParser: _authInfoParser, 51 | ); 52 | 53 | AuthInfo _authInfoParser(String body) { 54 | final jsonResponse = jsonDecode(body); 55 | return AuthInfo.fromJson(jsonResponse as Map); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/my_app/lib/l10n/arb/app_en.arb: -------------------------------------------------------------------------------- 1 | { 2 | "@@locale": "en", 3 | "account": "Account", 4 | "@account": { 5 | "description": "Text shown in the title of account text field" 6 | }, 7 | "password": "Password", 8 | "@password": { 9 | "description": "Text shown in the title of password text field" 10 | }, 11 | "signinAppBarTitle": "Sign In", 12 | "@signinAppBarTitle": { 13 | "description": "Title of the sign in page" 14 | }, 15 | 16 | "pageNotFound": "404 - Page not found", 17 | 18 | "homePageAppBarTitle": "Home", 19 | "signOut": "Sign out", 20 | "staySignedin": "Keep me signed in", 21 | "submit": "Submit", 22 | "authenticating": "Authenticating...", 23 | "accountCanNotBeEmpty": "Account can't be empty", 24 | "passwordCanNotBeEmpty": "Password can't be empty", 25 | "incorrectAccountOrPassword": "Account or password is incorrect", 26 | 27 | "apiAgreementConflict": "API agreement conflict", 28 | "badRequest": "Bad request", 29 | "forbidden": "Permission denied", 30 | "socket": "Socket error", 31 | "notFound": "Not found", 32 | "serviceUnavailable": "Service unavailable", 33 | "timeout": "Request timeout", 34 | "unauthorized": "Unauthorized", 35 | "unknown": "An error occurred with status code: {statusCode}", 36 | "@unknown": { 37 | "placeholders": { 38 | "statusCode": { 39 | "type": "int" 40 | } 41 | } 42 | }, 43 | 44 | "backendEnvEnableText": "Press {value} more times to access the backend environment settings page", 45 | "@backendEnvEnableText": { 46 | "placeholders": { 47 | "value": { 48 | "type": "int" 49 | } 50 | } 51 | }, 52 | "backendEnvAppBarTitle": "Backend Environment", 53 | "backendEnvRelease": "Release", 54 | "backendEnvDemo": "Demo", 55 | "backendEnvDevelop": "Develop", 56 | "backendEnvLocal": "Local", 57 | "backendEnvLocalDescription": "Enter the local IP address followed by a port number.\nFor example, you can access the host at http://192.168.0.1:8000 by entering 192.168.0.1:8000", 58 | "save": "Save", 59 | 60 | "incorrectInputFormat": "Incorrect input format" 61 | } -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/lib/features/authentication/data/auth_repository.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 4 | import 'package:{{project_name.snakeCase()}}/features/authentication/authentication.dart'; 5 | import 'package:{{project_name.snakeCase()}}/features/authentication/data/fake_auth_repository.dart'; 6 | import 'package:{{project_name.snakeCase()}}/features/backend_environment/backend_environment.dart'; 7 | import 'package:{{project_name.snakeCase()}}/services/rest_api_service/rest_api_service.dart'; 8 | 9 | final authRepoProvider = Provider((ref) { 10 | return const FakeAuthRepository(); 11 | // TODO: Replace the repository with AuthRepositoryImpl for real implementation 12 | // final restApiService = ref.watch(restApiServiceProvider); 13 | // return AuthRepositoryImpl(restApiService, ref); 14 | }); 15 | 16 | abstract class AuthRepository { 17 | Future> signIn({ 18 | required String username, 19 | required String password, 20 | }); 21 | 22 | Future> refreshToken(); 23 | } 24 | 25 | class AuthRepositoryImpl implements AuthRepository { 26 | const AuthRepositoryImpl(this.restApiService, this.ref); 27 | 28 | final RestApiService restApiService; 29 | final Ref ref; 30 | 31 | @override 32 | Future> signIn({ 33 | required String username, 34 | required String password, 35 | }) => 36 | restApiService.post( 37 | url: ref.read(hostControllerProvider).getUri(Endpoint.signIn), 38 | tokenType: TokenType.none, 39 | body: jsonEncode({ 40 | 'username': username, 41 | 'password': password, 42 | }), 43 | dataParser: _authInfoParser, 44 | ); 45 | 46 | @override 47 | Future> refreshToken() => restApiService.put( 48 | url: ref.read(hostControllerProvider).getUri(Endpoint.refreshToken), 49 | tokenType: TokenType.account, 50 | dataParser: _authInfoParser, 51 | ); 52 | 53 | AuthInfo _authInfoParser(String body) { 54 | final jsonResponse = jsonDecode(body); 55 | return AuthInfo.fromJson(jsonResponse as Map); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/lib/l10n/arb/app_en.arb: -------------------------------------------------------------------------------- 1 | { 2 | "@@locale": "en", 3 | "account": "Account", 4 | "@account": { 5 | "description": "Text shown in the title of account text field" 6 | }, 7 | "password": "Password", 8 | "@password": { 9 | "description": "Text shown in the title of password text field" 10 | }, 11 | "signinAppBarTitle": "Sign In", 12 | "@signinAppBarTitle": { 13 | "description": "Title of the sign in page" 14 | }, 15 | 16 | "pageNotFound": "404 - Page not found", 17 | 18 | "homePageAppBarTitle": "Home", 19 | "signOut": "Sign out", 20 | "staySignedin": "Keep me signed in", 21 | "submit": "Submit", 22 | "authenticating": "Authenticating...", 23 | "accountCanNotBeEmpty": "Account can't be empty", 24 | "passwordCanNotBeEmpty": "Password can't be empty", 25 | "incorrectAccountOrPassword": "Account or password is incorrect", 26 | 27 | "apiAgreementConflict": "API agreement conflict", 28 | "badRequest": "Bad request", 29 | "forbidden": "Permission denied", 30 | "socket": "Socket error", 31 | "notFound": "Not found", 32 | "serviceUnavailable": "Service unavailable", 33 | "timeout": "Request timeout", 34 | "unauthorized": "Unauthorized", 35 | "unknown": "An error occurred with status code: {statusCode}", 36 | "@unknown": { 37 | "placeholders": { 38 | "statusCode": { 39 | "type": "int" 40 | } 41 | } 42 | }, 43 | 44 | "backendEnvEnableText": "Press {value} more times to access the backend environment settings page", 45 | "@backendEnvEnableText": { 46 | "placeholders": { 47 | "value": { 48 | "type": "int" 49 | } 50 | } 51 | }, 52 | "backendEnvAppBarTitle": "Backend Environment", 53 | "backendEnvRelease": "Release", 54 | "backendEnvDemo": "Demo", 55 | "backendEnvDevelop": "Develop", 56 | "backendEnvLocal": "Local", 57 | "backendEnvLocalDescription": "Enter the local IP address followed by a port number.\nFor example, you can access the host at http://192.168.0.1:8000 by entering 192.168.0.1:8000", 58 | "save": "Save", 59 | 60 | "incorrectInputFormat": "Incorrect input format" 61 | } -------------------------------------------------------------------------------- /src/my_app/test/features/backend_environment/backend_robot.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:flutterprint/features/backend_environment/domain/backend_env.dart'; 3 | import 'package:flutterprint/features/backend_environment/presentation/backend_environment_setting_page.dart'; 4 | import 'package:flutterprint/widgets/countdown_trigger.dart'; 5 | import 'package:flutter_test/flutter_test.dart'; 6 | 7 | class BackendRobot { 8 | const BackendRobot(this.tester); 9 | final WidgetTester tester; 10 | 11 | void expectBannerFoundWithName(String bannerName) { 12 | final banner = find.byKey(Key(bannerName)); 13 | expect(banner, findsOneWidget); 14 | } 15 | 16 | void expectBannerNotFound() { 17 | final banner = find.byType(Banner); 18 | expect(banner, findsNothing); 19 | } 20 | 21 | Future tapCountdownTriggerNTimes(int times) async { 22 | final countdownTriggerWidget = find.byType(CountdownTrigger); 23 | expect(countdownTriggerWidget, findsOneWidget); 24 | 25 | for (int i = 0; i < times; ++i) { 26 | await tester.tap(countdownTriggerWidget); 27 | await tester.pump(); 28 | } 29 | } 30 | 31 | Future tapReleaseRadio() => _tapRadio(BackendEnv.release); 32 | 33 | Future tapDemoRadio() => _tapRadio(BackendEnv.demo); 34 | 35 | Future tapDevelopRadio() => _tapRadio(BackendEnv.develop); 36 | 37 | Future _tapRadio(BackendEnv env) async { 38 | final radio = find.byKey(Key(env.name)); 39 | await tester.tap(radio); 40 | await tester.pumpAndSettle(); 41 | } 42 | 43 | Future tapLocalHostRadio() async { 44 | final radio = find.byKey(BackendEnvironmentSettingPage.localHostRadioKey); 45 | await tester.tap(radio); 46 | await tester.pumpAndSettle(); 47 | } 48 | 49 | Future enterLocalHost(String value) async { 50 | final localHostTextField = 51 | find.byKey(BackendEnvironmentSettingPage.localHostTextFieldKey); 52 | expect(localHostTextField, findsOneWidget); 53 | await tester.enterText(localHostTextField, value); 54 | } 55 | 56 | Future tapSaveButton() async { 57 | final saveButton = find.byKey(BackendEnvironmentSettingPage.saveKey); 58 | await tester.tap(saveButton); 59 | await tester.pumpAndSettle(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/test/features/backend_environment/backend_robot.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:{{project_name.snakeCase()}}/features/backend_environment/domain/backend_env.dart'; 3 | import 'package:{{project_name.snakeCase()}}/features/backend_environment/presentation/backend_environment_setting_page.dart'; 4 | import 'package:{{project_name.snakeCase()}}/widgets/countdown_trigger.dart'; 5 | import 'package:flutter_test/flutter_test.dart'; 6 | 7 | class BackendRobot { 8 | const BackendRobot(this.tester); 9 | final WidgetTester tester; 10 | 11 | void expectBannerFoundWithName(String bannerName) { 12 | final banner = find.byKey(Key(bannerName)); 13 | expect(banner, findsOneWidget); 14 | } 15 | 16 | void expectBannerNotFound() { 17 | final banner = find.byType(Banner); 18 | expect(banner, findsNothing); 19 | } 20 | 21 | Future tapCountdownTriggerNTimes(int times) async { 22 | final countdownTriggerWidget = find.byType(CountdownTrigger); 23 | expect(countdownTriggerWidget, findsOneWidget); 24 | 25 | for (int i = 0; i < times; ++i) { 26 | await tester.tap(countdownTriggerWidget); 27 | await tester.pump(); 28 | } 29 | } 30 | 31 | Future tapReleaseRadio() => _tapRadio(BackendEnv.release); 32 | 33 | Future tapDemoRadio() => _tapRadio(BackendEnv.demo); 34 | 35 | Future tapDevelopRadio() => _tapRadio(BackendEnv.develop); 36 | 37 | Future _tapRadio(BackendEnv env) async { 38 | final radio = find.byKey(Key(env.name)); 39 | await tester.tap(radio); 40 | await tester.pumpAndSettle(); 41 | } 42 | 43 | Future tapLocalHostRadio() async { 44 | final radio = find.byKey(BackendEnvironmentSettingPage.localHostRadioKey); 45 | await tester.tap(radio); 46 | await tester.pumpAndSettle(); 47 | } 48 | 49 | Future enterLocalHost(String value) async { 50 | final localHostTextField = 51 | find.byKey(BackendEnvironmentSettingPage.localHostTextFieldKey); 52 | expect(localHostTextField, findsOneWidget); 53 | await tester.enterText(localHostTextField, value); 54 | } 55 | 56 | Future tapSaveButton() async { 57 | final saveButton = find.byKey(BackendEnvironmentSettingPage.saveKey); 58 | await tester.tap(saveButton); 59 | await tester.pumpAndSettle(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/my_app/lib/services/snackbar/app_snackbar_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 | import 'package:flutterprint/constant/app_sizes.dart'; 4 | import 'package:flutterprint/constant/theme/theme.dart'; 5 | 6 | final appSnackBarRepoProvider = Provider((ref) => AppSnackBarRepository(ref)); 7 | 8 | class AppSnackBarRepository { 9 | final Ref ref; 10 | 11 | const AppSnackBarRepository(this.ref); 12 | 13 | /// Clear snackbars based on the given [context] 14 | void clearSnackbars(BuildContext context) => 15 | ScaffoldMessenger.of(context).clearSnackBars(); 16 | 17 | /// Show a snackbar based on the given [context] 18 | void showSnackbar( 19 | BuildContext context, { 20 | required String message, 21 | SnackBarBehavior behavior = SnackBarBehavior.floating, 22 | bool overlayOther = true, 23 | Duration duration = const Duration(seconds: 3), 24 | }) { 25 | if (overlayOther) { 26 | ScaffoldMessenger.of(context).clearSnackBars(); 27 | } 28 | final colorScheme = context.colorScheme; 29 | ScaffoldMessenger.of(context).showSnackBar( 30 | SnackBar( 31 | margin: const EdgeInsets.all(Sizes.p32), 32 | padding: EdgeInsets.zero, 33 | content: Padding( 34 | padding: const EdgeInsets.symmetric( 35 | vertical: Sizes.p12, 36 | horizontal: Sizes.p16, 37 | ), 38 | child: Column( 39 | mainAxisSize: MainAxisSize.min, 40 | children: [ 41 | Container( 42 | padding: EdgeInsets.zero, 43 | margin: EdgeInsets.zero, 44 | alignment: Alignment.centerLeft, 45 | child: SizedBox( 46 | child: Align( 47 | alignment: Alignment.centerLeft, 48 | child: Text( 49 | message, 50 | style: Theme.of(context) 51 | .textTheme 52 | .titleMedium! 53 | .copyWith(color: colorScheme.surface), 54 | ), 55 | ), 56 | ), 57 | ), 58 | ], 59 | ), 60 | ), 61 | behavior: SnackBarBehavior.floating, 62 | duration: duration, 63 | ), 64 | ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/lib/services/snackbar/app_snackbar_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 | import 'package:{{project_name.snakeCase()}}/constant/app_sizes.dart'; 4 | import 'package:{{project_name.snakeCase()}}/constant/theme/theme.dart'; 5 | 6 | final appSnackBarRepoProvider = Provider((ref) => AppSnackBarRepository(ref)); 7 | 8 | class AppSnackBarRepository { 9 | final Ref ref; 10 | 11 | const AppSnackBarRepository(this.ref); 12 | 13 | /// Clear snackbars based on the given [context] 14 | void clearSnackbars(BuildContext context) => 15 | ScaffoldMessenger.of(context).clearSnackBars(); 16 | 17 | /// Show a snackbar based on the given [context] 18 | void showSnackbar( 19 | BuildContext context, { 20 | required String message, 21 | SnackBarBehavior behavior = SnackBarBehavior.floating, 22 | bool overlayOther = true, 23 | Duration duration = const Duration(seconds: 3), 24 | }) { 25 | if (overlayOther) { 26 | ScaffoldMessenger.of(context).clearSnackBars(); 27 | } 28 | final colorScheme = context.colorScheme; 29 | ScaffoldMessenger.of(context).showSnackBar( 30 | SnackBar( 31 | margin: const EdgeInsets.all(Sizes.p32), 32 | padding: EdgeInsets.zero, 33 | content: Padding( 34 | padding: const EdgeInsets.symmetric( 35 | vertical: Sizes.p12, 36 | horizontal: Sizes.p16, 37 | ), 38 | child: Column( 39 | mainAxisSize: MainAxisSize.min, 40 | children: [ 41 | Container( 42 | padding: EdgeInsets.zero, 43 | margin: EdgeInsets.zero, 44 | alignment: Alignment.centerLeft, 45 | child: SizedBox( 46 | child: Align( 47 | alignment: Alignment.centerLeft, 48 | child: Text( 49 | message, 50 | style: Theme.of(context) 51 | .textTheme 52 | .titleMedium! 53 | .copyWith(color: colorScheme.surface), 54 | ), 55 | ), 56 | ), 57 | ), 58 | ], 59 | ), 60 | ), 61 | behavior: SnackBarBehavior.floating, 62 | duration: duration, 63 | ), 64 | ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/my_app/lib/constant/theme/color_schemes.dart: -------------------------------------------------------------------------------- 1 | /// This file is generated from https://m3.material.io/theme-builder#/custom 2 | import 'package:flutter/material.dart'; 3 | 4 | const lightColorScheme = ColorScheme( 5 | brightness: Brightness.light, 6 | primary: Color(0xFF6750A4), 7 | onPrimary: Color(0xFFFFFFFF), 8 | primaryContainer: Color(0xFFEADDFF), 9 | onPrimaryContainer: Color(0xFF21005D), 10 | secondary: Color(0xFF625B71), 11 | onSecondary: Color(0xFFFFFFFF), 12 | secondaryContainer: Color(0xFFE8DEF8), 13 | onSecondaryContainer: Color(0xFF1D192B), 14 | tertiary: Color(0xFF7D5260), 15 | onTertiary: Color(0xFFFFFFFF), 16 | tertiaryContainer: Color(0xFFFFD8E4), 17 | onTertiaryContainer: Color(0xFF31111D), 18 | error: Color(0xFFB3261E), 19 | onError: Color(0xFFFFFFFF), 20 | errorContainer: Color(0xFFF9DEDC), 21 | onErrorContainer: Color(0xFF410E0B), 22 | outline: Color(0xFF79747E), 23 | background: Color(0xFFFFFBFE), 24 | onBackground: Color(0xFF1C1B1F), 25 | surface: Color(0xFFFFFBFE), 26 | onSurface: Color(0xFF1C1B1F), 27 | surfaceVariant: Color(0xFFE7E0EC), 28 | onSurfaceVariant: Color(0xFF49454F), 29 | inverseSurface: Color(0xFF313033), 30 | onInverseSurface: Color(0xFFF4EFF4), 31 | inversePrimary: Color(0xFFD0BCFF), 32 | shadow: Color(0xFF000000), 33 | surfaceTint: Color(0xFF6750A4), 34 | ); 35 | 36 | const darkColorScheme = ColorScheme( 37 | brightness: Brightness.dark, 38 | primary: Color(0xFFD0BCFF), 39 | onPrimary: Color(0xFF381E72), 40 | primaryContainer: Color(0xFF4F378B), 41 | onPrimaryContainer: Color(0xFFEADDFF), 42 | secondary: Color(0xFFCCC2DC), 43 | onSecondary: Color(0xFF332D41), 44 | secondaryContainer: Color(0xFF4A4458), 45 | onSecondaryContainer: Color(0xFFE8DEF8), 46 | tertiary: Color(0xFFEFB8C8), 47 | onTertiary: Color(0xFF492532), 48 | tertiaryContainer: Color(0xFF633B48), 49 | onTertiaryContainer: Color(0xFFFFD8E4), 50 | error: Color(0xFFF2B8B5), 51 | onError: Color(0xFF601410), 52 | errorContainer: Color(0xFF8C1D18), 53 | onErrorContainer: Color(0xFFF9DEDC), 54 | outline: Color(0xFF938F99), 55 | background: Color(0xFF1C1B1F), 56 | onBackground: Color(0xFFE6E1E5), 57 | surface: Color(0xFF1C1B1F), 58 | onSurface: Color(0xFFE6E1E5), 59 | surfaceVariant: Color(0xFF49454F), 60 | onSurfaceVariant: Color(0xFFCAC4D0), 61 | inverseSurface: Color(0xFFE6E1E5), 62 | onInverseSurface: Color(0xFF313033), 63 | inversePrimary: Color(0xFF6750A4), 64 | shadow: Color(0xFF000000), 65 | surfaceTint: Color(0xFFD0BCFF), 66 | ); 67 | -------------------------------------------------------------------------------- /src/my_app/ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/my_app/test/features/authentication/auth_flow_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutterprint/services/local_storage/data/shared_preference_repository.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | 4 | import '../../robot.dart'; 5 | 6 | void main() { 7 | group( 8 | '[AuthFlow]:', 9 | () { 10 | testWidgets( 11 | 'Verifies the sign in and sign out flow of the app by starting on the SignInPage, ' 12 | 'and verifying that entering and submitting the correct account and password redirects to the HomePage, ' 13 | 'and tapping the sign out button on the HomePage redirects back to the SignInPage', 14 | (tester) async { 15 | await tester.runAsync( 16 | () async { 17 | final r = Robot(tester); 18 | await r.pumpAppWidget(); 19 | 20 | // SignInPage should be present on app started 21 | r.authRobot.expectAccountAndPasswordFieldsFound(); 22 | await r.authRobot.enterAndSubmitAccountAndPassword(); 23 | 24 | // HomePage should be present after sign in 25 | r.authRobot.expectSignOutButtonFound(); 26 | await r.authRobot.tapSignOutButton(); 27 | 28 | // SignInPage should be present after sign out 29 | r.authRobot.expectAccountAndPasswordFieldsFound(); 30 | }, 31 | ); 32 | }, 33 | ); 34 | 35 | testWidgets( 36 | "Verifies the sign out flow of the app by starting on the HomePage if the 'stay signed in' value saved in shared preferences is true, " 37 | "and tapping the sign out button on the HomePage redirects back to the SignInPage", 38 | (tester) async { 39 | await tester.runAsync( 40 | () async { 41 | final r = Robot(tester); 42 | await r.pumpAppWidget( 43 | initialSharedPreferenceValues: { 44 | SharedPreferenceRepository.prefStaySignedInKey: true, 45 | }, 46 | ); 47 | // HomePage should be present on app started if 'stay signed in' value saved in shared preferences is true 48 | // upon successful authentication 49 | r.authRobot.expectSignOutButtonFound(); 50 | await r.authRobot.tapSignOutButton(); 51 | 52 | // SignInPage should be present after sign out 53 | r.authRobot.expectAccountAndPasswordFieldsFound(); 54 | }, 55 | ); 56 | }, 57 | ); 58 | }, 59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /src/my_app/lib/services/rest_api_service/data/rest_api_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 | import 'package:flutterprint/services/rest_api_service/rest_api_service.dart'; 4 | 5 | final restApiServiceProvider = Provider((ref) { 6 | final client = ref.read(httpClientProvider); 7 | final restApiService = RestApiServiceImpl(ref, httpClient: client); 8 | return restApiService; 9 | }); 10 | 11 | /// A service class that makes HTTP REST API requests 12 | /// and returns either an ApiFailure object or a successful result of generic type T 13 | abstract class RestApiService { 14 | /// Sends an HTTP GET request with the given headers to the given [url] 15 | /// 16 | /// {@macro rest_api_request_helper} 17 | Future> get({ 18 | required Uri url, 19 | required T Function(String) dataParser, 20 | Map? customHeaders, 21 | TokenType tokenType = TokenType.account, 22 | }); 23 | 24 | /// Sends an HTTP POST request with the given [body] and headers to the given [url] 25 | /// 26 | /// {@macro rest_api_request_helper} 27 | Future> post({ 28 | required Uri url, 29 | required T Function(String) dataParser, 30 | Map? customHeaders, 31 | TokenType tokenType = TokenType.account, 32 | required Object body, 33 | }); 34 | 35 | /// Sends an HTTP PATCH request with the given [body] and headers to the given [url] 36 | /// 37 | /// {@macro rest_api_request_helper} 38 | Future> patch({ 39 | required Uri url, 40 | required T Function(String) dataParser, 41 | Map? customHeaders, 42 | TokenType tokenType = TokenType.account, 43 | Object? body, 44 | }); 45 | 46 | /// Sends an HTTP PUT request with the given [body] and headers to the given [url] 47 | /// 48 | /// {@macro rest_api_request_helper} 49 | Future> put({ 50 | required Uri url, 51 | required T Function(String) dataParser, 52 | Map? customHeaders, 53 | TokenType tokenType = TokenType.account, 54 | Object? body, 55 | }); 56 | 57 | /// Sends an HTTP DELETE request with the given headers to the given [url] 58 | /// 59 | /// {@macro rest_api_request_helper} 60 | Future> delete({ 61 | required Uri url, 62 | required T Function(String) dataParser, 63 | Map? customHeaders, 64 | TokenType tokenType = TokenType.account, 65 | }); 66 | } 67 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/lib/constant/theme/color_schemes.dart: -------------------------------------------------------------------------------- 1 | /// This file is generated from https://m3.material.io/theme-builder#/custom 2 | import 'package:flutter/material.dart'; 3 | 4 | const lightColorScheme = ColorScheme( 5 | brightness: Brightness.light, 6 | primary: Color(0xFF6750A4), 7 | onPrimary: Color(0xFFFFFFFF), 8 | primaryContainer: Color(0xFFEADDFF), 9 | onPrimaryContainer: Color(0xFF21005D), 10 | secondary: Color(0xFF625B71), 11 | onSecondary: Color(0xFFFFFFFF), 12 | secondaryContainer: Color(0xFFE8DEF8), 13 | onSecondaryContainer: Color(0xFF1D192B), 14 | tertiary: Color(0xFF7D5260), 15 | onTertiary: Color(0xFFFFFFFF), 16 | tertiaryContainer: Color(0xFFFFD8E4), 17 | onTertiaryContainer: Color(0xFF31111D), 18 | error: Color(0xFFB3261E), 19 | onError: Color(0xFFFFFFFF), 20 | errorContainer: Color(0xFFF9DEDC), 21 | onErrorContainer: Color(0xFF410E0B), 22 | outline: Color(0xFF79747E), 23 | background: Color(0xFFFFFBFE), 24 | onBackground: Color(0xFF1C1B1F), 25 | surface: Color(0xFFFFFBFE), 26 | onSurface: Color(0xFF1C1B1F), 27 | surfaceVariant: Color(0xFFE7E0EC), 28 | onSurfaceVariant: Color(0xFF49454F), 29 | inverseSurface: Color(0xFF313033), 30 | onInverseSurface: Color(0xFFF4EFF4), 31 | inversePrimary: Color(0xFFD0BCFF), 32 | shadow: Color(0xFF000000), 33 | surfaceTint: Color(0xFF6750A4), 34 | ); 35 | 36 | const darkColorScheme = ColorScheme( 37 | brightness: Brightness.dark, 38 | primary: Color(0xFFD0BCFF), 39 | onPrimary: Color(0xFF381E72), 40 | primaryContainer: Color(0xFF4F378B), 41 | onPrimaryContainer: Color(0xFFEADDFF), 42 | secondary: Color(0xFFCCC2DC), 43 | onSecondary: Color(0xFF332D41), 44 | secondaryContainer: Color(0xFF4A4458), 45 | onSecondaryContainer: Color(0xFFE8DEF8), 46 | tertiary: Color(0xFFEFB8C8), 47 | onTertiary: Color(0xFF492532), 48 | tertiaryContainer: Color(0xFF633B48), 49 | onTertiaryContainer: Color(0xFFFFD8E4), 50 | error: Color(0xFFF2B8B5), 51 | onError: Color(0xFF601410), 52 | errorContainer: Color(0xFF8C1D18), 53 | onErrorContainer: Color(0xFFF9DEDC), 54 | outline: Color(0xFF938F99), 55 | background: Color(0xFF1C1B1F), 56 | onBackground: Color(0xFFE6E1E5), 57 | surface: Color(0xFF1C1B1F), 58 | onSurface: Color(0xFFE6E1E5), 59 | surfaceVariant: Color(0xFF49454F), 60 | onSurfaceVariant: Color(0xFFCAC4D0), 61 | inverseSurface: Color(0xFFE6E1E5), 62 | onInverseSurface: Color(0xFF313033), 63 | inversePrimary: Color(0xFF6750A4), 64 | shadow: Color(0xFF000000), 65 | surfaceTint: Color(0xFFD0BCFF), 66 | ); 67 | -------------------------------------------------------------------------------- /brick/hooks/post_gen.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:mason/mason.dart'; 4 | 5 | void run(HookContext context) async { 6 | final logger = context.logger; 7 | final directory = Directory.current.path; 8 | final projectName = context.vars['project_name'] as String; 9 | late List folders; 10 | 11 | if (Platform.isWindows) { 12 | folders = directory.split(r'\').toList(); 13 | } else { 14 | folders = directory.split('/').toList(); 15 | } 16 | 17 | final projectRoot = [...folders, projectName.snakeCase].join('/'); 18 | 19 | final isFlutterInstalled = await Flutter.installed( 20 | logger: logger, 21 | workingDirectory: projectRoot, 22 | ); 23 | 24 | if (isFlutterInstalled) { 25 | await Flutter.pubGet( 26 | logger: logger, 27 | workingDirectory: projectRoot, 28 | ); 29 | } 30 | 31 | logger.success('${context.vars['project_name']} generated successfully 🎉'); 32 | } 33 | 34 | class Flutter { 35 | static Future installed({ 36 | String? workingDirectory, 37 | required Logger logger, 38 | }) async { 39 | final progress = logger.progress('Checking if flutter is installed'); 40 | var isFlutterInstalled = false; 41 | try { 42 | logger.detail('Running: flutter --version'); 43 | final result = await Process.run( 44 | 'flutter', 45 | ['--version'], 46 | workingDirectory: workingDirectory, 47 | runInShell: true, 48 | ); 49 | logger 50 | ..detail('stdout:\n${result.stdout}') 51 | ..detail('stderr:\n${result.stderr}'); 52 | isFlutterInstalled = true; 53 | } catch (_) { 54 | isFlutterInstalled = false; 55 | } finally { 56 | progress.complete(); 57 | if (!isFlutterInstalled) { 58 | logger.alert('Flutter is not installed'); 59 | } 60 | return isFlutterInstalled; 61 | } 62 | } 63 | 64 | static Future pubGet({ 65 | String? workingDirectory, 66 | required Logger logger, 67 | }) async { 68 | final progress = 69 | logger.progress('Running flutter pub get in $workingDirectory'); 70 | try { 71 | final result = await Process.run( 72 | 'flutter', 73 | ['pub', 'get'], 74 | workingDirectory: workingDirectory, 75 | runInShell: true, 76 | ); 77 | logger 78 | ..detail('stdout:\n${result.stdout}') 79 | ..detail('stderr:\n${result.stderr}'); 80 | progress.complete(); 81 | } catch (_) { 82 | logger.alert('Running flutter pub get failed'); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/test/features/authentication/auth_flow_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:{{project_name.snakeCase()}}/services/local_storage/data/shared_preference_repository.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | 4 | import '../../robot.dart'; 5 | 6 | void main() { 7 | group( 8 | '[AuthFlow]:', 9 | () { 10 | testWidgets( 11 | 'Verifies the sign in and sign out flow of the app by starting on the SignInPage, ' 12 | 'and verifying that entering and submitting the correct account and password redirects to the HomePage, ' 13 | 'and tapping the sign out button on the HomePage redirects back to the SignInPage', 14 | (tester) async { 15 | await tester.runAsync( 16 | () async { 17 | final r = Robot(tester); 18 | await r.pumpAppWidget(); 19 | 20 | // SignInPage should be present on app started 21 | r.authRobot.expectAccountAndPasswordFieldsFound(); 22 | await r.authRobot.enterAndSubmitAccountAndPassword(); 23 | 24 | // HomePage should be present after sign in 25 | r.authRobot.expectSignOutButtonFound(); 26 | await r.authRobot.tapSignOutButton(); 27 | 28 | // SignInPage should be present after sign out 29 | r.authRobot.expectAccountAndPasswordFieldsFound(); 30 | }, 31 | ); 32 | }, 33 | ); 34 | 35 | testWidgets( 36 | "Verifies the sign out flow of the app by starting on the HomePage if the 'stay signed in' value saved in shared preferences is true, " 37 | "and tapping the sign out button on the HomePage redirects back to the SignInPage", 38 | (tester) async { 39 | await tester.runAsync( 40 | () async { 41 | final r = Robot(tester); 42 | await r.pumpAppWidget( 43 | initialSharedPreferenceValues: { 44 | SharedPreferenceRepository.prefStaySignedInKey: true, 45 | }, 46 | ); 47 | // HomePage should be present on app started if 'stay signed in' value saved in shared preferences is true 48 | // upon successful authentication 49 | r.authRobot.expectSignOutButtonFound(); 50 | await r.authRobot.tapSignOutButton(); 51 | 52 | // SignInPage should be present after sign out 53 | r.authRobot.expectAccountAndPasswordFieldsFound(); 54 | }, 55 | ); 56 | }, 57 | ); 58 | }, 59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /brick/__brick__/{{project_name.snakeCase()}}/lib/services/rest_api_service/data/rest_api_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 | import 'package:{{project_name.snakeCase()}}/services/rest_api_service/rest_api_service.dart'; 4 | 5 | final restApiServiceProvider = Provider((ref) { 6 | final client = ref.read(httpClientProvider); 7 | final restApiService = RestApiServiceImpl(ref, httpClient: client); 8 | return restApiService; 9 | }); 10 | 11 | /// A service class that makes HTTP REST API requests 12 | /// and returns either an ApiFailure object or a successful result of generic type T 13 | abstract class RestApiService { 14 | /// Sends an HTTP GET request with the given headers to the given [url] 15 | /// 16 | /// {@macro rest_api_request_helper} 17 | Future> get({ 18 | required Uri url, 19 | required T Function(String) dataParser, 20 | Map? customHeaders, 21 | TokenType tokenType = TokenType.account, 22 | }); 23 | 24 | /// Sends an HTTP POST request with the given [body] and headers to the given [url] 25 | /// 26 | /// {@macro rest_api_request_helper} 27 | Future> post({ 28 | required Uri url, 29 | required T Function(String) dataParser, 30 | Map? customHeaders, 31 | TokenType tokenType = TokenType.account, 32 | required Object body, 33 | }); 34 | 35 | /// Sends an HTTP PATCH request with the given [body] and headers to the given [url] 36 | /// 37 | /// {@macro rest_api_request_helper} 38 | Future> patch({ 39 | required Uri url, 40 | required T Function(String) dataParser, 41 | Map? customHeaders, 42 | TokenType tokenType = TokenType.account, 43 | Object? body, 44 | }); 45 | 46 | /// Sends an HTTP PUT request with the given [body] and headers to the given [url] 47 | /// 48 | /// {@macro rest_api_request_helper} 49 | Future> put({ 50 | required Uri url, 51 | required T Function(String) dataParser, 52 | Map? customHeaders, 53 | TokenType tokenType = TokenType.account, 54 | Object? body, 55 | }); 56 | 57 | /// Sends an HTTP DELETE request with the given headers to the given [url] 58 | /// 59 | /// {@macro rest_api_request_helper} 60 | Future> delete({ 61 | required Uri url, 62 | required T Function(String) dataParser, 63 | Map? customHeaders, 64 | TokenType tokenType = TokenType.account, 65 | }); 66 | } 67 | --------------------------------------------------------------------------------