diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..768f62ef --- /dev/null +++ b/.env.example @@ -0,0 +1,8 @@ +# You can get these keys from https://www.google.com/recaptcha/admin +RECAPTCHA_PUBLIC_KEY=your_public_key +RECAPTCHA_SECRET_KEY=your_secret_key + +# Create your api key from AWS SES / API Gateway: +# - https://aws.amazon.com/ses/; +# - https://aws.amazon.com/api-gateway/ +API_SEND_MAIL=your_api_send_mail diff --git a/.github/docs/WANTTODO.md b/.github/docs/WANTTODO.md index 3e675136..f050d3ec 100644 --- a/.github/docs/WANTTODO.md +++ b/.github/docs/WANTTODO.md @@ -23,8 +23,9 @@ First, Follow the [🤔 How to Use](./README.md#-how-to-use) steps. ## Contact Form - Related to Contact Form: - - Create your account inside `emailjs` and make your changes. - - You can see [this video](https://www.youtube.com/watch?v=9HW3MZ_tsdo), to help you on practice. + - Check my api [here](https://github.com/felipecastrosales/site-api) and make your changes. + - You need configurate your AWS SES, and put your credentials inside the `.env` file. + - After that, configure AWS Lambda and API Gateway, and deploy your API. ## Firebase diff --git a/.gitignore b/.gitignore index f5ee8928..84c66e62 100644 --- a/.gitignore +++ b/.gitignore @@ -76,3 +76,7 @@ firebase.json firebase-config.js .firebase package.json + +# Environment configuration +.env +lib/infra/env/env.g.dart \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..cf00943a --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,26 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "site", + "request": "launch", + "type": "dart", + "flutterMode": "debug" + }, + { + "name": "site (profile mode)", + "request": "launch", + "type": "dart", + "flutterMode": "profile" + }, + { + "name": "site (release mode)", + "request": "launch", + "type": "dart", + "flutterMode": "release" + } + ] +} \ No newline at end of file diff --git a/LICENSE b/LICENSE index 7fb594b1..a6a47875 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2022-2023 Felipe Sales +Copyright (c) 2022-2024 Felipe Sales Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index dc28bb00..94919f39 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ With a single codebase, you can access this example from mobile, web and even desktop. -I'm sure this will be one of the best examples of the Flutter Web project in a completely open-source way and with the amout of features that exist. +I'm sure this will be one of the best examples of the Flutter Web project in a completely open-source way and with the amount of features that exist. --- @@ -64,7 +64,7 @@ I'm sure this will be one of the best examples of the Flutter Web project in a c - All of them using [`mocktail`](https://pub.dev/packages/mocktail). - Internationalization: - With support to 3 languages: English, Portuguese and Spanish; -- Feature to send an email to the user using [`emailjs API`](https://www.emailjs.com/); +- Feature to send an email to the user using [`AWS SES`](https://aws.amazon.com/ses/), [`AWS Lambda`](https://aws.amazon.com/lambda/) and [`AWS API Gateway`](https://aws.amazon.com/api-gateway/); - Settings: - Firebase Hosting; - Google Domains; @@ -88,7 +88,7 @@ Challenges are always an opportunity for growth, and in this project that became One thing I realized was that Flutter Web still has a lot to evolve, but id does very well it proposes ([see here](https://docs.flutter.dev/development/platform-integration/web/faq#what-scenarios-are-ideal-for-flutter-on-the-web)). One of the main ones that still annoys me a little is the loading and rendering speed of the elements (there are some pre-load strategies, but that could be better and clearer IMHO. -I feel that the project can evolve a lot, and for that reason I was very carefull with its development. For it to be simple, scalable and capable of any developer, of any level, being able to use and understand it. Also, I'll always be on the lookout for issues and PRs to improve it. 🚀 +I feel that the project can evolve a lot, and for that reason I was very carefully with its development. For it to be simple, scalable and capable of any developer, of any level, being able to use and understand it. Also, I'll always be on the lookout for issues and PRs to improve it. 🚀 This project took me out of my comfort zone, and I'm very happy with the result. Also, releasing it to the community did me a lot of good. Let's grow together, because the **Forge is Daily**. 🏆 diff --git a/analysis_options.yaml b/analysis_options.yaml index 01e5e464..9b00343c 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,13 +1,13 @@ include: package:flutter_lints/flutter.yaml analyzer: - plugins: - - dart_code_metrics +# plugins: +# - dart_code_metrics exclude: - - lib/generated_plugin_registrant.dart - - test/** + # - lib/generated_plugin_registrant.dart + # - test/** - lib/app/core/l10n/localizations/** # Generated file - - lib/data/services/firebase/firebase_set_defaults.dart # To use dynamic type (because really need it) + # - lib/data/services/firebase/firebase_set_defaults.dart # To use dynamic type (because really need it) linter: rules: @@ -28,28 +28,28 @@ linter: - prefer_final_fields - prefer_single_quotes -dart_code_metrics: - metrics: - cyclomatic-complexity: 20 - number-of-parameters: 4 - maximum-nesting-level: 5 - metrics-exclude: - - test/** - rules: - - avoid-dynamic - - avoid-redundant-async - - avoid-passing-async-when-sync-expected - - avoid-redundant-async - - avoid-unnecessary-type-assertions - - avoid-unnecessary-type-casts - - avoid-unrelated-type-assertions - - avoid-unused-parameters - - avoid-nested-conditional-expressions - - newline-before-return - - no-boolean-literal-compare - - no-empty-block - - prefer-trailing-comma - - prefer-conditional-expressions - - no-equal-then-else - - prefer-moving-to-variable - - prefer-match-file-name \ No newline at end of file +# dart_code_metrics: +# metrics: +# cyclomatic-complexity: 20 +# number-of-parameters: 4 +# maximum-nesting-level: 5 +# metrics-exclude: +# - test/** +# rules: +# - avoid-dynamic +# - avoid-redundant-async +# - avoid-passing-async-when-sync-expected +# - avoid-redundant-async +# - avoid-unnecessary-type-assertions +# - avoid-unnecessary-type-casts +# - avoid-unrelated-type-assertions +# - avoid-unused-parameters +# - avoid-nested-conditional-expressions +# - newline-before-return +# - no-boolean-literal-compare +# - no-empty-block +# - prefer-trailing-comma +# - prefer-conditional-expressions +# - no-equal-then-else +# - prefer-moving-to-variable +# - prefer-match-file-name \ No newline at end of file diff --git a/android/app/build.gradle b/android/app/build.gradle index de4b9e7a..0ffbef55 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -47,7 +47,7 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.felipecastrosales.site" - minSdkVersion 19 + minSdkVersion flutter.minSdkVersion targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/android/build.gradle b/android/build.gradle index 4e8eb2b8..ab6ac9ce 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.6.21' + ext.kotlin_version = '1.9.24' repositories { google() mavenCentral() @@ -29,6 +29,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/lib/app/app_widget.dart b/lib/app/app_widget.dart index 1aca53b8..983d7ba3 100644 --- a/lib/app/app_widget.dart +++ b/lib/app/app_widget.dart @@ -1,27 +1,28 @@ -import 'package:flutter/material.dart'; - import 'package:firebase_remote_config/firebase_remote_config.dart'; -import 'package:http/http.dart' as http; +import 'package:flutter/material.dart'; +import 'package:site/app/core/globals/globals.dart'; import 'package:site/app/core/injections/injections.dart'; import 'package:site/app/core/l10n/l10n.dart'; import 'package:site/app/core/themes/app_theme.dart'; +import 'package:site/app/features/contact/contact.dart'; import 'package:site/app/features/home/home_page.dart'; class AppWidget extends StatelessWidget { AppWidget({ super.key, FirebaseRemoteConfig? firebaseRemoteConfig, - http.Client? httpClient, + ContactCubit? contactCubit, }) : _firebaseRemoteConfig = firebaseRemoteConfig ?? getIt(), - _httpClient = httpClient ?? getIt(); + _contactCubit = contactCubit ?? getIt(); final FirebaseRemoteConfig _firebaseRemoteConfig; - final http.Client _httpClient; + final ContactCubit _contactCubit; @override Widget build(BuildContext context) { return MaterialApp( + navigatorKey: NavigationService.navigatorKey, onGenerateTitle: (context) => AppTexts.get(context).projectTitle, debugShowCheckedModeBanner: false, theme: AppTheme.theme, @@ -29,7 +30,7 @@ class AppWidget extends StatelessWidget { supportedLocales: AppLocalizations.supportedLocales, home: HomePage( firebaseRemoteConfig: _firebaseRemoteConfig, - httpClient: _httpClient, + contactCubit: _contactCubit, ), ); } diff --git a/lib/app/core/globals/globals.dart b/lib/app/core/globals/globals.dart new file mode 100644 index 00000000..a952f658 --- /dev/null +++ b/lib/app/core/globals/globals.dart @@ -0,0 +1 @@ +export 'navigation_service.dart'; diff --git a/lib/app/core/globals/navigation_service.dart b/lib/app/core/globals/navigation_service.dart new file mode 100644 index 00000000..1620c34b --- /dev/null +++ b/lib/app/core/globals/navigation_service.dart @@ -0,0 +1,6 @@ +import 'package:flutter/material.dart'; + +class NavigationService { + static final navigatorKey = GlobalKey(); + static final navigatorKeyContext = navigatorKey.currentContext; +} diff --git a/lib/app/core/injections/injections.dart b/lib/app/core/injections/injections.dart index a48d7dd0..506ffd88 100644 --- a/lib/app/core/injections/injections.dart +++ b/lib/app/core/injections/injections.dart @@ -1,16 +1,14 @@ +import 'package:dio/dio.dart'; import 'package:firebase_remote_config/firebase_remote_config.dart'; +import 'package:site/app/features/contact/contact.dart'; import 'package:get_it/get_it.dart'; -import 'package:http/http.dart' as http; - -import 'package:site/data/repositories/contact/contact.dart'; -import 'package:site/data/services/firebase/firebase.dart'; final getIt = GetIt.I; void configureDependencies() { - if (!getIt.isRegistered()) { - getIt.registerSingleton( - http.Client(), + if (!getIt.isRegistered()) { + getIt.registerSingleton( + Dio(), ); } if (!getIt.isRegistered()) { @@ -18,17 +16,18 @@ void configureDependencies() { FirebaseRemoteConfig.instance, ); } - if (!getIt.isRegistered()) { - getIt.registerSingleton( - FirebaseServiceImpl(), - ); - } if (!getIt.isRegistered()) { - getIt.registerSingleton( - ContactRepositoryImpl( - firebaseRemoteConfig: getIt(), + getIt.registerLazySingleton( + () => ContactRepositoryImpl( httpClient: getIt(), ), ); } + if (!getIt.isRegistered()) { + getIt.registerFactory( + () => ContactCubit( + contactRepository: getIt(), + ), + ); + } } diff --git a/lib/app/core/l10n/localizations/app_localizations.dart b/lib/app/core/l10n/localizations/app_localizations.dart index 74fd4342..cc27e5b2 100644 --- a/lib/app/core/l10n/localizations/app_localizations.dart +++ b/lib/app/core/l10n/localizations/app_localizations.dart @@ -329,6 +329,30 @@ abstract class AppLocalizations { /// **'E-mail enviado com sucesso!'** String get emailSendedWithSuccess; + /// No description provided for @emailNotSended. + /// + /// In pt, this message translates to: + /// **'Erro ao enviar e-mail!'** + String get emailNotSended; + + /// No description provided for @emailTooManyRequests. + /// + /// In pt, this message translates to: + /// **'Tente novamente mais tarde!'** + String get emailTooManyRequests; + + /// No description provided for @emailUnauthorized. + /// + /// In pt, this message translates to: + /// **'O envio não foi autorizado!'** + String get emailUnauthorized; + + /// No description provided for @emailUnknowError. + /// + /// In pt, this message translates to: + /// **'Tente enviar de outra forma!'** + String get emailUnknowError; + /// No description provided for @letsChatCallMe. /// /// In pt, this message translates to: diff --git a/lib/app/core/l10n/localizations/app_localizations_en.dart b/lib/app/core/l10n/localizations/app_localizations_en.dart index 896e5a51..c9147564 100644 --- a/lib/app/core/l10n/localizations/app_localizations_en.dart +++ b/lib/app/core/l10n/localizations/app_localizations_en.dart @@ -121,6 +121,18 @@ class AppLocalizationsEn extends AppLocalizations { @override String get emailSendedWithSuccess => 'Email sent successfully!'; + @override + String get emailNotSended => 'Error to send email'; + + @override + String get emailTooManyRequests => 'Please try again later!'; + + @override + String get emailUnauthorized => 'Sending was not authorized!'; + + @override + String get emailUnknowError => 'Try sending in another way!'; + @override String get letsChatCallMe => 'Let\'s chat, call me:'; diff --git a/lib/app/core/l10n/localizations/app_localizations_es.dart b/lib/app/core/l10n/localizations/app_localizations_es.dart index f3ab796d..2e83e805 100644 --- a/lib/app/core/l10n/localizations/app_localizations_es.dart +++ b/lib/app/core/l10n/localizations/app_localizations_es.dart @@ -121,6 +121,18 @@ class AppLocalizationsEs extends AppLocalizations { @override String get emailSendedWithSuccess => '¡Email enviado exitosamente!'; + @override + String get emailNotSended => '¡Error al enviar el email!'; + + @override + String get emailTooManyRequests => '¡Inténtalo de nuevo más tarde!'; + + @override + String get emailUnauthorized => '¡El envío no fue autorizado!'; + + @override + String get emailUnknowError => '¡Intenta enviar de otra manera!'; + @override String get letsChatCallMe => 'Hablemos, llámame:'; diff --git a/lib/app/core/l10n/localizations/app_localizations_pt.dart b/lib/app/core/l10n/localizations/app_localizations_pt.dart index 8852f899..b2dc1283 100644 --- a/lib/app/core/l10n/localizations/app_localizations_pt.dart +++ b/lib/app/core/l10n/localizations/app_localizations_pt.dart @@ -121,6 +121,18 @@ class AppLocalizationsPt extends AppLocalizations { @override String get emailSendedWithSuccess => 'E-mail enviado com sucesso!'; + @override + String get emailNotSended => 'Erro ao enviar e-mail!'; + + @override + String get emailTooManyRequests => 'Tente novamente mais tarde!'; + + @override + String get emailUnauthorized => 'O envio não foi autorizado!'; + + @override + String get emailUnknowError => 'Tente enviar de outra forma!'; + @override String get letsChatCallMe => 'Vamos bater um papo, me chame:'; diff --git a/lib/app/core/l10n/localizations/app_texts.dart b/lib/app/core/l10n/localizations/app_texts.dart index 1c71d1d5..1a489c6d 100644 --- a/lib/app/core/l10n/localizations/app_texts.dart +++ b/lib/app/core/l10n/localizations/app_texts.dart @@ -1,9 +1,26 @@ import 'package:flutter/material.dart'; - +import 'package:site/app/core/globals/globals.dart'; +import 'package:site/app/core/l10n/l10n.dart'; import 'package:site/app/core/l10n/l10n.dart'; class AppTexts { static AppLocalizations get(BuildContext context) { + if (context == null) { + throw Exception('AppTexts.get: context is null'); + } + return Localizations.of(context, AppLocalizations)!; } + + static AppLocalizations get getViaNavigatorKey { + final navigatorKey = NavigationService.navigatorKeyContext; + + if (navigatorKey == null) { + throw Exception( + 'AppTexts.getViaNavigatorKey: navigatorKeyContext is null', + ); + } + + return Localizations.of(navigatorKey!, AppLocalizations)!; + } } diff --git a/lib/app/core/l10n/templates/app_en.arb b/lib/app/core/l10n/templates/app_en.arb index 64ebbb8c..e6da626b 100644 --- a/lib/app/core/l10n/templates/app_en.arb +++ b/lib/app/core/l10n/templates/app_en.arb @@ -38,6 +38,10 @@ "text": "Text", "sendEmailUpper": "SEND EMAIL", "emailSendedWithSuccess": "Email sent successfully!", + "emailNotSended": "Error to send email", + "emailTooManyRequests": "Please try again later!", + "emailUnauthorized": "Sending was not authorized!", + "emailUnknowError": "Try sending in another way!", "letsChatCallMe": "Let's chat, call me:", "hyphen": " - ", "username": "@felipecastrosales", diff --git a/lib/app/core/l10n/templates/app_es.arb b/lib/app/core/l10n/templates/app_es.arb index ecb2e5fe..4ae24100 100644 --- a/lib/app/core/l10n/templates/app_es.arb +++ b/lib/app/core/l10n/templates/app_es.arb @@ -38,6 +38,10 @@ "text": "Texto", "sendEmailUpper": "ENVIAR EMAIL", "emailSendedWithSuccess": "¡Email enviado exitosamente!", + "emailNotSended": "¡Error al enviar el email!", + "emailTooManyRequests": "¡Inténtalo de nuevo más tarde!", + "emailUnauthorized": "¡El envío no fue autorizado!", + "emailUnknowError": "¡Intenta enviar de otra manera!", "letsChatCallMe": "Hablemos, llámame:", "hyphen": " - ", "username": "@felipecastrosales", diff --git a/lib/app/core/l10n/templates/app_pt.arb b/lib/app/core/l10n/templates/app_pt.arb index a690b99d..56645118 100644 --- a/lib/app/core/l10n/templates/app_pt.arb +++ b/lib/app/core/l10n/templates/app_pt.arb @@ -38,6 +38,10 @@ "text": "Texto", "sendEmailUpper": "ENVIAR E-MAIL", "emailSendedWithSuccess": "E-mail enviado com sucesso!", + "emailNotSended": "Erro ao enviar e-mail!", + "emailTooManyRequests": "Tente novamente mais tarde!", + "emailUnauthorized": "O envio não foi autorizado!", + "emailUnknowError": "Tente enviar de outra forma!", "letsChatCallMe": "Vamos bater um papo, me chame:", "hyphen": " - ", "username": "@felipecastrosales", diff --git a/lib/app/core/platform_info/widgets/platform_info_widget.dart b/lib/app/core/platform_info/widgets/platform_info_widget.dart index 104833a2..dc66a3c6 100644 --- a/lib/app/core/platform_info/widgets/platform_info_widget.dart +++ b/lib/app/core/platform_info/widgets/platform_info_widget.dart @@ -30,18 +30,11 @@ class _PlatformInfoWidgetState extends State { return FutureBuilder( future: packageInfo, builder: (context, snapshot) { - if ([ - ConnectionState.none, - ConnectionState.active, - ConnectionState.waiting, - ].contains(snapshot.connectionState)) { - return const CircularProgressIndicator(); - } - if (snapshot.hasData && snapshot.data != null) { final data = snapshot.data!; + final buildNumber = data.buildNumber; final text = - 'v${data.version}${data.buildNumber.isEmpty ? '' : '+${data.buildNumber}'}'; + 'v${data.version}${buildNumber.isEmpty ? '' : '+$buildNumber'}'; return Padding( padding: padding, diff --git a/lib/app/core/result/result.dart b/lib/app/core/result/result.dart new file mode 100644 index 00000000..c1291020 --- /dev/null +++ b/lib/app/core/result/result.dart @@ -0,0 +1,15 @@ +sealed class Result { + const Result(); +} + +final class Success extends Result { + const Success(this.object) : super(); + + final T object; +} + +final class Failure extends Result { + const Failure(this.error) : super(); + + final R error; +} diff --git a/lib/app/core/shared/app_values.dart b/lib/app/core/shared/app_values.dart new file mode 100644 index 00000000..855e7b74 --- /dev/null +++ b/lib/app/core/shared/app_values.dart @@ -0,0 +1,3 @@ +class AppValues { + static const invalidValue = -1; +} diff --git a/lib/app/core/shared/shared.dart b/lib/app/core/shared/shared.dart index 07d8ef82..9243f7a9 100644 --- a/lib/app/core/shared/shared.dart +++ b/lib/app/core/shared/shared.dart @@ -2,3 +2,4 @@ export 'app_assets.dart'; export 'app_datas.dart'; export 'app_keys.dart'; export 'app_urls.dart'; +export 'app_values.dart'; diff --git a/lib/app/core/tokens/app_colors.dart b/lib/app/core/tokens/app_colors.dart index b268bf70..b4802b8e 100644 --- a/lib/app/core/tokens/app_colors.dart +++ b/lib/app/core/tokens/app_colors.dart @@ -15,5 +15,6 @@ class AppColors { static const white = Color(0xffffffff); static const black = Color(0xff000000); static const blackOpacity = Color(0xff121212); + static const green = Color(0XFF00B894); static const transparent = Colors.transparent; } diff --git a/lib/app/features/contact/contact.dart b/lib/app/features/contact/contact.dart new file mode 100644 index 00000000..aeeb7088 --- /dev/null +++ b/lib/app/features/contact/contact.dart @@ -0,0 +1,3 @@ +export 'data/data.dart'; +export 'domain/domain.dart'; +export 'presentation/presentation.dart'; diff --git a/lib/app/features/contact/data/data.dart b/lib/app/features/contact/data/data.dart new file mode 100644 index 00000000..50664310 --- /dev/null +++ b/lib/app/features/contact/data/data.dart @@ -0,0 +1,2 @@ +export 'repositories/repositories.dart'; +export 'results/results.dart'; \ No newline at end of file diff --git a/lib/app/features/contact/data/repositories/contact_repository_impl.dart b/lib/app/features/contact/data/repositories/contact_repository_impl.dart new file mode 100644 index 00000000..c53d3657 --- /dev/null +++ b/lib/app/features/contact/data/repositories/contact_repository_impl.dart @@ -0,0 +1,53 @@ +import 'dart:developer'; +import 'dart:io'; + +import 'package:dio/dio.dart'; +import 'package:site/app/core/result/result.dart'; + +import 'package:site/app/features/contact/contact.dart'; +import 'package:site/data/constants/constants_api.dart'; + +class ContactRepositoryImpl implements ContactRepository { + ContactRepositoryImpl({ + required Dio httpClient, + }) : _httpClient = httpClient; + + final Dio _httpClient; + + @override + Future> sendMail({ + required ContactModel contact, + }) async { + try { + final response = await _httpClient.post( + ConstantsAPI.apiSendMail, + data: ContactUser.toJson(contact), + ); + + if (response.statusCode == HttpStatus.ok) { + return Success( + ContactAnswer.fromResponse( + contact: contact, + response: response, + ), + ); + } + + return const Failure(ContactFailedResult.unknown); + } on DioException catch (e, s) { + log('[Error]: DioException - ContactRepositoryImpl.sendMail', error: e, stackTrace: s); + + return switch (e.response?.statusCode) { + HttpStatus.unauthorized => + const Failure(ContactFailedResult.unauthorized), + HttpStatus.tooManyRequests => + const Failure(ContactFailedResult.tooManyRequests), + _ => const Failure(ContactFailedResult.unknown), + }; + } catch (e, s) { + log('[Error]: ContactRepositoryImpl.sendMail', error: e, stackTrace: s); + + return const Failure(ContactFailedResult.error); + } + } +} diff --git a/lib/data/repositories/contact/contact.dart b/lib/app/features/contact/data/repositories/repositories.dart similarity index 53% rename from lib/data/repositories/contact/contact.dart rename to lib/app/features/contact/data/repositories/repositories.dart index dd4eaebc..16b0082c 100644 --- a/lib/data/repositories/contact/contact.dart +++ b/lib/app/features/contact/data/repositories/repositories.dart @@ -1,2 +1 @@ -export 'contact_repository.dart'; export 'contact_repository_impl.dart'; diff --git a/lib/app/features/contact/data/results/contact_failed_result.dart b/lib/app/features/contact/data/results/contact_failed_result.dart new file mode 100644 index 00000000..c0f8c1ef --- /dev/null +++ b/lib/app/features/contact/data/results/contact_failed_result.dart @@ -0,0 +1,17 @@ +import 'package:site/app/core/l10n/l10n.dart'; + +enum ContactFailedResult { + tooManyRequests, + unauthorized, + unknown, + error; + + String message(AppLocalizations texts) { + return switch (this) { + ContactFailedResult.tooManyRequests => texts.emailTooManyRequests, + ContactFailedResult.unauthorized => texts.emailUnauthorized, + ContactFailedResult.unknown => texts.emailUnknowError, + ContactFailedResult.error => texts.emailNotSended, + }; + } +} diff --git a/lib/app/features/contact/data/results/results.dart b/lib/app/features/contact/data/results/results.dart new file mode 100644 index 00000000..5380ab94 --- /dev/null +++ b/lib/app/features/contact/data/results/results.dart @@ -0,0 +1 @@ +export 'contact_failed_result.dart'; diff --git a/lib/app/features/contact/domain/domain.dart b/lib/app/features/contact/domain/domain.dart new file mode 100644 index 00000000..b9314f19 --- /dev/null +++ b/lib/app/features/contact/domain/domain.dart @@ -0,0 +1,2 @@ +export 'models/models.dart'; +export 'repositories/repositories.dart'; diff --git a/lib/app/features/contact/domain/models/contact_answer.dart b/lib/app/features/contact/domain/models/contact_answer.dart new file mode 100644 index 00000000..cb599c3a --- /dev/null +++ b/lib/app/features/contact/domain/models/contact_answer.dart @@ -0,0 +1,31 @@ +import 'package:dio/dio.dart'; +import 'package:site/app/core/shared/shared.dart'; +import 'package:site/app/features/contact/domain/models/contact_model.dart'; + +class ContactAnswer extends ContactModel { + factory ContactAnswer.fromResponse({ + required ContactModel contact, + required Response response, + }) { + return ContactAnswer( + name: contact.name, + email: contact.email, + message: contact.message, + subject: contact.subject, + statusCode: response.statusCode ?? AppValues.invalidValue, + responseMessage: '${(response.data as Map)['message']}', + ); + } + + ContactAnswer({ + required super.name, + required super.email, + required super.message, + required super.subject, + required this.statusCode, + required this.responseMessage, + }); + + final int statusCode; + final String responseMessage; +} diff --git a/lib/data/models/contact.dart b/lib/app/features/contact/domain/models/contact_model.dart similarity index 77% rename from lib/data/models/contact.dart rename to lib/app/features/contact/domain/models/contact_model.dart index 07013d45..7246294e 100644 --- a/lib/data/models/contact.dart +++ b/lib/app/features/contact/domain/models/contact_model.dart @@ -1,5 +1,5 @@ -class Contact { - Contact({ +abstract class ContactModel { + ContactModel({ required this.name, required this.email, required this.message, diff --git a/lib/app/features/contact/domain/models/contact_user.dart b/lib/app/features/contact/domain/models/contact_user.dart new file mode 100644 index 00000000..0aa423dd --- /dev/null +++ b/lib/app/features/contact/domain/models/contact_user.dart @@ -0,0 +1,24 @@ +import 'dart:convert'; + +import 'package:site/app/features/contact/contact.dart'; + +class ContactUser extends ContactModel { + ContactUser({ + required super.name, + required super.email, + required super.message, + required super.subject, + }); + + static Map toJson(ContactModel contact) { + return { + 'sender_name': contact.name, + 'source_email': contact.email, + 'template_subject': contact.subject, + 'template_body': contact.message, + }; + } + + static String toJsonString(ContactModel contact) => + jsonEncode(toJson(contact)); +} diff --git a/lib/app/features/contact/domain/models/models.dart b/lib/app/features/contact/domain/models/models.dart new file mode 100644 index 00000000..d3da820f --- /dev/null +++ b/lib/app/features/contact/domain/models/models.dart @@ -0,0 +1,3 @@ +export 'contact_answer.dart'; +export 'contact_user.dart'; +export 'contact_model.dart'; diff --git a/lib/app/features/contact/domain/repositories/contact_repository.dart b/lib/app/features/contact/domain/repositories/contact_repository.dart new file mode 100644 index 00000000..c44b8218 --- /dev/null +++ b/lib/app/features/contact/domain/repositories/contact_repository.dart @@ -0,0 +1,8 @@ +import 'package:site/app/core/result/result.dart'; +import 'package:site/app/features/contact/contact.dart'; + +abstract class ContactRepository { + Future> sendMail({ + required ContactModel contact, + }); +} diff --git a/lib/app/features/contact/domain/repositories/repositories.dart b/lib/app/features/contact/domain/repositories/repositories.dart new file mode 100644 index 00000000..7f88437c --- /dev/null +++ b/lib/app/features/contact/domain/repositories/repositories.dart @@ -0,0 +1 @@ +export 'contact_repository.dart'; diff --git a/lib/app/features/contact/presentation/cubit/contact_cubit.dart b/lib/app/features/contact/presentation/cubit/contact_cubit.dart new file mode 100644 index 00000000..745c4ade --- /dev/null +++ b/lib/app/features/contact/presentation/cubit/contact_cubit.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:site/app/core/l10n/l10n.dart'; +import 'package:site/app/core/result/result.dart'; +import 'package:site/app/features/contact/contact.dart'; + +part 'contact_state.dart'; + +class ContactCubit extends Cubit { + ContactCubit({ + required ContactRepository contactRepository, + AppLocalizations? appLocalizations, + }) : _contactRepository = contactRepository, + _appLocalizations = appLocalizations, + super( + const ContactInitial(), + ); + + final ContactRepository _contactRepository; + final AppLocalizations? _appLocalizations; + + Future sendMail({ + required ContactModel contact, + }) async { + emit(const ContactLoading()); + + final result = await _contactRepository.sendMail(contact: contact); + + switch (result) { + case Success(object: final contactAnswer): + emit( + ContactSuccess( + contact: contactAnswer, + message: (_appLocalizations ?? AppTexts.getViaNavigatorKey) + .emailSendedWithSuccess, + ), + ); + case Failure(error: final contactFailedResult): + emit( + ContactError( + contact: contact, + message: contactFailedResult + .message(_appLocalizations ?? AppTexts.getViaNavigatorKey), + ), + ); + } + } +} diff --git a/lib/app/features/contact/presentation/cubit/contact_state.dart b/lib/app/features/contact/presentation/cubit/contact_state.dart new file mode 100644 index 00000000..20690a1c --- /dev/null +++ b/lib/app/features/contact/presentation/cubit/contact_state.dart @@ -0,0 +1,49 @@ +part of 'contact_cubit.dart'; + +@immutable +sealed class ContactState extends Equatable { + const ContactState(); + + @override + List get props => []; +} + +final class ContactInitial extends ContactState { + const ContactInitial(); + + @override + List get props => []; +} + +final class ContactLoading extends ContactState { + const ContactLoading(); + + @override + List get props => []; +} + +final class ContactSuccess extends ContactState { + const ContactSuccess({ + required this.contact, + required this.message, + }); + + final ContactModel contact; + final String message; + + @override + List get props => [contact, message]; +} + +final class ContactError extends ContactState { + const ContactError({ + required this.contact, + required this.message, + }); + + final ContactModel contact; + final String message; + + @override + List get props => [contact, message]; +} diff --git a/lib/app/features/contact/presentation/presentation.dart b/lib/app/features/contact/presentation/presentation.dart new file mode 100644 index 00000000..ac29eaf8 --- /dev/null +++ b/lib/app/features/contact/presentation/presentation.dart @@ -0,0 +1,2 @@ +export 'widgets/widgets.dart'; +export 'cubit/contact_cubit.dart'; \ No newline at end of file diff --git a/lib/app/features/home/widgets/contact/widgets/custom_form.dart b/lib/app/features/contact/presentation/widgets/form/custom_form.dart similarity index 97% rename from lib/app/features/home/widgets/contact/widgets/custom_form.dart rename to lib/app/features/contact/presentation/widgets/form/custom_form.dart index fa633d08..452e6029 100644 --- a/lib/app/features/home/widgets/contact/widgets/custom_form.dart +++ b/lib/app/features/contact/presentation/widgets/form/custom_form.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:site/app/core/l10n/l10n.dart'; -import 'package:site/app/features/home/widgets/contact/widgets/widgets.dart'; +import 'package:site/app/features/contact/contact.dart'; import 'package:site/app/utils/contact_validators.dart'; import 'package:site/app/widgets/buttons/buttons.dart'; import 'package:site/app/widgets/dividers/dividers.dart'; diff --git a/lib/app/features/home/widgets/contact/widgets/custom_text_form_field.dart b/lib/app/features/contact/presentation/widgets/form/custom_text_form_field.dart similarity index 100% rename from lib/app/features/home/widgets/contact/widgets/custom_text_form_field.dart rename to lib/app/features/contact/presentation/widgets/form/custom_text_form_field.dart diff --git a/lib/app/features/home/widgets/contact/widgets/widgets.dart b/lib/app/features/contact/presentation/widgets/form/form.dart similarity index 100% rename from lib/app/features/home/widgets/contact/widgets/widgets.dart rename to lib/app/features/contact/presentation/widgets/form/form.dart diff --git a/lib/app/features/home/widgets/contact/contact_mobile.dart b/lib/app/features/contact/presentation/widgets/ui/contact_mobile.dart similarity index 98% rename from lib/app/features/home/widgets/contact/contact_mobile.dart rename to lib/app/features/contact/presentation/widgets/ui/contact_mobile.dart index 38798deb..af946c28 100644 --- a/lib/app/features/home/widgets/contact/contact_mobile.dart +++ b/lib/app/features/contact/presentation/widgets/ui/contact_mobile.dart @@ -40,6 +40,7 @@ class ContactMobile extends StatelessWidget { ), ), SingleChildScrollViewWithoutScroll( + primary: false, child: Column( children: [ MobileBody( diff --git a/lib/app/features/home/widgets/contact/contact_web.dart b/lib/app/features/contact/presentation/widgets/ui/contact_web.dart similarity index 100% rename from lib/app/features/home/widgets/contact/contact_web.dart rename to lib/app/features/contact/presentation/widgets/ui/contact_web.dart diff --git a/lib/app/features/contact/presentation/widgets/ui/contact_widget.dart b/lib/app/features/contact/presentation/widgets/ui/contact_widget.dart new file mode 100644 index 00000000..5aea7aaf --- /dev/null +++ b/lib/app/features/contact/presentation/widgets/ui/contact_widget.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:site/app/core/responsive/responsive.dart'; +import 'package:site/app/core/shared/app_keys.dart'; +import 'package:site/app/features/contact/contact.dart'; +import 'package:site/app/widgets/snack_bars/snack_bars.dart'; + +class ContactWidget extends StatelessWidget { + const ContactWidget({ + super.key, + required this.contactCubit, + }); + + final ContactCubit contactCubit; + + @override + Widget build(BuildContext context) { + final formKey = GlobalKey(); + final nameController = TextEditingController(); + final emailController = TextEditingController(); + final messageController = TextEditingController(); + final subjectController = TextEditingController(); + + final form = BlocListener( + listener: (context, state) { + if ([ContactSuccess, ContactError].contains(state.runtimeType)) { + appShowSnackBarFromContact(context, state); + } + + if (state is ContactSuccess) { + for (var controller in [ + nameController, + emailController, + messageController, + subjectController, + ]) { + controller.clear(); + } + } + }, + child: CustomForm( + formKey: formKey, + nameController: nameController, + emailController: emailController, + subjectController: subjectController, + messageController: messageController, + onPressed: () { + if (formKey.currentState?.validate() ?? false) { + contactCubit.sendMail( + contact: ContactUser( + name: nameController.text, + email: emailController.text, + subject: subjectController.text, + message: messageController.text, + ), + ); + } + }, + ), + ); + + return BlocProvider.value( + value: contactCubit, + child: LayoutBuilder( + key: AppKeys.contact, + builder: (context, constraints) { + return constraints.maxWidth < Breakpoints.contact + ? ContactMobile(form) + : ContactWeb(form); + }, + ), + ); + } +} diff --git a/lib/app/features/contact/presentation/widgets/ui/ui.dart b/lib/app/features/contact/presentation/widgets/ui/ui.dart new file mode 100644 index 00000000..15ec8997 --- /dev/null +++ b/lib/app/features/contact/presentation/widgets/ui/ui.dart @@ -0,0 +1,3 @@ +export 'contact_mobile.dart'; +export 'contact_web.dart'; +export 'contact_widget.dart'; diff --git a/lib/app/features/contact/presentation/widgets/widgets.dart b/lib/app/features/contact/presentation/widgets/widgets.dart new file mode 100644 index 00000000..f3a2eb9c --- /dev/null +++ b/lib/app/features/contact/presentation/widgets/widgets.dart @@ -0,0 +1,2 @@ +export 'form/form.dart'; +export 'ui/ui.dart'; diff --git a/lib/app/features/home/home_page.dart b/lib/app/features/home/home_page.dart index 6cbc51c2..0e844079 100644 --- a/lib/app/features/home/home_page.dart +++ b/lib/app/features/home/home_page.dart @@ -1,13 +1,10 @@ -import 'package:flutter/material.dart'; - import 'package:firebase_remote_config/firebase_remote_config.dart'; -import 'package:http/http.dart' as http; +import 'package:flutter/material.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:site/app/core/injections/injections.dart'; import 'package:site/app/core/responsive/responsive.dart'; -import 'package:site/app/features/home/widgets/contact/contact_widget.dart'; -import 'package:site/app/features/home/widgets/contact/controller/contact_controller.dart'; +import 'package:site/app/features/contact/contact.dart'; import 'package:site/app/features/home/widgets/experience/experience.dart'; import 'package:site/app/features/home/widgets/footer/footer.dart'; import 'package:site/app/features/home/widgets/presentation/presentation.dart'; @@ -15,18 +12,19 @@ import 'package:site/app/features/home/widgets/projects/projects.dart'; import 'package:site/app/features/home/widgets/social/social.dart'; import 'package:site/app/widgets/app_bar/app_bar.dart'; import 'package:site/app/widgets/drawer/drawer.dart'; -import 'package:site/data/repositories/contact/contact.dart'; class HomePage extends StatefulWidget { HomePage({ super.key, FirebaseRemoteConfig? firebaseRemoteConfig, - http.Client? httpClient, + ContactCubit? contactCubit, }) : _firebaseRemoteConfig = firebaseRemoteConfig ?? getIt(), - _httpClient = httpClient ?? getIt(); + _contactCubit = contactCubit; + /// The [FirebaseRemoteConfig] instance is here to be used for future updates and configurations. + // ignore: unused_field final FirebaseRemoteConfig _firebaseRemoteConfig; - final http.Client _httpClient; + final ContactCubit? _contactCubit; @override State createState() => _HomePageState(); @@ -47,12 +45,7 @@ class _HomePageState extends State { const Experience(), const Social(), ContactWidget( - contactController: ContactController( - contactRepository: ContactRepositoryImpl( - firebaseRemoteConfig: widget._firebaseRemoteConfig, - httpClient: widget._httpClient, - ), - ), + contactCubit: widget._contactCubit ?? getIt(), ), const CustomFooter(), ]; diff --git a/lib/app/features/home/widgets/contact/contact_widget.dart b/lib/app/features/home/widgets/contact/contact_widget.dart deleted file mode 100644 index f9c2d8a6..00000000 --- a/lib/app/features/home/widgets/contact/contact_widget.dart +++ /dev/null @@ -1,82 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:site/app/core/injections/injections.dart'; -import 'package:site/app/core/l10n/l10n.dart'; -import 'package:site/app/core/responsive/responsive.dart'; -import 'package:site/app/core/shared/app_keys.dart'; -import 'package:site/app/core/tokens/tokens.dart'; -import 'package:site/app/features/home/widgets/contact/contact_mobile.dart'; -import 'package:site/app/features/home/widgets/contact/contact_web.dart'; -import 'package:site/app/features/home/widgets/contact/controller/contact_controller.dart'; -import 'package:site/app/features/home/widgets/contact/widgets/widgets.dart'; -import 'package:site/app/widgets/snack_bars/snack_bars.dart'; -import 'package:site/data/models/models.dart' as models; -import 'package:site/data/repositories/contact/contact.dart'; - -class ContactWidget extends StatelessWidget { - ContactWidget({ - super.key, - ContactController? contactController, - }) : _contactController = contactController ?? - ContactController( - contactRepository: ContactRepositoryImpl( - firebaseRemoteConfig: getIt(), - httpClient: getIt(), - ), - ); - - final ContactController? _contactController; - - @override - Widget build(BuildContext context) { - final formKey = GlobalKey(); - final nameController = TextEditingController(); - final emailController = TextEditingController(); - final messageController = TextEditingController(); - final subjectController = TextEditingController(); - - Widget contactForm() { - return CustomForm( - formKey: formKey, - nameController: nameController, - emailController: emailController, - subjectController: subjectController, - messageController: messageController, - onPressed: () { - if (formKey.currentState?.validate() ?? false) { - appShowSnackBar( - context, - text: AppTexts.get(context).emailSendedWithSuccess, - icon: Icons.check, - color: AppColors.primaryDark, - width: 300, - ); - _contactController?.sendMail( - contact: models.Contact( - name: nameController.text, - email: emailController.text, - message: messageController.text, - subject: subjectController.text, - ), - ); - nameController.clear(); - emailController.clear(); - messageController.clear(); - subjectController.clear(); - } - }, - ); - } - - return LayoutBuilder( - key: AppKeys.contact, - builder: (context, constraints) { - final form = contactForm(); - - return constraints.maxWidth < Breakpoints.contact - ? ContactMobile(form) - : ContactWeb(form); - }, - ); - } -} diff --git a/lib/app/features/home/widgets/contact/controller/contact_controller.dart b/lib/app/features/home/widgets/contact/controller/contact_controller.dart deleted file mode 100644 index 273e62ff..00000000 --- a/lib/app/features/home/widgets/contact/controller/contact_controller.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:site/app/core/injections/injections.dart'; - -import 'package:site/data/models/models.dart'; -import 'package:site/data/repositories/contact/contact.dart'; - -class ContactController extends ChangeNotifier { - ContactController({ - ContactRepositoryImpl? contactRepository, - }) : _contactRepository = contactRepository ?? getIt(); - - final ContactRepositoryImpl _contactRepository; - - Future sendMail({ - required Contact contact, - }) { - return _contactRepository.sendMail( - contact: contact, - ); - } -} diff --git a/lib/app/features/home/widgets/footer/widgets/text_with_link.dart b/lib/app/features/home/widgets/footer/widgets/text_with_link.dart index eb673a72..65c4d047 100644 --- a/lib/app/features/home/widgets/footer/widgets/text_with_link.dart +++ b/lib/app/features/home/widgets/footer/widgets/text_with_link.dart @@ -20,7 +20,7 @@ class TextWithLink extends StatelessWidget { clipBehavior: Clip.antiAlias, child: InkWell( splashColor: AppColors.primary, - overlayColor: MaterialStateProperty.all( + overlayColor: WidgetStateProperty.all( AppColors.primary.withOpacity(0.25), ), onTap: () => LaunchUrls().launchURL(link), diff --git a/lib/app/utils/contact_validators.dart b/lib/app/utils/contact_validators.dart index 266107e4..62e27677 100644 --- a/lib/app/utils/contact_validators.dart +++ b/lib/app/utils/contact_validators.dart @@ -30,6 +30,8 @@ class ContactValidators { } static String? email(String? value, [BuildContext? context]) { + value = value?.trim(); + if (value == null || value.isEmpty) { return AppTexts.get(context!).insertValidEmail; } diff --git a/lib/app/widgets/app_bar/widgets/web_app_bar_title.dart b/lib/app/widgets/app_bar/widgets/web_app_bar_title.dart index 7cb7fb68..d2a8b137 100644 --- a/lib/app/widgets/app_bar/widgets/web_app_bar_title.dart +++ b/lib/app/widgets/app_bar/widgets/web_app_bar_title.dart @@ -26,7 +26,7 @@ class WebAppBarTitle extends StatelessWidget with ScrollToMixin { clipBehavior: Clip.antiAlias, child: InkWell( splashColor: AppColors.primary, - overlayColor: MaterialStateProperty.all( + overlayColor: WidgetStateProperty.all( AppColors.primary.withOpacity(0.25), ), onTap: () => scrollTo(index, itemScrollController), diff --git a/lib/app/widgets/snack_bars/app_show_snack_bar.dart b/lib/app/widgets/snack_bars/app_show_snack_bar.dart index 07c7e7d5..2eb179f2 100644 --- a/lib/app/widgets/snack_bars/app_show_snack_bar.dart +++ b/lib/app/widgets/snack_bars/app_show_snack_bar.dart @@ -1,8 +1,8 @@ -import 'package:flutter/material.dart'; - import 'package:auto_size_text/auto_size_text.dart'; +import 'package:flutter/material.dart'; import 'package:site/app/core/tokens/tokens.dart'; +import 'package:site/app/features/contact/contact.dart'; ScaffoldFeatureController appShowSnackBar( BuildContext context, { @@ -44,3 +44,28 @@ ScaffoldFeatureController appShowSnackBar( ), ); } + +ScaffoldFeatureController appShowSnackBarFromContact( + BuildContext context, + ContactState state, +) { + return appShowSnackBar( + context, + width: 300, + text: switch (state) { + ContactSuccess() => state.message, + ContactError() => state.message, + _ => '', + }, + icon: switch (state) { + ContactSuccess() => Icons.check, + ContactError() => Icons.error, + _ => Icons.info, + }, + color: switch (state) { + ContactSuccess() => AppColors.green, + ContactError() => AppColors.red, + _ => AppColors.primaryDark, + }, + ); +} diff --git a/lib/app/widgets/utils_widgets/single_child_scroll_view_without_scroll.dart b/lib/app/widgets/utils_widgets/single_child_scroll_view_without_scroll.dart index be78e5ab..17d9001e 100644 --- a/lib/app/widgets/utils_widgets/single_child_scroll_view_without_scroll.dart +++ b/lib/app/widgets/utils_widgets/single_child_scroll_view_without_scroll.dart @@ -17,14 +17,17 @@ class SingleChildScrollViewWithoutScroll extends StatelessWidget { const SingleChildScrollViewWithoutScroll({ super.key, required this.child, + this.primary, }); final Widget child; + final bool? primary; @override Widget build(BuildContext context) { return SingleChildScrollView( physics: const NeverScrollableScrollPhysics(), + primary: primary, child: child, ); } diff --git a/lib/data/constants/constants_api.dart b/lib/data/constants/constants_api.dart index 88b49a97..3350faef 100644 --- a/lib/data/constants/constants_api.dart +++ b/lib/data/constants/constants_api.dart @@ -1,7 +1,11 @@ +import 'package:site/infra/env/env.dart'; + class ConstantsAPI { - static const baseUrl = 'https://api.emailjs.com/api/v1.0/email/send'; - static const headers = { - 'origin': 'http://localhost', - 'Content-Type': 'application/json', - }; + /// Send email endpoint. + static final apiSendMail = Env.apiSendMail; + + /// Recaptcha information. + static final recaptchaPublicKey = Env.recaptchaPublicKey; + static final recaptchaSecretKey = Env.recaptchaSecretKey; + static const recaptchaUrl = 'https://www.google.com/recaptcha/api/siteverify'; } diff --git a/lib/data/models/models.dart b/lib/data/models/models.dart deleted file mode 100644 index 4c72b197..00000000 --- a/lib/data/models/models.dart +++ /dev/null @@ -1 +0,0 @@ -export 'contact.dart'; diff --git a/lib/data/repositories/contact/contact_repository.dart b/lib/data/repositories/contact/contact_repository.dart deleted file mode 100644 index d0dc29b7..00000000 --- a/lib/data/repositories/contact/contact_repository.dart +++ /dev/null @@ -1,5 +0,0 @@ -import 'package:site/data/models/models.dart'; - -abstract class ContactRepository { - Future sendMail({required Contact contact}); -} diff --git a/lib/data/repositories/contact/contact_repository_impl.dart b/lib/data/repositories/contact/contact_repository_impl.dart deleted file mode 100644 index d6d70a2a..00000000 --- a/lib/data/repositories/contact/contact_repository_impl.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'dart:convert'; - -import 'package:firebase_remote_config/firebase_remote_config.dart'; -import 'package:http/http.dart' as http; - -import 'package:site/data/constants/constants_api.dart'; -import 'package:site/data/models/models.dart'; -import 'package:site/data/repositories/contact/contact.dart'; - -class ContactRepositoryImpl implements ContactRepository { - ContactRepositoryImpl({ - required FirebaseRemoteConfig firebaseRemoteConfig, - required http.Client httpClient, - }) : _firebaseRemoteConfig = firebaseRemoteConfig, - _httpClient = httpClient; - - final FirebaseRemoteConfig _firebaseRemoteConfig; - final http.Client _httpClient; - - @override - Future sendMail({ - required Contact contact, - }) async { - final serviceId = _firebaseRemoteConfig.getString('service_id'); - final templateId = _firebaseRemoteConfig.getString('template_id'); - final userId = _firebaseRemoteConfig.getString('user_id'); - final toEmail = _firebaseRemoteConfig.getString('to_email'); - final baseUrl = Uri.parse(ConstantsAPI.baseUrl); - - final response = await _httpClient.post( - baseUrl, - headers: ConstantsAPI.headers, - body: json.encode( - { - 'service_id': serviceId, - 'template_id': templateId, - 'user_id': userId, - 'template_params': { - 'user_name': contact.name, - 'user_email': contact.email, - 'user_subject': contact.email, - 'user_message': contact.message, - 'to_email': toEmail, - }, - }, - ), - ); - - return response; - } -} diff --git a/lib/data/services/firebase/firebase_service_impl.dart b/lib/data/services/firebase/firebase_service_impl.dart index dea81d34..ae6151d7 100644 --- a/lib/data/services/firebase/firebase_service_impl.dart +++ b/lib/data/services/firebase/firebase_service_impl.dart @@ -23,7 +23,8 @@ class FirebaseServiceImpl implements FirebaseService { try { await remoteConfig.setDefaults(remoteConfigKeys); await remoteConfig.setConfigSettings(remoteConfigSettings); - await remoteConfig.fetchAndActivate(); + // as long as no fetchAndActivate is needed, this is not necessary + // await remoteConfig.fetchAndActivate(); } catch (e, s) { developer.log( 'setUpRemoteConfig', diff --git a/lib/data/services/recaptcha/recaptcha.dart b/lib/data/services/recaptcha/recaptcha.dart new file mode 100644 index 00000000..bd6d9d05 --- /dev/null +++ b/lib/data/services/recaptcha/recaptcha.dart @@ -0,0 +1,2 @@ +export 'recaptcha_model.dart'; +export 'recaptcha_service.dart'; diff --git a/lib/data/services/recaptcha/recaptcha_model.dart b/lib/data/services/recaptcha/recaptcha_model.dart new file mode 100644 index 00000000..f5cfcf01 --- /dev/null +++ b/lib/data/services/recaptcha/recaptcha_model.dart @@ -0,0 +1,81 @@ +import 'dart:convert'; + +import 'package:equatable/equatable.dart'; + +class RecaptchaResponse extends Equatable { + const RecaptchaResponse({ + required this.success, + required this.challengeTimeStamp, + required this.hostName, + required this.score, + required this.action, + this.errorCodes = const [], + }); + + factory RecaptchaResponse.fromMap(Map json) { + return RecaptchaResponse( + success: json['success'] ?? false, + challengeTimeStamp: DateTime.parse(json['challenge_ts']), + hostName: json['hostname'] ?? '', + score: double.tryParse('${json['score']}') ?? 0.0, + action: json['action'] ?? '', + errorCodes: json['error-codes'] ?? [], + ); + } + + factory RecaptchaResponse.fromJson(String source) => + RecaptchaResponse.fromMap(json.decode(source)); + + final bool success; + final DateTime challengeTimeStamp; + final String hostName; + final double score; + final String action; + final List errorCodes; + + RecaptchaResponse copyWith({ + bool? success, + DateTime? challengeTimeStamp, + String? hostName, + double? score, + String? action, + List? errorCodes, + }) { + return RecaptchaResponse( + success: success ?? this.success, + challengeTimeStamp: challengeTimeStamp ?? this.challengeTimeStamp, + hostName: hostName ?? this.hostName, + score: score ?? this.score, + action: action ?? this.action, + errorCodes: errorCodes ?? this.errorCodes, + ); + } + + Map toMap() { + return { + 'success': success, + 'challenge_ts': challengeTimeStamp.millisecondsSinceEpoch, + 'hostname': hostName, + 'score': score, + 'action': action, + 'error-codes': errorCodes, + }; + } + + String toJson() => json.encode(toMap()); + + @override + String toString() { + return 'RecaptchaResponse(success: $success, challengeTimeStamp: $challengeTimeStamp, hostName: $hostName, score: $score, action: $action, errorCodes: $errorCodes)'; + } + + @override + List get props => [ + success, + challengeTimeStamp, + hostName, + score, + action, + errorCodes, + ]; +} diff --git a/lib/data/services/recaptcha/recaptcha_service.dart b/lib/data/services/recaptcha/recaptcha_service.dart new file mode 100644 index 00000000..82c9b534 --- /dev/null +++ b/lib/data/services/recaptcha/recaptcha_service.dart @@ -0,0 +1,70 @@ +import 'dart:developer'; + +import 'package:dio/dio.dart'; +import 'package:flutter/foundation.dart'; +import 'package:g_recaptcha_v3/g_recaptcha_v3.dart'; +import 'package:site/data/constants/constants_api.dart'; +import 'package:site/data/services/recaptcha/recaptcha.dart'; + +class RecaptchaService { + RecaptchaService._(); + + static Future initiate() async => + await GRecaptchaV3.ready(ConstantsAPI.recaptchaPublicKey); + + static Future isNotABot() async { + if ([ + TargetPlatform.iOS, + TargetPlatform.android, + ].contains(defaultTargetPlatform)) { + return true; + } + + final verificationResponse = await _getVerificationResponse(); + + if (verificationResponse == null) { + return false; + } + + final score = verificationResponse.score; + return score >= 0.5 && score < 1; + } + + static Future _getVerificationResponse() async { + try { + final token = await GRecaptchaV3.execute('submit') ?? ''; + + if (token.isNotEmpty) { + final response = await Dio().post( + ConstantsAPI.recaptchaUrl, + // body: { + // 'secret': ConstantsAPI.recaptchaSecretKey, + // 'response': token, + // }, + // headers: { + // 'Access-Control-Allow-Origin': '*', + // 'Access-Control-Allow-Methods': 'POST', + // 'Access-Control-Allow-Headers': + // 'Origin, X-Requested-With, Content-Type, Accept', + // }, + data: { + 'secret': ConstantsAPI.recaptchaSecretKey, + 'response': token, + }, + ); + + final body = response.data; + + return RecaptchaResponse.fromJson(body); + } else { + log('RecaptchaService._getVerificationResponse, token is empty'); + } + } catch (e, s) { + log( + 'RecaptchaService._getVerificationResponse, error: $e, stackTrace: $s', + ); + } + + return null; + } +} diff --git a/lib/infra/env/env.dart b/lib/infra/env/env.dart new file mode 100644 index 00000000..bcf2aad4 --- /dev/null +++ b/lib/infra/env/env.dart @@ -0,0 +1,22 @@ +import 'package:envied/envied.dart'; + +part 'env.g.dart'; + +// Rename and / or create .env file in the root of the project +// (already has a .env.example file to help) +// - Add the following variables. +// And run the following commands: +// dart run build_runner clean +// dart run build_runner build --delete-conflicting-outputs + +@Envied(path: '.env') +final class Env { + @EnviedField(varName: 'RECAPTCHA_PUBLIC_KEY', obfuscate: true) + static final String recaptchaPublicKey = _Env.recaptchaPublicKey; + + @EnviedField(varName: 'RECAPTCHA_SECRET_KEY', obfuscate: true) + static final String recaptchaSecretKey = _Env.recaptchaSecretKey; + + @EnviedField(varName: 'API_SEND_MAIL', obfuscate: true) + static final String apiSendMail = _Env.apiSendMail; +} diff --git a/lib/main.dart b/lib/main.dart index 6a2d8627..7c15565e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,6 @@ -import 'package:flutter/material.dart'; +import 'dart:async'; +import 'package:flutter/material.dart'; import 'package:url_strategy/url_strategy.dart'; import 'package:site/app/app_widget.dart'; @@ -9,6 +10,11 @@ import 'package:site/data/services/firebase/firebase.dart'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); await FirebaseServiceImpl().setUpInitialization(); + + // if (kIsWeb) { + // await RecaptchaService.initiate(); + // } + setPathUrlStrategy(); configureDependencies(); runApp( diff --git a/pubspec.lock b/pubspec.lock index df31c51c..a1e8cc87 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,14 +1,30 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" + url: "https://pub.dev" + source: hosted + version: "67.0.0" _flutterfire_internals: dependency: transitive description: name: _flutterfire_internals - sha256: "4eec93681221723a686ad580c2e7d960e1017cf1a4e0a263c2573c2c6b0bf5cd" + sha256: e4be6711f96d3d4eebe79728897d645b7a5585bbfdd6d534878d202c171266d7 + url: "https://pub.dev" + source: hosted + version: "1.3.34" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" url: "https://pub.dev" source: hosted - version: "1.3.25" + version: "6.4.1" archive: dependency: transitive description: @@ -41,6 +57,22 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.0" + bloc: + dependency: transitive + description: + name: bloc + sha256: "106842ad6569f0b60297619e9e0b1885c2fb9bf84812935490e6c5275777804e" + url: "https://pub.dev" + source: hosted + version: "8.1.4" + bloc_test: + dependency: "direct main" + description: + name: bloc_test + sha256: "165a6ec950d9252ebe36dc5335f2e6eb13055f33d56db0eeb7642768849b43d2" + url: "https://pub.dev" + source: hosted + version: "9.1.7" boolean_selector: dependency: transitive description: @@ -49,6 +81,70 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + build: + dependency: transitive + description: + name: build + sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + build_config: + dependency: transitive + description: + name: build_config + sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + url: "https://pub.dev" + source: hosted + version: "1.1.1" + build_daemon: + dependency: transitive + description: + name: build_daemon + sha256: "0343061a33da9c5810b2d6cee51945127d8f4c060b7fbdd9d54917f0a3feaaa1" + url: "https://pub.dev" + source: hosted + version: "4.0.1" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + build_runner: + dependency: "direct dev" + description: + name: build_runner + sha256: "1414d6d733a85d8ad2f1dfcb3ea7945759e35a123cb99ccfac75d0758f75edfa" + url: "https://pub.dev" + source: hosted + version: "2.4.10" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + sha256: "4ae8ffe5ac758da294ecf1802f2aff01558d8b1b00616aa7538ea9a8a5d50799" + url: "https://pub.dev" + source: hosted + version: "7.3.0" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb + url: "https://pub.dev" + source: hosted + version: "8.9.2" characters: dependency: transitive description: @@ -57,6 +153,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + url: "https://pub.dev" + source: hosted + version: "2.0.3" clock: dependency: transitive description: @@ -65,6 +169,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + code_builder: + dependency: transitive + description: + name: code_builder + sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37 + url: "https://pub.dev" + source: hosted + version: "4.10.0" collection: dependency: transitive description: @@ -81,6 +193,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.1" + coverage: + dependency: transitive + description: + name: coverage + sha256: "3945034e86ea203af7a056d98e98e42a5518fff200d6e8e6647e1886b07e936e" + url: "https://pub.dev" + source: hosted + version: "1.8.0" crypto: dependency: transitive description: @@ -89,6 +209,30 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.3" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" + url: "https://pub.dev" + source: hosted + version: "2.3.6" + diff_match_patch: + dependency: transitive + description: + name: diff_match_patch + sha256: "2efc9e6e8f449d0abe15be240e2c2a3bcd977c8d126cfd70598aee60af35c0a4" + url: "https://pub.dev" + source: hosted + version: "0.4.1" + dio: + dependency: "direct main" + description: + name: dio + sha256: "11e40df547d418cc0c4900a9318b26304e665da6fa4755399a9ff9efd09034b5" + url: "https://pub.dev" + source: hosted + version: "5.4.3+1" email_validator: dependency: "direct main" description: @@ -97,6 +241,30 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.17" + envied: + dependency: "direct main" + description: + name: envied + sha256: bbff9c76120e4dc5e2e36a46690cf0a26feb65e7765633f4e8d916bcd173a450 + url: "https://pub.dev" + source: hosted + version: "0.5.4+1" + envied_generator: + dependency: "direct dev" + description: + name: envied_generator + sha256: "517b70de08d13dcd40e97b4e5347e216a0b1c75c99e704f3c85c0474a392d14a" + url: "https://pub.dev" + source: hosted + version: "0.5.4+1" + equatable: + dependency: "direct main" + description: + name: equatable + sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + url: "https://pub.dev" + source: hosted + version: "2.0.5" fake_async: dependency: transitive description: @@ -125,34 +293,34 @@ packages: dependency: "direct main" description: name: firebase_analytics - sha256: b13cbf1ee78744ca5e6b762e9218db3bd3967a0edfed75f58339907892a2ccb9 + sha256: "08f98034f51c8018d08cd56ac51f44c63ab684f85a7b8f0ede92c4acfb23a2bc" url: "https://pub.dev" source: hosted - version: "10.8.9" + version: "10.10.6" firebase_analytics_platform_interface: dependency: transitive description: name: firebase_analytics_platform_interface - sha256: "416b33d62033db5ecd2df719fcb657ad04e9995fa0fc392ffdab4ca0e76cb679" + sha256: "4de04e25bd739184eb2cfcd76c2f336c9e03bf65457e1e17d027d65f2344a2df" url: "https://pub.dev" source: hosted - version: "3.9.9" + version: "3.10.7" firebase_analytics_web: dependency: transitive description: name: firebase_analytics_web - sha256: "9dca9d8d468172444ef18cabb73fe99f7aae24733bfad67115bd36bffd2d65c1" + sha256: "77ded8cb214193cc816395625ab8a031539cdd870d667123d1b9d53c7303a82d" url: "https://pub.dev" source: hosted - version: "0.5.5+21" + version: "0.5.7+6" firebase_core: dependency: "direct main" description: name: firebase_core - sha256: "53316975310c8af75a96e365f9fccb67d1c544ef0acdbf0d88bbe30eedd1c4f9" + sha256: "4b5100e2dbc3fe72c2d4241a046d3f01457fe11293283a324f5c52575e3406f8" url: "https://pub.dev" source: hosted - version: "2.27.0" + version: "2.31.1" firebase_core_platform_interface: dependency: "direct main" description: @@ -165,39 +333,55 @@ packages: dependency: "direct main" description: name: firebase_core_web - sha256: c8e1d59385eee98de63c92f961d2a7062c5d9a65e7f45bdc7f1b0b205aab2492 + sha256: "43d9e951ac52b87ae9cc38ecdcca1e8fa7b52a1dd26a96085ba41ce5108db8e9" url: "https://pub.dev" source: hosted - version: "2.11.5" + version: "2.17.0" firebase_remote_config: dependency: "direct main" description: name: firebase_remote_config - sha256: b085a72c007bd8f177a7ab98b8292d764659b07fb6b0561b84125239ee656efc + sha256: e685eb602a528b9ad2ae5c09f244728a484c4289dbf273af03042e737e752a6e url: "https://pub.dev" source: hosted - version: "4.3.17" + version: "4.4.6" firebase_remote_config_platform_interface: dependency: transitive description: name: firebase_remote_config_platform_interface - sha256: c589e007156b2c9f903253764c108abb96c1b56dd17cf0b91afc4b72ccab7bb6 + sha256: "86621fa95c515491d1914cee690475c84dc46dd87a71cf16a8df4e280aa1c123" url: "https://pub.dev" source: hosted - version: "1.4.25" + version: "1.4.34" firebase_remote_config_web: dependency: transitive description: name: firebase_remote_config_web - sha256: "92443c70e2721ab9d4beb23eb1d9f971da7381332451daee04f619b0f9204569" + sha256: ee05916a72d9630a838df0cc581b006991eee949690a9f212731a981be864234 + url: "https://pub.dev" + source: hosted + version: "1.6.6" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" url: "https://pub.dev" source: hosted - version: "1.4.25" + version: "1.1.0" flutter: dependency: "direct main" description: flutter source: sdk version: "0.0.0" + flutter_bloc: + dependency: "direct main" + description: + name: flutter_bloc + sha256: f0ecf6e6eb955193ca60af2d5ca39565a86b8a142452c5b24d96fb477428f4d2 + url: "https://pub.dev" + source: hosted + version: "8.1.5" flutter_driver: dependency: transitive description: flutter @@ -207,10 +391,10 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: e2a421b7e59244faef694ba7b30562e489c2b489866e505074eb005cd7060db7 + sha256: "3f41d009ba7172d5ff9be5f6e6e6abb4300e263aab8866d2a0842ed2a70f8f0c" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "4.0.0" flutter_localizations: dependency: "direct main" description: flutter @@ -234,11 +418,27 @@ packages: description: flutter source: sdk version: "0.0.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" + source: hosted + version: "4.0.0" fuchsia_remote_debug_protocol: dependency: transitive description: flutter source: sdk version: "0.0.0" + g_recaptcha_v3: + dependency: "direct main" + description: + name: g_recaptcha_v3 + sha256: b493d9bbad64bb4631a2b7bb86f7b4c6c6a3c2b327729c4130b3c95817bedf29 + url: "https://pub.dev" + source: hosted + version: "0.0.6" get_it: dependency: "direct main" description: @@ -247,14 +447,38 @@ packages: url: "https://pub.dev" source: hosted version: "7.6.7" + glob: + dependency: transitive + description: + name: glob + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + graphs: + dependency: transitive + description: + name: graphs + sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 + url: "https://pub.dev" + source: hosted + version: "2.3.1" http: - dependency: "direct main" + dependency: transitive description: name: http - sha256: a2bbf9d017fcced29139daa8ed2bba4ece450ab222871df93ca9eec6f80c34ba + sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.1" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + url: "https://pub.dev" + source: hosted + version: "3.2.1" http_parser: dependency: transitive description: @@ -272,58 +496,82 @@ packages: dependency: "direct main" description: name: intl - sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf url: "https://pub.dev" source: hosted - version: "0.18.1" + version: "0.19.0" + io: + dependency: transitive + description: + name: io + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + url: "https://pub.dev" + source: hosted + version: "1.0.4" js: dependency: transitive description: name: js - sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf + url: "https://pub.dev" + source: hosted + version: "0.7.1" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 url: "https://pub.dev" source: hosted - version: "0.6.7" + version: "4.8.1" leak_tracker: dependency: transitive description: name: leak_tracker - sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" url: "https://pub.dev" source: hosted - version: "10.0.0" + version: "10.0.4" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.3" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.1" lints: dependency: transitive description: name: lints - sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 + sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235" url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "4.0.0" + logging: + dependency: transitive + description: + name: logging + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + url: "https://pub.dev" + source: hosted + version: "1.2.0" lottie: dependency: "direct main" description: name: lottie - sha256: ce2bb2605753915080e4ee47f036a64228c88dc7f56f7bc1dbe912d75b55b1e2 + sha256: "6a24ade5d3d918c306bb1c21a6b9a04aab0489d51a2582522eea820b4093b62b" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.2" matcher: dependency: transitive description: @@ -344,10 +592,18 @@ packages: dependency: transitive description: name: meta - sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" + url: "https://pub.dev" + source: hosted + version: "1.12.0" + mime: + dependency: transitive + description: + name: mime + sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.0.5" mocktail: dependency: "direct dev" description: @@ -356,22 +612,46 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.3" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + package_config: + dependency: transitive + description: + name: package_config + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" package_info_plus: dependency: "direct main" description: name: package_info_plus - sha256: "88bc797f44a94814f2213db1c9bd5badebafdfb8290ca9f78d4b9ee2a3db4d79" + sha256: b93d8b4d624b4ea19b0a5a208b2d6eff06004bc3ce74c06040b120eeadd00ce0 url: "https://pub.dev" source: hosted - version: "5.0.1" + version: "8.0.0" package_info_plus_platform_interface: dependency: transitive description: name: package_info_plus_platform_interface - sha256: "9bc8ba46813a4cc42c66ab781470711781940780fd8beddd0c3da62506d3a6c6" + sha256: f49918f3433a3146047372f9d4f1f847511f2acd5cd030e1f44fe5a50036b70e url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.0" path: dependency: transitive description: @@ -420,6 +700,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.7.4" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" + source: hosted + version: "1.5.1" process: dependency: transitive description: @@ -428,6 +716,38 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.2" + provider: + dependency: transitive + description: + name: provider + sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c + url: "https://pub.dev" + source: hosted + version: "6.1.2" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 + url: "https://pub.dev" + source: hosted + version: "1.2.3" + recase: + dependency: transitive + description: + name: recase + sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213 + url: "https://pub.dev" + source: hosted + version: "4.1.0" scrollable_positioned_list: dependency: "direct main" description: @@ -436,11 +756,67 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.8" + shelf: + dependency: transitive + description: + name: shelf + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + url: "https://pub.dev" + source: hosted + version: "1.4.1" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e + url: "https://pub.dev" + source: hosted + version: "1.1.2" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" + url: "https://pub.dev" + source: hosted + version: "1.0.4" sky_engine: dependency: transitive description: flutter source: sdk version: "0.0.99" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" + url: "https://pub.dev" + source: hosted + version: "1.5.0" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: "84cf769ad83aa6bb61e0aa5a18e53aea683395f196a6f39c4c881fb90ed4f7ae" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" + url: "https://pub.dev" + source: hosted + version: "0.10.12" source_span: dependency: transitive description: @@ -465,6 +841,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + url: "https://pub.dev" + source: hosted + version: "2.1.0" string_scanner: dependency: transitive description: @@ -489,14 +873,38 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.1" + test: + dependency: transitive + description: + name: test + sha256: "7ee446762c2c50b3bd4ea96fe13ffac69919352bd3b4b17bac3f3465edc58073" + url: "https://pub.dev" + source: hosted + version: "1.25.2" test_api: dependency: transitive description: name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" + url: "https://pub.dev" + source: hosted + version: "0.7.0" + test_core: + dependency: transitive + description: + name: test_core + sha256: "2bc4b4ecddd75309300d8096f781c0e3280ca1ef85beda558d33fcbedc2eead4" url: "https://pub.dev" source: hosted - version: "0.6.1" + version: "0.6.0" + timing: + dependency: transitive + description: + name: timing + sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" + url: "https://pub.dev" + source: hosted + version: "1.0.1" typed_data: dependency: transitive description: @@ -509,10 +917,10 @@ packages: dependency: "direct main" description: name: url_launcher - sha256: "0ecc004c62fd3ed36a2ffcbe0dd9700aee63bd7532d0b642a488b1ec310f492e" + sha256: "6ce1e04375be4eed30548f10a315826fd933c1e493206eab82eed01f438c8d2e" url: "https://pub.dev" source: hosted - version: "6.2.5" + version: "6.2.6" url_launcher_android: dependency: transitive description: @@ -557,10 +965,10 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: fff0932192afeedf63cdd50ecbb1bc825d31aed259f02bb8dba0f3b729a5e88b + sha256: "8d9e750d8c9338601e709cd0885f95825086bd8b642547f26bda435aade95d8a" url: "https://pub.dev" source: hosted - version: "2.2.3" + version: "2.3.1" url_launcher_windows: dependency: transitive description: @@ -613,18 +1021,34 @@ packages: dependency: transitive description: name: vm_service - sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" url: "https://pub.dev" source: hosted - version: "13.0.0" + version: "14.2.1" + watcher: + dependency: transitive + description: + name: watcher + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + url: "https://pub.dev" + source: hosted + version: "1.1.0" web: dependency: transitive description: name: web - sha256: "4188706108906f002b3a293509234588823c8c979dc83304e229ff400c996b05" + sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" url: "https://pub.dev" source: hosted - version: "0.4.2" + version: "0.5.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: "58c6666b342a38816b2e7e50ed0f1e261959630becd4c879c4f26bfa14aa5a42" + url: "https://pub.dev" + source: hosted + version: "2.4.5" webdriver: dependency: transitive description: @@ -633,6 +1057,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.3" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" + url: "https://pub.dev" + source: hosted + version: "1.2.1" win32: dependency: transitive description: @@ -649,6 +1081,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.5.0" + yaml: + dependency: transitive + description: + name: yaml + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + url: "https://pub.dev" + source: hosted + version: "3.1.2" sdks: - dart: ">=3.3.0 <4.0.0" - flutter: ">=3.16.6" + dart: ">=3.4.0 <4.0.0" + flutter: ">=3.19.0" diff --git a/pubspec.yaml b/pubspec.yaml index c0aff6bc..d89d0a9e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,35 +1,42 @@ name: site description: Felipe Sales | Social Links. publish_to: 'none' -version: 2.1.2 +version: 2.2.1 environment: sdk: '>=3.3.0 <4.0.0' dependencies: auto_size_text: ^3.0.0 + bloc_test: ^9.1.7 + dio: ^5.4.3+1 email_validator: ^2.1.17 - firebase_analytics: ^10.8.5 - firebase_core: ^2.25.4 + envied: ^0.5.4+1 + equatable: ^2.0.5 + firebase_analytics: ^10.10.6 + firebase_core: ^2.31.1 firebase_core_platform_interface: ^5.0.0 - firebase_core_web: ^2.11.4 - firebase_remote_config: ^4.3.13 + firebase_core_web: ^2.17.0 + firebase_remote_config: ^4.4.6 flutter: sdk: flutter + flutter_bloc: ^8.1.5 flutter_localizations: sdk: flutter - flutter_svg: ^2.0.9 + flutter_svg: ^2.0.10+1 + g_recaptcha_v3: ^0.0.6 get_it: ^7.6.7 - http: 1.2.0 - intl: 0.18.1 - lottie: ^3.0.0 - package_info_plus: ^5.0.1 + intl: ^0.19.0 + lottie: ^3.1.2 + package_info_plus: ^8.0.0 scrollable_positioned_list: ^0.3.8 - url_launcher: ^6.2.4 + url_launcher: ^6.2.6 url_strategy: ^0.2.0 dev_dependencies: - flutter_lints: ^3.0.1 + build_runner: ^2.4.10 + envied_generator: ^0.5.4+1 + flutter_lints: ^4.0.0 flutter_test: sdk: flutter integration_test: diff --git a/test/app/app_widget_test.dart b/test/app/app_widget_test.dart index c373e63c..33dc50d8 100644 --- a/test/app/app_widget_test.dart +++ b/test/app/app_widget_test.dart @@ -7,11 +7,11 @@ import '../utils/utils.dart'; void main() { late MockFirebaseRemoteConfig mockFirebaseRemoteConfig; - late MockHttpClient mockHttpClient; + late MockContactCubit mockContactCubit; setUp(() { mockFirebaseRemoteConfig = MockFirebaseRemoteConfig(); - mockHttpClient = MockHttpClient(); + mockContactCubit = MockContactCubit(); }); testWidgets('Should renders AppWidget', (tester) async { @@ -19,7 +19,7 @@ void main() { tester: tester, widget: AppWidget( firebaseRemoteConfig: mockFirebaseRemoteConfig, - httpClient: mockHttpClient, + contactCubit: mockContactCubit, ), ); diff --git a/test/app/features/contact/data/repositories/contact_repository_impl_test.dart b/test/app/features/contact/data/repositories/contact_repository_impl_test.dart new file mode 100644 index 00000000..ecb6a1c3 --- /dev/null +++ b/test/app/features/contact/data/repositories/contact_repository_impl_test.dart @@ -0,0 +1,55 @@ +import 'package:dio/dio.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:site/app/features/contact/contact.dart'; + +import '../../../../../utils/utils.dart'; + +void main() { + late MockHttpClient mockHttpClient; + late ContactRepositoryImpl contactRepository; + + setUp(() { + mockHttpClient = MockHttpClient(); + contactRepository = ContactRepositoryImpl( + httpClient: mockHttpClient, + ); + + when( + () => mockHttpClient.post( + any(), + data: any(named: 'data'), + options: any(named: 'options'), + ), + ).thenAnswer( + (_) async => Response( + data: {}, + statusCode: 200, + requestOptions: RequestOptions(path: ''), + ), + ); + }); + + setUpAll(() { + registerFallbackValue(UriFake()); + }); + + test('ContactRepositoryImpl', () async { + expect( + contactRepository, + isNotNull, + ); + + await contactRepository.sendMail( + contact: AppFixtures().tContactUser, + ); + + verify( + () => mockHttpClient.post( + any(), + data: any(named: 'data'), + options: any(named: 'options'), + ), + ).called(1); + }); +} diff --git a/test/app/features/contact/presentation/cubit/contact_cubit_test.dart b/test/app/features/contact/presentation/cubit/contact_cubit_test.dart new file mode 100644 index 00000000..d329e7e7 --- /dev/null +++ b/test/app/features/contact/presentation/cubit/contact_cubit_test.dart @@ -0,0 +1,88 @@ +import 'package:flutter_test/flutter_test.dart'; + +import 'package:bloc_test/bloc_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:site/app/core/l10n/l10n.dart'; +import 'package:site/app/core/result/result.dart'; +import 'package:site/app/features/contact/contact.dart'; + +import '../../../../../utils/mocks/mock_app_texts.dart'; +import '../../../../../utils/utils.dart'; + +void main() { + late MockContactRepository mockContactRepository; + late AppLocalizations appLocalizations; + + setUp(() { + mockContactRepository = MockContactRepository(); + appLocalizations = MockAppLocalizations(); + + setUpLocationMock(appLocalizations); + }); + + group('ContactCubit', () { + final contact = AppFixtures().tContactUser; + final contactAnswer = AppFixtures().tContactAnswer; + + blocTest( + 'emits [ContactLoading, ContactSuccess] when sendMail is successful', + build: () { + when( + () => mockContactRepository.sendMail(contact: contact), + ).thenAnswer( + (_) async => Success(contactAnswer), + ); + + return ContactCubit( + contactRepository: mockContactRepository, + appLocalizations: appLocalizations, + ); + }, + act: (cubit) => cubit.sendMail(contact: contact), + expect: () => [ + const ContactLoading(), + ContactSuccess( + contact: contactAnswer, + message: MockAppLocalizationsHelper.emailSendedWithSuccess, + ), + ], + ); + + blocTest( + 'emits [ContactLoading, ContactError] when sendMail fails', + build: () { + when( + () => mockContactRepository.sendMail(contact: contact), + ).thenAnswer( + (_) async => const Failure(ContactFailedResult.unknown), + ); + + return ContactCubit( + contactRepository: mockContactRepository, + appLocalizations: appLocalizations, + ); + }, + act: (cubit) => cubit.sendMail(contact: contact), + expect: () => [ + const ContactLoading(), + ContactError( + contact: contact, + message: MockAppLocalizationsHelper.emailUnknowError, + ), + ], + ); + }); +} + +void setUpLocationMock(AppLocalizations appLocalizations) { + when(() => appLocalizations.emailSendedWithSuccess) + .thenReturn(MockAppLocalizationsHelper.emailSendedWithSuccess); + when(() => appLocalizations.emailTooManyRequests) + .thenReturn(MockAppLocalizationsHelper.emailTooManyRequests); + when(() => appLocalizations.emailUnauthorized) + .thenReturn(MockAppLocalizationsHelper.emailUnauthorized); + when(() => appLocalizations.emailUnknowError) + .thenReturn(MockAppLocalizationsHelper.emailUnknowError); + when(() => appLocalizations.emailNotSended) + .thenReturn(MockAppLocalizationsHelper.emailNotSended); +} diff --git a/test/app/features/home/widgets/contact/widgets/custom_form_test.dart b/test/app/features/contact/presentation/widgets/form/custom_form_test.dart similarity index 92% rename from test/app/features/home/widgets/contact/widgets/custom_form_test.dart rename to test/app/features/contact/presentation/widgets/form/custom_form_test.dart index 7d7cfc4b..17223e2f 100644 --- a/test/app/features/home/widgets/contact/widgets/custom_form_test.dart +++ b/test/app/features/contact/presentation/widgets/form/custom_form_test.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:site/app/features/home/widgets/contact/widgets/widgets.dart'; +import 'package:site/app/features/contact/contact.dart'; import '../../../../../../flutter_test_config.dart'; diff --git a/test/app/features/home/widgets/contact/widgets/custom_text_form_field_test.dart b/test/app/features/contact/presentation/widgets/form/custom_text_form_field_test.dart similarity index 92% rename from test/app/features/home/widgets/contact/widgets/custom_text_form_field_test.dart rename to test/app/features/contact/presentation/widgets/form/custom_text_form_field_test.dart index 7c91d0b5..3dda0de1 100644 --- a/test/app/features/home/widgets/contact/widgets/custom_text_form_field_test.dart +++ b/test/app/features/contact/presentation/widgets/form/custom_text_form_field_test.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:site/app/features/home/widgets/contact/widgets/widgets.dart'; +import 'package:site/app/features/contact/presentation/widgets/widgets.dart'; import 'package:site/app/utils/utils.dart'; import '../../../../../../flutter_test_config.dart'; diff --git a/test/app/features/home/widgets/contact/contact_mobile_test.dart b/test/app/features/contact/presentation/widgets/ui/contact_mobile_test.dart similarity index 72% rename from test/app/features/home/widgets/contact/contact_mobile_test.dart rename to test/app/features/contact/presentation/widgets/ui/contact_mobile_test.dart index f4775f5b..fe01033a 100644 --- a/test/app/features/home/widgets/contact/contact_mobile_test.dart +++ b/test/app/features/contact/presentation/widgets/ui/contact_mobile_test.dart @@ -2,9 +2,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:site/app/features/home/widgets/contact/contact_mobile.dart'; +import 'package:site/app/features/contact/presentation/widgets/ui/contact_mobile.dart'; -import '../../../../../flutter_test_config.dart'; +import '../../../../../../flutter_test_config.dart'; void main() { testWidgets('Should renders ContactMobile', (tester) async { diff --git a/test/app/features/home/widgets/contact/contact_web_test.dart b/test/app/features/contact/presentation/widgets/ui/contact_web_test.dart similarity index 74% rename from test/app/features/home/widgets/contact/contact_web_test.dart rename to test/app/features/contact/presentation/widgets/ui/contact_web_test.dart index cdce7f9b..500dc39a 100644 --- a/test/app/features/home/widgets/contact/contact_web_test.dart +++ b/test/app/features/contact/presentation/widgets/ui/contact_web_test.dart @@ -1,10 +1,9 @@ import 'package:flutter/material.dart'; - import 'package:flutter_test/flutter_test.dart'; -import 'package:site/app/features/home/widgets/contact/contact_web.dart'; +import 'package:site/app/features/contact/contact.dart'; -import '../../../../../flutter_test_config.dart'; +import '../../../../../../flutter_test_config.dart'; void main() { testWidgets('Should renders ContactWeb', (tester) async { diff --git a/test/app/features/contact/presentation/widgets/ui/contact_widget_test.dart b/test/app/features/contact/presentation/widgets/ui/contact_widget_test.dart new file mode 100644 index 00000000..ca2aaa8f --- /dev/null +++ b/test/app/features/contact/presentation/widgets/ui/contact_widget_test.dart @@ -0,0 +1,77 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:site/app/features/contact/contact.dart'; + +import '../../../../../../flutter_test_config.dart'; +import '../../../../../../utils/utils.dart'; + +void main() { + late ContactCubit contactCubit; + + setUp(() { + contactCubit = MockContactCubit(); + + when(() => contactCubit.stream).thenAnswer( + (_) => Stream.value(const ContactInitial()), + ); + + when(() => contactCubit.state).thenReturn(const ContactInitial()); + }); + + testWidgets('Should renders Contact', (tester) async { + await appWidgetTest( + tester: tester, + widget: ContactWidget( + contactCubit: contactCubit, + ), + ); + + final contactWidget = find.byType(ContactWidget); + expect(contactWidget, findsOneWidget); + }); + + group('ContactWidget LayoutBuilder Should renders', () { + final contactMobile = find.byType(ContactMobile); + final contactWeb = find.byType(ContactWeb); + + testWidgets( + 'ContactMobile when constraints is less than Breakpoints.contact', + (tester) async { + tester.view.physicalSize = const Size(200, 400); + + await appWidgetTest( + tester: tester, + widget: ContactWidget( + contactCubit: contactCubit, + ), + ); + + expect(contactMobile, findsOneWidget); + expect(contactWeb, findsNothing); + + addTearDown(tester.view.resetPhysicalSize); + }, + ); + + testWidgets( + 'ContactWeb when constraints is greater than Breakpoints.contact', + (tester) async { + tester.view.physicalSize = const Size(2000, 400); + + await appWidgetTest( + tester: tester, + widget: ContactWidget( + contactCubit: contactCubit, + ), + ); + + expect(contactMobile, findsNothing); + expect(contactWeb, findsOneWidget); + + addTearDown(tester.view.resetPhysicalSize); + }, + ); + }); +} diff --git a/test/app/features/home/home_page_test.dart b/test/app/features/home/home_page_test.dart index 674c4d2b..e9be35c5 100644 --- a/test/app/features/home/home_page_test.dart +++ b/test/app/features/home/home_page_test.dart @@ -1,9 +1,8 @@ import 'package:flutter/material.dart'; - import 'package:flutter_test/flutter_test.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; -import 'package:site/app/core/responsive/responsive.dart'; +import 'package:site/app/core/responsive/responsive.dart'; import 'package:site/app/features/home/home_page.dart'; import 'package:site/app/widgets/drawer/drawer.dart'; @@ -12,11 +11,11 @@ import '../../../utils/utils.dart'; void main() { late MockFirebaseRemoteConfig mockFirebaseRemoteConfig; - late MockHttpClient mockHttpClient; + late MockContactCubit mockContactCubit; setUp(() { mockFirebaseRemoteConfig = MockFirebaseRemoteConfig(); - mockHttpClient = MockHttpClient(); + mockContactCubit = MockContactCubit(); }); testWidgets('Should renders HomePage', (tester) async { @@ -24,7 +23,7 @@ void main() { tester: tester, widget: HomePage( firebaseRemoteConfig: mockFirebaseRemoteConfig, - httpClient: mockHttpClient, + contactCubit: mockContactCubit, ), ); @@ -55,23 +54,19 @@ void main() { ); testWidgets('Find CustomDrawer when is to show', (tester) async { - final isToShowDrawerWidth = tester.binding.window.physicalSizeTestValue = - Size(Breakpoints.appBar.toDouble(), 400); + tester.view.physicalSize = Size(Breakpoints.appBar.toDouble(), 400); await appWidgetTest( tester: tester, - widget: MediaQuery( - data: MediaQueryData(size: isToShowDrawerWidth), - child: HomePage( - firebaseRemoteConfig: mockFirebaseRemoteConfig, - httpClient: mockHttpClient, - ), + widget: HomePage( + firebaseRemoteConfig: mockFirebaseRemoteConfig, + contactCubit: mockContactCubit, ), ); expect(findDrawer, findsOneWidget); - addTearDown(tester.binding.window.clearPhysicalSizeTestValue); + addTearDown(tester.view.resetPhysicalSize); }); testWidgets('Not find Drawer when is not to show', (tester) async { @@ -79,13 +74,13 @@ void main() { tester: tester, widget: HomePage( firebaseRemoteConfig: mockFirebaseRemoteConfig, - httpClient: mockHttpClient, + contactCubit: mockContactCubit, ), ); expect(findDrawer, findsNothing); - addTearDown(tester.binding.window.clearPhysicalSizeTestValue); + addTearDown(tester.view.resetPhysicalSize); }); }); } diff --git a/test/app/features/home/widgets/contact/contact_widget_test.dart b/test/app/features/home/widgets/contact/contact_widget_test.dart deleted file mode 100644 index 6b95821e..00000000 --- a/test/app/features/home/widgets/contact/contact_widget_test.dart +++ /dev/null @@ -1,82 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:flutter_test/flutter_test.dart'; - -import 'package:site/app/features/home/widgets/contact/contact_mobile.dart'; -import 'package:site/app/features/home/widgets/contact/contact_web.dart'; -import 'package:site/app/features/home/widgets/contact/contact_widget.dart'; -import 'package:site/app/features/home/widgets/contact/controller/contact_controller.dart'; - -import '../../../../../flutter_test_config.dart'; -import '../../../../../utils/utils.dart'; - -void main() { - late ContactController contactController; - - setUp(() { - contactController = MockContactController(); - }); - - testWidgets('Should renders Contact', (tester) async { - await appWidgetTest( - tester: tester, - widget: ContactWidget( - contactController: contactController, - ), - ); - - final contactWidget = find.byType(ContactWidget); - expect(contactWidget, findsOneWidget); - }); - - group('ContactWidget LayoutBuilder Should renders', () { - final contactMobile = find.byType(ContactMobile); - final contactWeb = find.byType(ContactWeb); - - testWidgets( - 'ContactMobile when constraints is less than Breakpoints.contact', - (tester) async { - final widthLargeSize = - tester.binding.window.physicalSizeTestValue = const Size(200, 400); - - await appWidgetTest( - tester: tester, - widget: MediaQuery( - data: MediaQueryData(size: widthLargeSize), - child: ContactWidget( - contactController: contactController, - ), - ), - ); - - expect(contactMobile, findsOneWidget); - expect(contactWeb, findsNothing); - - addTearDown(tester.binding.window.clearPhysicalSizeTestValue); - }, - ); - - testWidgets( - 'ContactWeb when constraints is greater than Breakpoints.contact', - (tester) async { - final widthLargeSize = - tester.binding.window.physicalSizeTestValue = const Size(2000, 400); - - await appWidgetTest( - tester: tester, - widget: MediaQuery( - data: MediaQueryData(size: widthLargeSize), - child: ContactWidget( - contactController: contactController, - ), - ), - ); - - expect(contactMobile, findsNothing); - expect(contactWeb, findsOneWidget); - - addTearDown(tester.binding.window.clearPhysicalSizeTestValue); - }, - ); - }); -} diff --git a/test/app/features/home/widgets/contact/controller/contact_controller_test.dart b/test/app/features/home/widgets/contact/controller/contact_controller_test.dart deleted file mode 100644 index 9d50e997..00000000 --- a/test/app/features/home/widgets/contact/controller/contact_controller_test.dart +++ /dev/null @@ -1,89 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:firebase_core/firebase_core.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:http/http.dart' as http; -import 'package:mocktail/mocktail.dart'; - -import 'package:site/app/features/home/widgets/contact/controller/contact_controller.dart'; -import 'package:site/data/repositories/contact/contact.dart'; - -import '../../../../../../utils/utils.dart'; - -void main() { - setupFirebaseAuthMocks(); - - late MockFirebaseRemoteConfig mockFirebaseRemoteConfig; - late MockHttpClient mockHttpClient; - late ContactRepositoryImpl contactRepository; - late ContactController contactController; - - setUp(() { - mockFirebaseRemoteConfig = MockFirebaseRemoteConfig(); - mockHttpClient = MockHttpClient(); - contactRepository = ContactRepositoryImpl( - firebaseRemoteConfig: mockFirebaseRemoteConfig, - httpClient: mockHttpClient, - ); - contactController = ContactController( - contactRepository: contactRepository, - ); - - when( - () => mockFirebaseRemoteConfig.getString('service_id'), - ).thenReturn('service_id'); - - when( - () => mockFirebaseRemoteConfig.getString('template_id'), - ).thenReturn('template_id'); - - when( - () => mockFirebaseRemoteConfig.getString('user_id'), - ).thenReturn('user_id'); - - when( - () => mockFirebaseRemoteConfig.getString('to_email'), - ).thenReturn('to_email'); - - when( - () => mockHttpClient.post( - any(), - headers: any(named: 'headers'), - body: any(named: 'body'), - ), - ).thenAnswer( - (_) async => http.Response('', 200), - ); - }); - - setUpAll(() async { - if (Firebase.apps.isEmpty) await Firebase.initializeApp(); - registerFallbackValue(UriFake()); - }); - - test('ContactController', () { - expect( - contactController, - isInstanceOf(), - ); - - expect( - contactController, - isInstanceOf(), - ); - - expect( - contactController.sendMail, - isInstanceOf(), - ); - - var sendMail = contactController.sendMail( - contact: AppFixtures().tContact, - ); - - expect( - () => sendMail, - isInstanceOf(), - ); - }); -} diff --git a/test/app/features/home/widgets/presentation/presentation_test.dart b/test/app/features/home/widgets/presentation/presentation_test.dart index 6e553b8c..20a68d25 100644 --- a/test/app/features/home/widgets/presentation/presentation_test.dart +++ b/test/app/features/home/widgets/presentation/presentation_test.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; - import 'package:flutter_test/flutter_test.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; @@ -28,42 +27,34 @@ void main() { testWidgets( 'PresentationMobile when constraints is less than Breakpoints.presentation', (tester) async { - final widthSmallSize = tester.binding.window.physicalSizeTestValue = - const Size(400, 400); + tester.view.physicalSize = const Size(400, 400); await appWidgetTest( tester: tester, - widget: MediaQuery( - data: MediaQueryData(size: widthSmallSize), - child: Presentation(ItemScrollController()), - ), + widget: Presentation(ItemScrollController()), ); expect(presentationMobile, findsOneWidget); expect(presentationWeb, findsNothing); - addTearDown(tester.binding.window.clearPhysicalSizeTestValue); + addTearDown(tester.view.resetPhysicalSize); }, ); testWidgets( 'PresentationWeb when constraints is greater than Breakpoints.presentation', (tester) async { - final widthLargeSize = tester.binding.window.physicalSizeTestValue = - const Size(2000, 1000); + tester.view.physicalSize = const Size(2000, 1000); await appWidgetTest( tester: tester, - widget: MediaQuery( - data: MediaQueryData(size: widthLargeSize), - child: Presentation(ItemScrollController()), - ), + widget: Presentation(ItemScrollController()), ); expect(presentationMobile, findsNothing); expect(presentationWeb, findsOneWidget); - addTearDown(tester.binding.window.clearPhysicalSizeTestValue); + addTearDown(tester.view.resetPhysicalSize); }, ); }); diff --git a/test/data/repositories/contact/contact_repository_impl_test.dart b/test/data/repositories/contact/contact_repository_impl_test.dart deleted file mode 100644 index 775b7458..00000000 --- a/test/data/repositories/contact/contact_repository_impl_test.dart +++ /dev/null @@ -1,67 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:http/http.dart' as http; -import 'package:mocktail/mocktail.dart'; - -import 'package:site/data/repositories/contact/contact.dart'; - -import '../../../utils/utils.dart'; - -void main() { - late MockFirebaseRemoteConfig mockFirebaseRemoteConfig; - late MockHttpClient mockHttpClient; - late ContactRepositoryImpl contactRepository; - - setUp(() { - mockFirebaseRemoteConfig = MockFirebaseRemoteConfig(); - mockHttpClient = MockHttpClient(); - - contactRepository = ContactRepositoryImpl( - firebaseRemoteConfig: mockFirebaseRemoteConfig, - httpClient: mockHttpClient, - ); - - when( - () => mockFirebaseRemoteConfig.getString('service_id'), - ).thenReturn('service_id'); - - when( - () => mockFirebaseRemoteConfig.getString('template_id'), - ).thenReturn('template_id'); - - when( - () => mockFirebaseRemoteConfig.getString('user_id'), - ).thenReturn('user_id'); - - when( - () => mockFirebaseRemoteConfig.getString('to_email'), - ).thenReturn('to_email'); - - when( - () => mockHttpClient.post( - any(), - headers: any(named: 'headers'), - body: any(named: 'body'), - ), - ).thenAnswer( - (_) async => http.Response( - '', - 200, - ), - ); - }); - - setUpAll(() { - registerFallbackValue(UriFake()); - }); - - test('ContactRepositoryImpl', () async { - expect( - contactRepository, - isNotNull, - ); - - await contactRepository.sendMail( - contact: AppFixtures().tContact, - ); - }); -} diff --git a/test/utils/fixtures/app_fixtures.dart b/test/utils/fixtures/app_fixtures.dart index 51093165..0f8b819e 100644 --- a/test/utils/fixtures/app_fixtures.dart +++ b/test/utils/fixtures/app_fixtures.dart @@ -1,10 +1,21 @@ -import 'package:site/data/models/models.dart'; +import 'package:site/app/features/contact/contact.dart'; + +import '../mocks/mock_app_texts.dart'; class AppFixtures { - final tContact = Contact( + final tContactUser = ContactUser( + name: 'felipecastrosales', + email: 'fakeemail@gmail.com', + message: 'Hello, World!', + subject: 'Hello, World!', + ); + + final tContactAnswer = ContactAnswer( name: 'felipecastrosales', email: 'fakeemail@gmail.com', message: 'Hello, World!', subject: 'Hello, World!', + statusCode: 200, + responseMessage: MockAppLocalizationsHelper.emailSendedWithSuccess, ); } diff --git a/test/utils/mocks/mock_app_texts.dart b/test/utils/mocks/mock_app_texts.dart new file mode 100644 index 00000000..f56b01b5 --- /dev/null +++ b/test/utils/mocks/mock_app_texts.dart @@ -0,0 +1,12 @@ +import 'package:mocktail/mocktail.dart'; +import 'package:site/app/core/l10n/l10n.dart'; + +class MockAppLocalizations extends Mock implements AppLocalizations {} + +class MockAppLocalizationsHelper { + static const emailSendedWithSuccess = 'Email sended with success'; + static const emailTooManyRequests = 'Email too many requests'; + static const emailUnauthorized = 'Email unauthorized'; + static const emailUnknowError = 'Email unknown error'; + static const emailNotSended = 'Email not sended'; +} diff --git a/test/utils/mocks/mock_contact_controller.dart b/test/utils/mocks/mock_contact_controller.dart deleted file mode 100644 index 8d03a1b0..00000000 --- a/test/utils/mocks/mock_contact_controller.dart +++ /dev/null @@ -1,5 +0,0 @@ -import 'package:mocktail/mocktail.dart'; - -import 'package:site/app/features/home/widgets/contact/controller/contact_controller.dart'; - -class MockContactController extends Mock implements ContactController {} diff --git a/test/utils/mocks/mock_contact_cubit.dart b/test/utils/mocks/mock_contact_cubit.dart new file mode 100644 index 00000000..98e226e3 --- /dev/null +++ b/test/utils/mocks/mock_contact_cubit.dart @@ -0,0 +1,5 @@ +import 'package:mocktail/mocktail.dart'; + +import 'package:site/app/features/contact/contact.dart'; + +class MockContactCubit extends Mock implements ContactCubit {} diff --git a/test/utils/mocks/mock_contact_repository.dart b/test/utils/mocks/mock_contact_repository.dart new file mode 100644 index 00000000..eed0d967 --- /dev/null +++ b/test/utils/mocks/mock_contact_repository.dart @@ -0,0 +1,4 @@ +import 'package:mocktail/mocktail.dart'; +import 'package:site/app/features/contact/contact.dart'; + +class MockContactRepository extends Mock implements ContactRepository {} diff --git a/test/utils/mocks/mock_http_client.dart b/test/utils/mocks/mock_http_client.dart index 254543e8..50480cc4 100644 --- a/test/utils/mocks/mock_http_client.dart +++ b/test/utils/mocks/mock_http_client.dart @@ -1,4 +1,4 @@ -import 'package:http/http.dart' as http; +import 'package:dio/dio.dart'; import 'package:mocktail/mocktail.dart'; -class MockHttpClient extends Mock implements http.Client {} +class MockHttpClient extends Mock implements Dio {} diff --git a/test/utils/mocks/mocks.dart b/test/utils/mocks/mocks.dart index b68f7754..c99c6290 100644 --- a/test/utils/mocks/mocks.dart +++ b/test/utils/mocks/mocks.dart @@ -1,3 +1,4 @@ -export 'mock_contact_controller.dart'; +export 'mock_contact_cubit.dart'; +export 'mock_contact_repository.dart'; export 'mock_firebase_remote_config.dart'; export 'mock_http_client.dart'; diff --git a/web/index.html b/web/index.html index 5aa5ed91..b2b724a2 100644 --- a/web/index.html +++ b/web/index.html @@ -35,6 +35,7 @@ Felipe Sales | Social Links + @@ -75,12 +76,12 @@