Added customer service and contact screens and state

This commit is contained in:
2026-01-28 17:36:01 +01:00
parent 12edcd0940
commit cd578352f9
27 changed files with 1115 additions and 10 deletions

2
.idea/modules.xml generated
View File

@@ -3,6 +3,7 @@
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/modules/auth/melos_auth.iml" filepath="$PROJECT_DIR$/modules/auth/melos_auth.iml" />
<module fileurl="file://$PROJECT_DIR$/modules/legacy/modules/customer_service/melos_customer_service.iml" filepath="$PROJECT_DIR$/modules/legacy/modules/customer_service/melos_customer_service.iml" />
<module fileurl="file://$PROJECT_DIR$/modules/dashboard_shell/melos_dashboard_shell.iml" filepath="$PROJECT_DIR$/modules/dashboard_shell/melos_dashboard_shell.iml" />
<module fileurl="file://$PROJECT_DIR$/packages/design_system/melos_design_system.iml" filepath="$PROJECT_DIR$/packages/design_system/melos_design_system.iml" />
<module fileurl="file://$PROJECT_DIR$/packages/fonts/melos_fonts.iml" filepath="$PROJECT_DIR$/packages/fonts/melos_fonts.iml" />
@@ -12,7 +13,6 @@
<module fileurl="file://$PROJECT_DIR$/modules/legacy/modules/legacy_dashboard_shell/melos_legacy_dashboard_shell.iml" filepath="$PROJECT_DIR$/modules/legacy/modules/legacy_dashboard_shell/melos_legacy_dashboard_shell.iml" />
<module fileurl="file://$PROJECT_DIR$/packages/navigation/melos_navigation.iml" filepath="$PROJECT_DIR$/packages/navigation/melos_navigation.iml" />
<module fileurl="file://$PROJECT_DIR$/modules/notifications/melos_notifications.iml" filepath="$PROJECT_DIR$/modules/notifications/melos_notifications.iml" />
<module fileurl="file://$PROJECT_DIR$/modules/payments_dashboard_shell/melos_payments_dashboard_shell.iml" filepath="$PROJECT_DIR$/modules/payments_dashboard_shell/melos_payments_dashboard_shell.iml" />
<module fileurl="file://$PROJECT_DIR$/modules/profile/melos_profile.iml" filepath="$PROJECT_DIR$/modules/profile/melos_profile.iml" />
<module fileurl="file://$PROJECT_DIR$/apps/mobile_app/melos_sf_app_platform.iml" filepath="$PROJECT_DIR$/apps/mobile_app/melos_sf_app_platform.iml" />
<module fileurl="file://$PROJECT_DIR$/packages/sf_infrastructure/melos_sf_infrastructure.iml" filepath="$PROJECT_DIR$/packages/sf_infrastructure/melos_sf_infrastructure.iml" />

View File

@@ -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',

View File

@@ -6,6 +6,10 @@
#include "generated_plugin_registrant.h"
#include <url_launcher_linux/url_launcher_plugin.h>
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);
}

View File

@@ -3,6 +3,7 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
url_launcher_linux
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST

View File

@@ -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

View File

@@ -17,10 +17,13 @@
<excludeFolder url="file://$MODULE_DIR$/macos/Flutter" />
<excludeFolder url="file://$MODULE_DIR$/macos/Pods" />
<excludeFolder url="file://$MODULE_DIR$/macos/.symlinks" />
<excludeFolder url="file://$MODULE_DIR$/modules/customer_service/.dart_tool" />
<excludeFolder url="file://$MODULE_DIR$/modules/customer_service/.pub" />
<excludeFolder url="file://$MODULE_DIR$/modules/customer_service/build" />
</content>
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="Dart SDK" level="project" />
<orderEntry type="library" name="Flutter Plugins" level="project" />
<orderEntry type="library" name="Dart Packages" level="project" />
</component>
</module>
</module>

View File

@@ -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<void> buildPage(BuildContext context, GoRouterState state) {
final NavigationContract navigationContract = GetIt.I<NavigationContract>();
return MaterialPage<void>(
key: state.pageKey,
child: CustomerServiceScreen(navigationContract: navigationContract),
);
}
}

View File

@@ -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;
}

View File

@@ -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>(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<SendEmailRequestEntity> get copyWith => _$SendEmailRequestEntityCopyWithImpl<SendEmailRequestEntity>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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

View File

@@ -0,0 +1,5 @@
import 'package:customer_service/src/domain/entities/send_email_request_entity.dart';
abstract class SendEmailUseCase {
Future<void> sendEmail({required SendEmailRequestEntity request});
}

View File

@@ -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<void> sendEmail({required SendEmailRequestEntity request}) async {
//return _repository.signUp(request: request);
}
}

View File

@@ -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<String, String> country = <String, String>{
'SPAIN': 'España',
'PORTUGAL': 'Portugal',
'FRANCE': 'France',
'ITALIA': 'Italia',
'GERMANY': 'Deutschland',
'OTHER': I18n.other,
};
const Map<String, String> channel = <String, String>{
'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)
)
],
)
)
)
),
);
}
}

View File

@@ -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
)
)
),
],
),
)
);
}
}

View File

@@ -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<SendEmailUseCase>((ref) {
//final authRepository = ref.read(customerServiceRepositoryProvider);
return SendEmailUseCaseImpl();
});

View File

@@ -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, ContactViewState>(
ContactViewModel.new,
);
class ContactViewModel extends Notifier<ContactViewState> {
//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();
}
}

View File

@@ -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;
}

View File

@@ -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>(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<ContactViewState> get copyWith => _$ContactViewStateCopyWithImpl<ContactViewState>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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

View File

@@ -0,0 +1,8 @@
/*
import 'package:flutter_riverpod/flutter_riverpod.dart';
final customerServiceRepositoryProvider = Provider<CustomerServiceRepository>((ref) {
final remote = ref.read(customerServiceRemoteDatasourceProvider);
return CustomerServiceRepositoryImpl(remote);
});
*/

View File

@@ -1 +0,0 @@
// TODO Implement this library.

View File

@@ -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)),

View File

@@ -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';

View File

@@ -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

View File

@@ -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';

View File

@@ -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!"
}

View File

@@ -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!"
}

View File

@@ -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';
}