diff --git a/.idea/modules.xml b/.idea/modules.xml index cfa0d987..de9529fa 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -3,6 +3,7 @@ + @@ -12,7 +13,6 @@ - diff --git a/apps/mobile_app/lib/navigation/app_router.dart b/apps/mobile_app/lib/navigation/app_router.dart index 485f4469..d6a540a0 100644 --- a/apps/mobile_app/lib/navigation/app_router.dart +++ b/apps/mobile_app/lib/navigation/app_router.dart @@ -1,4 +1,5 @@ import 'package:auth/auth.dart'; +import 'package:customer_service/customer_service.dart'; import 'package:legacy_dashboard_shell/legacy_dashboard_builder.dart'; import 'package:dashboard_shell/dashboard_builder.dart'; import 'package:flutter/material.dart'; @@ -43,6 +44,11 @@ void configureAppRouter() { ], ), + GoRoute( + path: AppRoutes.customerService, + name: 'customer_service', + pageBuilder: CustomerServiceBuilder().buildPage, + ), GoRoute( path: AppRoutes.login, name: 'login', diff --git a/apps/mobile_app/linux/flutter/generated_plugin_registrant.cc b/apps/mobile_app/linux/flutter/generated_plugin_registrant.cc index e71a16d2..f6f23bfe 100644 --- a/apps/mobile_app/linux/flutter/generated_plugin_registrant.cc +++ b/apps/mobile_app/linux/flutter/generated_plugin_registrant.cc @@ -6,6 +6,10 @@ #include "generated_plugin_registrant.h" +#include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); } diff --git a/apps/mobile_app/linux/flutter/generated_plugins.cmake b/apps/mobile_app/linux/flutter/generated_plugins.cmake index 2e1de87a..f16b4c34 100644 --- a/apps/mobile_app/linux/flutter/generated_plugins.cmake +++ b/apps/mobile_app/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + url_launcher_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/apps/mobile_app/pubspec.yaml b/apps/mobile_app/pubspec.yaml index 9fdc39d3..71f59dc4 100644 --- a/apps/mobile_app/pubspec.yaml +++ b/apps/mobile_app/pubspec.yaml @@ -41,8 +41,6 @@ dependencies: path: ../../modules/auth home: path: ../../modules/home - hub: - path: ../../modules/legacy/modules/hub profile: path: ../../modules/profile notifications: @@ -51,6 +49,10 @@ dependencies: path: ../../modules/dashboard_shell legacy_dashboard_shell: path: ../../modules/legacy/modules/legacy_dashboard_shell + hub: + path: ../../modules/legacy/modules/hub + customer_service: + path: ../../modules/legacy/modules/customer_service splash: path: ../../modules/splash #packages dependencies go here diff --git a/modules/legacy/melos_legacy.iml b/modules/legacy/melos_legacy.iml index 26a45e79..bb9b68c7 100644 --- a/modules/legacy/melos_legacy.iml +++ b/modules/legacy/melos_legacy.iml @@ -17,10 +17,13 @@ + + + - + \ No newline at end of file diff --git a/modules/legacy/modules/customer_service/lib/src/customer_service_builder.dart b/modules/legacy/modules/customer_service/lib/src/customer_service_builder.dart new file mode 100644 index 00000000..a5d4d885 --- /dev/null +++ b/modules/legacy/modules/customer_service/lib/src/customer_service_builder.dart @@ -0,0 +1,18 @@ +import 'package:customer_service/src/presentation/customer_service_screen.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:get_it/get_it.dart'; +import 'package:navigation/navigation.dart'; + +class CustomerServiceBuilder { + const CustomerServiceBuilder(); + + Page buildPage(BuildContext context, GoRouterState state) { + final NavigationContract navigationContract = GetIt.I(); + + return MaterialPage( + key: state.pageKey, + child: CustomerServiceScreen(navigationContract: navigationContract), + ); + } +} diff --git a/modules/legacy/modules/customer_service/lib/src/domain/entities/send_email_request_entity.dart b/modules/legacy/modules/customer_service/lib/src/domain/entities/send_email_request_entity.dart new file mode 100644 index 00000000..55204429 --- /dev/null +++ b/modules/legacy/modules/customer_service/lib/src/domain/entities/send_email_request_entity.dart @@ -0,0 +1,15 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'send_email_request_entity.freezed.dart'; + +@freezed +abstract class SendEmailRequestEntity with _$SendEmailRequestEntity{ + const factory SendEmailRequestEntity({ + required String country, + required String channel, + required String name, + required String email, + required String subject, + required String body, + }) = _SendEmailRequestEntity; +} \ No newline at end of file diff --git a/modules/legacy/modules/customer_service/lib/src/domain/entities/send_email_request_entity.freezed.dart b/modules/legacy/modules/customer_service/lib/src/domain/entities/send_email_request_entity.freezed.dart new file mode 100644 index 00000000..a92da8c7 --- /dev/null +++ b/modules/legacy/modules/customer_service/lib/src/domain/entities/send_email_request_entity.freezed.dart @@ -0,0 +1,286 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'send_email_request_entity.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; +/// @nodoc +mixin _$SendEmailRequestEntity { + + String get country; String get channel; String get name; String get email; String get subject; String get body; +/// Create a copy of SendEmailRequestEntity +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$SendEmailRequestEntityCopyWith get copyWith => _$SendEmailRequestEntityCopyWithImpl(this as SendEmailRequestEntity, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is SendEmailRequestEntity&&(identical(other.country, country) || other.country == country)&&(identical(other.channel, channel) || other.channel == channel)&&(identical(other.name, name) || other.name == name)&&(identical(other.email, email) || other.email == email)&&(identical(other.subject, subject) || other.subject == subject)&&(identical(other.body, body) || other.body == body)); +} + + +@override +int get hashCode => Object.hash(runtimeType,country,channel,name,email,subject,body); + +@override +String toString() { + return 'SendEmailRequestEntity(country: $country, channel: $channel, name: $name, email: $email, subject: $subject, body: $body)'; +} + + +} + +/// @nodoc +abstract mixin class $SendEmailRequestEntityCopyWith<$Res> { + factory $SendEmailRequestEntityCopyWith(SendEmailRequestEntity value, $Res Function(SendEmailRequestEntity) _then) = _$SendEmailRequestEntityCopyWithImpl; +@useResult +$Res call({ + String country, String channel, String name, String email, String subject, String body +}); + + + + +} +/// @nodoc +class _$SendEmailRequestEntityCopyWithImpl<$Res> + implements $SendEmailRequestEntityCopyWith<$Res> { + _$SendEmailRequestEntityCopyWithImpl(this._self, this._then); + + final SendEmailRequestEntity _self; + final $Res Function(SendEmailRequestEntity) _then; + +/// Create a copy of SendEmailRequestEntity +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? country = null,Object? channel = null,Object? name = null,Object? email = null,Object? subject = null,Object? body = null,}) { + return _then(_self.copyWith( +country: null == country ? _self.country : country // ignore: cast_nullable_to_non_nullable +as String,channel: null == channel ? _self.channel : channel // ignore: cast_nullable_to_non_nullable +as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable +as String,email: null == email ? _self.email : email // ignore: cast_nullable_to_non_nullable +as String,subject: null == subject ? _self.subject : subject // ignore: cast_nullable_to_non_nullable +as String,body: null == body ? _self.body : body // ignore: cast_nullable_to_non_nullable +as String, + )); +} + +} + + +/// Adds pattern-matching-related methods to [SendEmailRequestEntity]. +extension SendEmailRequestEntityPatterns on SendEmailRequestEntity { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _SendEmailRequestEntity value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _SendEmailRequestEntity() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _SendEmailRequestEntity value) $default,){ +final _that = this; +switch (_that) { +case _SendEmailRequestEntity(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _SendEmailRequestEntity value)? $default,){ +final _that = this; +switch (_that) { +case _SendEmailRequestEntity() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( String country, String channel, String name, String email, String subject, String body)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _SendEmailRequestEntity() when $default != null: +return $default(_that.country,_that.channel,_that.name,_that.email,_that.subject,_that.body);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( String country, String channel, String name, String email, String subject, String body) $default,) {final _that = this; +switch (_that) { +case _SendEmailRequestEntity(): +return $default(_that.country,_that.channel,_that.name,_that.email,_that.subject,_that.body);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String country, String channel, String name, String email, String subject, String body)? $default,) {final _that = this; +switch (_that) { +case _SendEmailRequestEntity() when $default != null: +return $default(_that.country,_that.channel,_that.name,_that.email,_that.subject,_that.body);case _: + return null; + +} +} + +} + +/// @nodoc + + +class _SendEmailRequestEntity implements SendEmailRequestEntity { + const _SendEmailRequestEntity({required this.country, required this.channel, required this.name, required this.email, required this.subject, required this.body}); + + +@override final String country; +@override final String channel; +@override final String name; +@override final String email; +@override final String subject; +@override final String body; + +/// Create a copy of SendEmailRequestEntity +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$SendEmailRequestEntityCopyWith<_SendEmailRequestEntity> get copyWith => __$SendEmailRequestEntityCopyWithImpl<_SendEmailRequestEntity>(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _SendEmailRequestEntity&&(identical(other.country, country) || other.country == country)&&(identical(other.channel, channel) || other.channel == channel)&&(identical(other.name, name) || other.name == name)&&(identical(other.email, email) || other.email == email)&&(identical(other.subject, subject) || other.subject == subject)&&(identical(other.body, body) || other.body == body)); +} + + +@override +int get hashCode => Object.hash(runtimeType,country,channel,name,email,subject,body); + +@override +String toString() { + return 'SendEmailRequestEntity(country: $country, channel: $channel, name: $name, email: $email, subject: $subject, body: $body)'; +} + + +} + +/// @nodoc +abstract mixin class _$SendEmailRequestEntityCopyWith<$Res> implements $SendEmailRequestEntityCopyWith<$Res> { + factory _$SendEmailRequestEntityCopyWith(_SendEmailRequestEntity value, $Res Function(_SendEmailRequestEntity) _then) = __$SendEmailRequestEntityCopyWithImpl; +@override @useResult +$Res call({ + String country, String channel, String name, String email, String subject, String body +}); + + + + +} +/// @nodoc +class __$SendEmailRequestEntityCopyWithImpl<$Res> + implements _$SendEmailRequestEntityCopyWith<$Res> { + __$SendEmailRequestEntityCopyWithImpl(this._self, this._then); + + final _SendEmailRequestEntity _self; + final $Res Function(_SendEmailRequestEntity) _then; + +/// Create a copy of SendEmailRequestEntity +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? country = null,Object? channel = null,Object? name = null,Object? email = null,Object? subject = null,Object? body = null,}) { + return _then(_SendEmailRequestEntity( +country: null == country ? _self.country : country // ignore: cast_nullable_to_non_nullable +as String,channel: null == channel ? _self.channel : channel // ignore: cast_nullable_to_non_nullable +as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable +as String,email: null == email ? _self.email : email // ignore: cast_nullable_to_non_nullable +as String,subject: null == subject ? _self.subject : subject // ignore: cast_nullable_to_non_nullable +as String,body: null == body ? _self.body : body // ignore: cast_nullable_to_non_nullable +as String, + )); +} + + +} + +// dart format on diff --git a/modules/legacy/modules/customer_service/lib/src/domain/repositories/customer_service_repository.dart b/modules/legacy/modules/customer_service/lib/src/domain/repositories/customer_service_repository.dart new file mode 100644 index 00000000..e69de29b diff --git a/modules/legacy/modules/customer_service/lib/src/domain/send_email_use_case.dart b/modules/legacy/modules/customer_service/lib/src/domain/send_email_use_case.dart new file mode 100644 index 00000000..e07870df --- /dev/null +++ b/modules/legacy/modules/customer_service/lib/src/domain/send_email_use_case.dart @@ -0,0 +1,5 @@ +import 'package:customer_service/src/domain/entities/send_email_request_entity.dart'; + +abstract class SendEmailUseCase { + Future sendEmail({required SendEmailRequestEntity request}); +} diff --git a/modules/legacy/modules/customer_service/lib/src/domain/send_email_use_case_impl.dart b/modules/legacy/modules/customer_service/lib/src/domain/send_email_use_case_impl.dart new file mode 100644 index 00000000..86002122 --- /dev/null +++ b/modules/legacy/modules/customer_service/lib/src/domain/send_email_use_case_impl.dart @@ -0,0 +1,13 @@ +import 'package:customer_service/src/domain/entities/send_email_request_entity.dart'; +import 'package:customer_service/src/domain/send_email_use_case.dart'; + +class SendEmailUseCaseImpl implements SendEmailUseCase { + //SignUpUseCaseImpl(this._repository); + + //final AuthRepository _repository; + + @override + Future sendEmail({required SendEmailRequestEntity request}) async { + //return _repository.signUp(request: request); + } +} diff --git a/modules/legacy/modules/customer_service/lib/src/presentation/contact_screen.dart b/modules/legacy/modules/customer_service/lib/src/presentation/contact_screen.dart new file mode 100644 index 00000000..d5507856 --- /dev/null +++ b/modules/legacy/modules/customer_service/lib/src/presentation/contact_screen.dart @@ -0,0 +1,122 @@ +import 'package:customer_service/src/presentation/state/contact_view_model.dart'; +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:navigation/navigation.dart'; +import 'package:sf_localizations/sf_localizations.dart'; +import 'package:utils/utils.dart'; + +const Map country = { + 'SPAIN': 'España', + 'PORTUGAL': 'Portugal', + 'FRANCE': 'France', + 'ITALIA': 'Italia', + 'GERMANY': 'Deutschland', + 'OTHER': I18n.other, +}; + +const Map channel = { + 'ONLINE_SHOP': I18n.channelOnline, + 'AMAZON': I18n.channelAmazon, + 'STORE': I18n.channelStore, + 'OTHER': I18n.other, +}; + +class ContactScreen extends ConsumerWidget { + final NavigationContract navigationContract; + + const ContactScreen({super.key, required this.navigationContract}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final theme = ref.watch(themePortProvider); + + final vm = ref.read(contactViewModelProvider.notifier); + final viewState = ref.watch(contactViewModelProvider); + + return Scaffold( + backgroundColor: theme.getColorFor(ThemeCode.backgroundPrimary), + body: SafeArea( + child: Container( + padding: SizeUtils.getByScreen( + small: EdgeInsets.symmetric(horizontal: 38, vertical: 14), + big: EdgeInsets.symmetric(horizontal: 36, vertical: 12) + ), + child: SingleChildScrollView( + child: Column( + children: [ + Stack( + children: [ + SizedBox( + height: SizeUtils.getByScreen(small: 36, big: 36), + child: Align( + alignment: Alignment.centerLeft, + child: Icon(Icons.arrow_back, size: SizeUtils.getByScreen(small: 36, big: 34)) + ) + ), + Center(child: Text(context.translate(I18n.contactTitle), + style: TextStyle(fontSize: SizeUtils.getByScreen(small: 28, big: 27)))) + ], + ), + SizedBox(height: SizeUtils.getByScreen(small: 40, big: 38)), + CustomDropdown( + items: country.values.map(Text.new).toList(growable: false), + values: country.keys.toList(), + onChanged: (x){vm.setCountry(x);}, + hint: 'Choose your country' + ), + SizedBox(height: SizeUtils.getByScreen(small: 18, big: 17)), + CustomDropdown( + items: channel.values.map(Text.new).toList(growable: false), + values: channel.keys.toList(), + onChanged: (x){vm.setChannel(x);}, + hint: 'Purchase channel' + ), + SizedBox(height: SizeUtils.getByScreen(small: 18, big: 17)), + CustomTextField( + controller: vm.nameController, + hint: 'Enter your name', + ), + SizedBox(height: SizeUtils.getByScreen(small: 18, big: 17)), + CustomTextField( + controller: vm.emailController, + keyboardType: TextInputType.emailAddress, + hint: 'Enter your email', + ), + SizedBox(height: SizeUtils.getByScreen(small: 18, big: 17)), + CustomTextField( + controller: vm.subjectController, + hint: 'Your message subject', + ), + SizedBox(height: SizeUtils.getByScreen(small: 18, big: 17)), + CustomTextField( + controller: vm.bodyController, + keyboardType: TextInputType.multiline, + hint: 'Your message', + lines: 8, + ), + if (viewState.errorMessage.isNotEmpty) ...[ + const SizedBox(height: 4), + Text( + viewState.errorMessage, + textAlign: TextAlign.center, + style: const TextStyle( + color: Color.fromRGBO(239, 17, 17, 1), + fontSize: 12, + ), + ), + ], + SizedBox(height: SizeUtils.getByScreen(small: 28, big: 27)), + PrimaryButton( + onPressed: vm.sendEmail, + text: 'Send!', + color: theme.getColorFor(ThemeCode.buttonPrimary) + ) + ], + ) + ) + ) + ), + ); + } +} \ No newline at end of file diff --git a/modules/legacy/modules/customer_service/lib/src/presentation/customer_service_screen.dart b/modules/legacy/modules/customer_service/lib/src/presentation/customer_service_screen.dart new file mode 100644 index 00000000..28f839ff --- /dev/null +++ b/modules/legacy/modules/customer_service/lib/src/presentation/customer_service_screen.dart @@ -0,0 +1,139 @@ +import 'package:customer_service/src/presentation/contact_screen.dart'; +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:navigation/navigation.dart'; +import 'package:sf_localizations/sf_localizations.dart'; +import 'package:utils/utils.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class CustomerServiceScreen extends ConsumerWidget { + final NavigationContract navigationContract; + + const CustomerServiceScreen({super.key, required this.navigationContract}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final theme = ref.watch(themePortProvider); + + return Scaffold( + backgroundColor: theme.getColorFor(ThemeCode.backgroundPrimary), + body: SafeArea( + child: Container( + padding: SizeUtils.getByScreen( + small: EdgeInsets.symmetric(horizontal: 18, vertical: 14), + big: EdgeInsets.symmetric(horizontal: 16, vertical: 12) + ), + child: Column( + children: [ + Stack( + children: [ + SizedBox( + height: SizeUtils.getByScreen(small: 36, big: 36), + child: Align( + alignment: Alignment.centerLeft, + child: Icon(Icons.arrow_back, size: SizeUtils.getByScreen(small: 36, big: 34)) + ) + ), + Center(child: Text(context.translate(I18n.customerService), + style: TextStyle(fontSize: SizeUtils.getByScreen(small: 28, big: 27)))) + ], + ), + SizedBox(height: SizeUtils.getByScreen(small: 40, big: 38)), + AppSectionButton( + onPressed: () async { + final Uri url = Uri.parse('https://www.savefamilygps.com/'); + if (!await launchUrl(url)) { + throw Exception('Could not launch $url'); + } + }, + icon: Icons.sunny, + text: "Visit our Website" + ), + SizedBox(height: SizeUtils.getByScreen(small: 10, big: 9)), + AppSectionButton( + onPressed: () async { + final Uri url = Uri.parse('https://savefamilygpshelp.zendesk.com/hc/es'); + if (!await launchUrl(url)) { + throw Exception('Could not launch $url'); + } + }, + icon: Icons.handshake_outlined, + text: "Can we help you?" + ), + SizedBox(height: SizeUtils.getByScreen(small: 10, big: 9)), + AppSectionButton( + onPressed: (){Navigator.push(context, + MaterialPageRoute( + builder: (_) => ContactScreen(navigationContract: navigationContract), + ));}, + icon: Icons.email_outlined, + text: context.translate(I18n.contactTitle) + ), + ], + ) + ) + ), + ); + } +} + + +class AppSectionButton extends ConsumerWidget { + + final GestureTapCallback onPressed; + final IconData icon; + final String text; + + const AppSectionButton({ + required this.onPressed, + required this.icon, + required this.text, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final theme = ref.read(themePortProvider); + + return GestureDetector( + onTap: onPressed, + child: Container( + padding: SizeUtils.getByScreen( + small: EdgeInsets.symmetric(horizontal: 22, vertical: 10), + big: EdgeInsets.symmetric(horizontal: 21, vertical: 8) + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.all( + Radius.circular(SizeUtils.getByScreen(small: 12, big: 18))), + color: theme.getColorFor(ThemeCode.backgroundSecondary), + ), + child: Row( + children: [ + Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + color: theme.getColorFor(ThemeCode.backgroundPrimary), + ), + padding: EdgeInsets.all( + SizeUtils.getByScreen(small: 4, big: 12)), + child: Icon(icon, + size: SizeUtils.getByScreen(small: 40, big: 44), + color: Color(0xFF588EA5), + weight: 30, + ), + ), + SizedBox(width: SizeUtils.getByScreen(small: 16, big: 15)), + Expanded( + child: Text(context.translate(text), + style: TextStyle( + fontSize: SizeUtils.getByScreen(small: 18, big: 19), + fontWeight: FontWeight.w500 + ) + ) + ), + ], + ), + ) + ); + } +} \ No newline at end of file diff --git a/modules/legacy/modules/customer_service/lib/src/presentation/providers/send_email_use_case_provider.dart b/modules/legacy/modules/customer_service/lib/src/presentation/providers/send_email_use_case_provider.dart new file mode 100644 index 00000000..b4fe853a --- /dev/null +++ b/modules/legacy/modules/customer_service/lib/src/presentation/providers/send_email_use_case_provider.dart @@ -0,0 +1,9 @@ +import 'package:customer_service/src/domain/send_email_use_case.dart'; +import 'package:customer_service/src/domain/send_email_use_case_impl.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +final sendEmailUseCaseProvider = +Provider.autoDispose((ref) { + //final authRepository = ref.read(customerServiceRepositoryProvider); + return SendEmailUseCaseImpl(); +}); diff --git a/modules/legacy/modules/customer_service/lib/src/presentation/state/contact_view_model.dart b/modules/legacy/modules/customer_service/lib/src/presentation/state/contact_view_model.dart new file mode 100644 index 00000000..6f804672 --- /dev/null +++ b/modules/legacy/modules/customer_service/lib/src/presentation/state/contact_view_model.dart @@ -0,0 +1,129 @@ +// import 'package:customer_service/src/domain/send_email_use_case.dart'; +// import 'package:customer_service/src/presentation/providers/send_email_use_case_provider.dart'; +import 'package:customer_service/src/presentation/state/contact_view_state.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:sf_localizations/sf_localizations.dart'; +import 'package:url_launcher/url_launcher.dart'; + +final contactViewModelProvider = +NotifierProvider.autoDispose( + ContactViewModel.new, +); + +class ContactViewModel extends Notifier { + //late final SendEmailUseCase _sendEmailUseCase; + + late final TextEditingController nameController; + late final TextEditingController emailController; + late final TextEditingController subjectController; + late final TextEditingController bodyController; + + static final RegExp _emailRegex = RegExp( + r'^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$', + caseSensitive: false, + ); + + @override + ContactViewState build() { + //_sendEmailUseCase = ref.read(sendEmailUseCaseProvider); + + nameController = TextEditingController(); + emailController = TextEditingController(); + subjectController = TextEditingController(); + bodyController = TextEditingController(); + + nameController.addListener(_onNameChanged); + emailController.addListener(_onEmailChanged); + subjectController.addListener(_onSubjectChanged); + bodyController.addListener(_onBodyChanged); + + ref.onDispose(disposeControllers); + + return const ContactViewState(); + } + + void setCountry(String value) { + if (value == state.country) return; + + state = state.copyWith(country: value, errorMessage: ''); + } + + void setChannel(String value) { + if (value == state.channel) return; + + state = state.copyWith(channel: value, errorMessage: ''); + } + + void _onNameChanged() { + final text = nameController.text; + if (text == state.name) return; + + state = state.copyWith(name: text, errorMessage: ''); + } + + void _onEmailChanged() { + final text = emailController.text; + if (text == state.email) return; + + state = state.copyWith(email: text, errorMessage: ''); + state = state.copyWith(emailError: _emailErrorFor(text)); + } + + bool _isValidEmail(String email) => _emailRegex.hasMatch(email); + + String _emailErrorFor(String value) { + final email = value.trim(); + if (email.isEmpty) return I18n.errorEmailRequired; + if (!_isValidEmail(email)) return I18n.errorEmailInvalid; + return ''; + } + + void _onSubjectChanged() { + final text = subjectController.text; + if (text == state.subject) return; + + state = state.copyWith(subject: text, errorMessage: ''); + } + + void _onBodyChanged() { + final text = bodyController.text; + if (text == state.body) return; + + state = state.copyWith(body: text, errorMessage: ''); + } + + void sendEmail() async { + final receiver = 'aitorarana@savefamilygps.com'; + + //final name = state.name; + final sender = state.email; + final subject = state.subject; + final body = state.body; + + if (sender.isEmpty) { + state = state.copyWith(errorMessage: I18n.errorEmailRequired); + return; + } + if (_isValidEmail(sender)) { + state = state.copyWith(errorMessage: I18n.errorEmailInvalid); + } + + final Uri url = Uri.parse('mailto:$receiver?from=$sender&subject=$subject&body=$body'); + if (!await launchUrl(url)) { + throw Exception('Could not launch $url'); + } + } + + void disposeControllers() { + nameController.removeListener(_onNameChanged); + emailController.removeListener(_onEmailChanged); + subjectController.removeListener(_onSubjectChanged); + bodyController.removeListener(_onBodyChanged); + + nameController.dispose(); + emailController.dispose(); + subjectController.dispose(); + bodyController.dispose(); + } +} \ No newline at end of file diff --git a/modules/legacy/modules/customer_service/lib/src/presentation/state/contact_view_state.dart b/modules/legacy/modules/customer_service/lib/src/presentation/state/contact_view_state.dart new file mode 100644 index 00000000..08133560 --- /dev/null +++ b/modules/legacy/modules/customer_service/lib/src/presentation/state/contact_view_state.dart @@ -0,0 +1,17 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'contact_view_state.freezed.dart'; + +@freezed +abstract class ContactViewState with _$ContactViewState{ + const factory ContactViewState({ + @Default('') String country, + @Default('') String channel, + @Default('') String name, + @Default('') String email, + @Default('') String subject, + @Default('') String body, + @Default('') String errorMessage, + @Default('') String emailError, + }) = _ContactViewState; +} \ No newline at end of file diff --git a/modules/legacy/modules/customer_service/lib/src/presentation/state/contact_view_state.freezed.dart b/modules/legacy/modules/customer_service/lib/src/presentation/state/contact_view_state.freezed.dart new file mode 100644 index 00000000..31f58ae1 --- /dev/null +++ b/modules/legacy/modules/customer_service/lib/src/presentation/state/contact_view_state.freezed.dart @@ -0,0 +1,292 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'contact_view_state.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; +/// @nodoc +mixin _$ContactViewState { + + String get country; String get channel; String get name; String get email; String get subject; String get body; String get errorMessage; String get emailError; +/// Create a copy of ContactViewState +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$ContactViewStateCopyWith get copyWith => _$ContactViewStateCopyWithImpl(this as ContactViewState, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is ContactViewState&&(identical(other.country, country) || other.country == country)&&(identical(other.channel, channel) || other.channel == channel)&&(identical(other.name, name) || other.name == name)&&(identical(other.email, email) || other.email == email)&&(identical(other.subject, subject) || other.subject == subject)&&(identical(other.body, body) || other.body == body)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.emailError, emailError) || other.emailError == emailError)); +} + + +@override +int get hashCode => Object.hash(runtimeType,country,channel,name,email,subject,body,errorMessage,emailError); + +@override +String toString() { + return 'ContactViewState(country: $country, channel: $channel, name: $name, email: $email, subject: $subject, body: $body, errorMessage: $errorMessage, emailError: $emailError)'; +} + + +} + +/// @nodoc +abstract mixin class $ContactViewStateCopyWith<$Res> { + factory $ContactViewStateCopyWith(ContactViewState value, $Res Function(ContactViewState) _then) = _$ContactViewStateCopyWithImpl; +@useResult +$Res call({ + String country, String channel, String name, String email, String subject, String body, String errorMessage, String emailError +}); + + + + +} +/// @nodoc +class _$ContactViewStateCopyWithImpl<$Res> + implements $ContactViewStateCopyWith<$Res> { + _$ContactViewStateCopyWithImpl(this._self, this._then); + + final ContactViewState _self; + final $Res Function(ContactViewState) _then; + +/// Create a copy of ContactViewState +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? country = null,Object? channel = null,Object? name = null,Object? email = null,Object? subject = null,Object? body = null,Object? errorMessage = null,Object? emailError = null,}) { + return _then(_self.copyWith( +country: null == country ? _self.country : country // ignore: cast_nullable_to_non_nullable +as String,channel: null == channel ? _self.channel : channel // ignore: cast_nullable_to_non_nullable +as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable +as String,email: null == email ? _self.email : email // ignore: cast_nullable_to_non_nullable +as String,subject: null == subject ? _self.subject : subject // ignore: cast_nullable_to_non_nullable +as String,body: null == body ? _self.body : body // ignore: cast_nullable_to_non_nullable +as String,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable +as String,emailError: null == emailError ? _self.emailError : emailError // ignore: cast_nullable_to_non_nullable +as String, + )); +} + +} + + +/// Adds pattern-matching-related methods to [ContactViewState]. +extension ContactViewStatePatterns on ContactViewState { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _ContactViewState value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _ContactViewState() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _ContactViewState value) $default,){ +final _that = this; +switch (_that) { +case _ContactViewState(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _ContactViewState value)? $default,){ +final _that = this; +switch (_that) { +case _ContactViewState() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( String country, String channel, String name, String email, String subject, String body, String errorMessage, String emailError)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _ContactViewState() when $default != null: +return $default(_that.country,_that.channel,_that.name,_that.email,_that.subject,_that.body,_that.errorMessage,_that.emailError);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( String country, String channel, String name, String email, String subject, String body, String errorMessage, String emailError) $default,) {final _that = this; +switch (_that) { +case _ContactViewState(): +return $default(_that.country,_that.channel,_that.name,_that.email,_that.subject,_that.body,_that.errorMessage,_that.emailError);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String country, String channel, String name, String email, String subject, String body, String errorMessage, String emailError)? $default,) {final _that = this; +switch (_that) { +case _ContactViewState() when $default != null: +return $default(_that.country,_that.channel,_that.name,_that.email,_that.subject,_that.body,_that.errorMessage,_that.emailError);case _: + return null; + +} +} + +} + +/// @nodoc + + +class _ContactViewState implements ContactViewState { + const _ContactViewState({this.country = '', this.channel = '', this.name = '', this.email = '', this.subject = '', this.body = '', this.errorMessage = '', this.emailError = ''}); + + +@override@JsonKey() final String country; +@override@JsonKey() final String channel; +@override@JsonKey() final String name; +@override@JsonKey() final String email; +@override@JsonKey() final String subject; +@override@JsonKey() final String body; +@override@JsonKey() final String errorMessage; +@override@JsonKey() final String emailError; + +/// Create a copy of ContactViewState +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$ContactViewStateCopyWith<_ContactViewState> get copyWith => __$ContactViewStateCopyWithImpl<_ContactViewState>(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _ContactViewState&&(identical(other.country, country) || other.country == country)&&(identical(other.channel, channel) || other.channel == channel)&&(identical(other.name, name) || other.name == name)&&(identical(other.email, email) || other.email == email)&&(identical(other.subject, subject) || other.subject == subject)&&(identical(other.body, body) || other.body == body)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.emailError, emailError) || other.emailError == emailError)); +} + + +@override +int get hashCode => Object.hash(runtimeType,country,channel,name,email,subject,body,errorMessage,emailError); + +@override +String toString() { + return 'ContactViewState(country: $country, channel: $channel, name: $name, email: $email, subject: $subject, body: $body, errorMessage: $errorMessage, emailError: $emailError)'; +} + + +} + +/// @nodoc +abstract mixin class _$ContactViewStateCopyWith<$Res> implements $ContactViewStateCopyWith<$Res> { + factory _$ContactViewStateCopyWith(_ContactViewState value, $Res Function(_ContactViewState) _then) = __$ContactViewStateCopyWithImpl; +@override @useResult +$Res call({ + String country, String channel, String name, String email, String subject, String body, String errorMessage, String emailError +}); + + + + +} +/// @nodoc +class __$ContactViewStateCopyWithImpl<$Res> + implements _$ContactViewStateCopyWith<$Res> { + __$ContactViewStateCopyWithImpl(this._self, this._then); + + final _ContactViewState _self; + final $Res Function(_ContactViewState) _then; + +/// Create a copy of ContactViewState +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? country = null,Object? channel = null,Object? name = null,Object? email = null,Object? subject = null,Object? body = null,Object? errorMessage = null,Object? emailError = null,}) { + return _then(_ContactViewState( +country: null == country ? _self.country : country // ignore: cast_nullable_to_non_nullable +as String,channel: null == channel ? _self.channel : channel // ignore: cast_nullable_to_non_nullable +as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable +as String,email: null == email ? _self.email : email // ignore: cast_nullable_to_non_nullable +as String,subject: null == subject ? _self.subject : subject // ignore: cast_nullable_to_non_nullable +as String,body: null == body ? _self.body : body // ignore: cast_nullable_to_non_nullable +as String,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable +as String,emailError: null == emailError ? _self.emailError : emailError // ignore: cast_nullable_to_non_nullable +as String, + )); +} + + +} + +// dart format on diff --git a/modules/legacy/modules/customer_service/lib/src/providers/customer_service_repository_provider.dart b/modules/legacy/modules/customer_service/lib/src/providers/customer_service_repository_provider.dart new file mode 100644 index 00000000..a8d0bd56 --- /dev/null +++ b/modules/legacy/modules/customer_service/lib/src/providers/customer_service_repository_provider.dart @@ -0,0 +1,8 @@ +/* +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +final customerServiceRepositoryProvider = Provider((ref) { + final remote = ref.read(customerServiceRemoteDatasourceProvider); + return CustomerServiceRepositoryImpl(remote); +}); +*/ diff --git a/modules/legacy/modules/dashboard_shell/lib/legacy_dashboard_builder.dart b/modules/legacy/modules/dashboard_shell/lib/legacy_dashboard_builder.dart deleted file mode 100644 index b9f78bb0..00000000 --- a/modules/legacy/modules/dashboard_shell/lib/legacy_dashboard_builder.dart +++ /dev/null @@ -1 +0,0 @@ -// TODO Implement this library. \ No newline at end of file diff --git a/modules/legacy/modules/hub/lib/src/features/hub/presentation/hub_screen.dart b/modules/legacy/modules/hub/lib/src/features/hub/presentation/hub_screen.dart index 47eb7af8..408f99ab 100644 --- a/modules/legacy/modules/hub/lib/src/features/hub/presentation/hub_screen.dart +++ b/modules/legacy/modules/hub/lib/src/features/hub/presentation/hub_screen.dart @@ -51,7 +51,7 @@ class HubScreen extends ConsumerWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ AppSectionButton( - onPressed: (){}, + onPressed: (){navigationContract.pushTo(AppRoutes.customerService);}, icon: SFIcons.customerService, text: I18n.customerService), SizedBox(height: SizeUtils.getByScreen(small: 8, big: 7)), diff --git a/modules/legacy/modules/hub/lib/src/features/hub/presentation/state/hub_view_state.dart b/modules/legacy/modules/hub/lib/src/features/hub/presentation/state/hub_view_state.dart index f7968fa4..a1c6dbde 100644 --- a/modules/legacy/modules/hub/lib/src/features/hub/presentation/state/hub_view_state.dart +++ b/modules/legacy/modules/hub/lib/src/features/hub/presentation/state/hub_view_state.dart @@ -1,7 +1,6 @@ import 'package:hub/src/features/hub/domain/entities/device_entity.dart'; import 'package:hub/src/features/hub/domain/entities/position_entity.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:latlong2/latlong.dart'; part 'hub_view_state.freezed.dart'; diff --git a/modules/legacy/modules/hub/pubspec.yaml b/modules/legacy/modules/hub/pubspec.yaml index 44a59922..aa7ceb59 100644 --- a/modules/legacy/modules/hub/pubspec.yaml +++ b/modules/legacy/modules/hub/pubspec.yaml @@ -33,7 +33,7 @@ dependencies: sdk: flutter #modules dependencies go here dashboard_shell: - path: ../../modules/dashboard_shell + path: ../../../../modules/dashboard_shell #packages dependencies go here design_system: path: ../../../../packages/design_system diff --git a/packages/navigation/lib/app_routes.dart b/packages/navigation/lib/app_routes.dart index 45ba35c8..c4ddab10 100644 --- a/packages/navigation/lib/app_routes.dart +++ b/packages/navigation/lib/app_routes.dart @@ -17,6 +17,8 @@ class AppRoutes { static const legacy = '/legacy'; + static const customerService = '$legacy/customer_service'; + static const legacyDashboard = '$legacy/dashboard'; static const dashboardHub = '$legacyDashboard/hub'; diff --git a/packages/sf_localizations/assets/l10n/en.json b/packages/sf_localizations/assets/l10n/en.json index b58dd5dc..4ea242cc 100755 --- a/packages/sf_localizations/assets/l10n/en.json +++ b/packages/sf_localizations/assets/l10n/en.json @@ -139,5 +139,17 @@ "watchesOnMap": "Smartwatch on the map", "home": "Home", "location": "Location", - "chat": "Chat" + "chat": "Chat", + "channelOnline": "SF online shop", + "channelAmazon": "Amazon", + "channelStore": "Physical store", + "other": "Other", + "contactTitle": "Contact us", + "selectCountry": "Choose your country", + "selectChannel": "Purchase channel", + "enterName": "Enter your name", + "enterEmail": "Enter your email", + "enterSubject": "Your message subject", + "enterMessage": "Your message", + "sendEmail": "Send!" } \ No newline at end of file diff --git a/packages/sf_localizations/assets/l10n/es.json b/packages/sf_localizations/assets/l10n/es.json index c2e75c56..48151842 100644 --- a/packages/sf_localizations/assets/l10n/es.json +++ b/packages/sf_localizations/assets/l10n/es.json @@ -139,5 +139,17 @@ "watchesOnMap": "Reloj inteligente en el mapa", "home": "Inicio", "location": "Mapa", - "chat": "Chat" + "chat": "Chat", + "channelOnline": "Tienda online SF", + "channelAmazon": "Amazon", + "channelStore": "Tienda física", + "other": "Otro", + "contactTitle": "Contacta con nosotros", + "selectCountry": "Selecciona tu país", + "selectChannel": "Caal de compra", + "enterName": "Introduce tu nombre", + "enterEmail": "Introduce tu correo electrónico", + "enterSubject": "Asunto del mensaje", + "enterMessage": "Tu mensaje", + "sendEmail": "!Enviar!" } \ No newline at end of file diff --git a/packages/sf_localizations/lib/src/generated/i18n.dart b/packages/sf_localizations/lib/src/generated/i18n.dart index bd01ab75..764d1d8a 100755 --- a/packages/sf_localizations/lib/src/generated/i18n.dart +++ b/packages/sf_localizations/lib/src/generated/i18n.dart @@ -170,4 +170,16 @@ class I18n { static const String home = 'home'; static const String location = 'location'; static const String chat = 'chat'; + static const String channelOnline = 'channelOnline'; + static const String channelAmazon = 'channelAmazon'; + static const String channelStore = 'channelStore'; + static const String other = 'other'; + static const String contactTitle = 'contactTitle'; + static const String selectCountry = 'selectCountry'; + static const String selectChannel = 'selectChannel'; + static const String enterName = 'enterName'; + static const String enterEmail = 'enterEmail'; + static const String enterSubject = 'enterSubject'; + static const String enterMessage = 'enterMessage'; + static const String sendEmail = 'sendEmail'; }