refactor(legacy-customer-service): migrate contact form to AsyncNotifier
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
import 'package:sf_localizations/sf_localizations.dart';
|
||||
|
||||
class EmailValidator {
|
||||
const EmailValidator._();
|
||||
|
||||
static final _regex = RegExp(
|
||||
r'^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$',
|
||||
caseSensitive: false,
|
||||
);
|
||||
|
||||
static String? validate(String value) {
|
||||
final email = value.trim();
|
||||
if (email.isEmpty) return I18n.errorEmailRequired;
|
||||
if (!_regex.hasMatch(email)) return I18n.errorEmailInvalid;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,78 @@
|
||||
import 'package:customer_service/src/presentation/state/contact_view_model.dart';
|
||||
import 'package:customer_service/src/domain/email_validator.dart';
|
||||
import 'package:customer_service/src/presentation/providers/contact_controller.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:legacy_theme/legacy_theme.dart';
|
||||
import 'package:legacy_ui/legacy_ui.dart';
|
||||
import 'package:navigation/navigation.dart';
|
||||
import 'package:sf_localizations/sf_localizations.dart';
|
||||
import 'package:sf_shared/sf_shared.dart';
|
||||
import 'package:utils/utils.dart';
|
||||
|
||||
class ContactScreen extends ConsumerWidget {
|
||||
class ContactScreen extends ConsumerStatefulWidget {
|
||||
final NavigationContract navigationContract;
|
||||
|
||||
const ContactScreen({super.key, required this.navigationContract});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
ConsumerState<ContactScreen> createState() => _ContactScreenState();
|
||||
}
|
||||
|
||||
final viewModel = ref.read(contactViewModelProvider.notifier);
|
||||
class _ContactScreenState extends ConsumerState<ContactScreen> {
|
||||
late final TextEditingController _nameController;
|
||||
late final TextEditingController _emailController;
|
||||
late final TextEditingController _subjectController;
|
||||
late final TextEditingController _bodyController;
|
||||
String _country = '';
|
||||
String _channel = '';
|
||||
String? _localError;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_nameController = TextEditingController();
|
||||
_emailController = TextEditingController();
|
||||
_subjectController = TextEditingController();
|
||||
_bodyController = TextEditingController();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nameController.dispose();
|
||||
_emailController.dispose();
|
||||
_subjectController.dispose();
|
||||
_bodyController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onSend() {
|
||||
final emailError = EmailValidator.validate(_emailController.text);
|
||||
if (emailError != null) {
|
||||
setState(() => _localError = emailError);
|
||||
return;
|
||||
}
|
||||
setState(() => _localError = null);
|
||||
|
||||
ref.read(contactControllerProvider.notifier).sendEmail(
|
||||
country: _country,
|
||||
channel: _channel,
|
||||
name: _nameController.text,
|
||||
email: _emailController.text.trim(),
|
||||
subject: _subjectController.text,
|
||||
message: _bodyController.text,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
ref.listen(contactControllerProvider, (_, next) {
|
||||
next.showErrorOn(context);
|
||||
});
|
||||
|
||||
final isLoading = ref.watch(
|
||||
contactControllerProvider.select((s) => s.isLoading),
|
||||
);
|
||||
|
||||
return LegacyPageLayout(
|
||||
title: context.translate(I18n.contactTitle),
|
||||
@@ -26,33 +83,76 @@ class ContactScreen extends ConsumerWidget {
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
const _CountrySection(),
|
||||
_CountrySection(
|
||||
onChanged: (value) => setState(() => _country = value),
|
||||
),
|
||||
SizedBox(height: SizeUtils.getByScreen(small: 18, big: 17)),
|
||||
const _ChannelSection(),
|
||||
_ChannelSection(
|
||||
onChanged: (value) => setState(() => _channel = value),
|
||||
),
|
||||
SizedBox(height: SizeUtils.getByScreen(small: 18, big: 17)),
|
||||
const _NameSection(),
|
||||
CustomTextField(
|
||||
controller: _nameController,
|
||||
hint: context.translate(I18n.enterName),
|
||||
),
|
||||
SizedBox(height: SizeUtils.getByScreen(small: 18, big: 17)),
|
||||
const _EmailSection(),
|
||||
CustomTextField(
|
||||
controller: _emailController,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
hint: context.translate(I18n.enterEmail),
|
||||
),
|
||||
SizedBox(height: SizeUtils.getByScreen(small: 18, big: 17)),
|
||||
const _SubjectSection(),
|
||||
CustomTextField(
|
||||
controller: _subjectController,
|
||||
hint: context.translate(I18n.enterSubject),
|
||||
),
|
||||
SizedBox(height: SizeUtils.getByScreen(small: 18, big: 17)),
|
||||
_MessageSection(onSubmit: viewModel.sendEmail),
|
||||
const _ErrorMessageSection(),
|
||||
CustomTextField(
|
||||
controller: _bodyController,
|
||||
keyboardType: TextInputType.multiline,
|
||||
hint: context.translate(I18n.enterMessage),
|
||||
lines: 8,
|
||||
onSubmitted: (_) => _onSend(),
|
||||
),
|
||||
if (_localError != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
child: Text(
|
||||
context.translate(_localError!),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
footer: _SendSection(onSend: viewModel.sendEmail),
|
||||
footer: Container(
|
||||
padding: SizeUtils.getByScreen(
|
||||
small: const EdgeInsets.symmetric(horizontal: 38, vertical: 14),
|
||||
big: const EdgeInsets.symmetric(horizontal: 36, vertical: 12),
|
||||
),
|
||||
child: PrimaryButton(
|
||||
onPressed: isLoading ? null : _onSend,
|
||||
text: context.translate(I18n.sendEmail),
|
||||
color: context.sfColors.legacyPrimary,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CountrySection extends ConsumerWidget {
|
||||
const _CountrySection();
|
||||
class _CountrySection extends StatelessWidget {
|
||||
final ValueChanged<String> onChanged;
|
||||
|
||||
const _CountrySection({required this.onChanged});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final List<String> countries = [
|
||||
Widget build(BuildContext context) {
|
||||
final countries = [
|
||||
'España',
|
||||
'Portugal',
|
||||
'France',
|
||||
@@ -61,149 +161,32 @@ class _CountrySection extends ConsumerWidget {
|
||||
context.translate(I18n.other),
|
||||
];
|
||||
|
||||
final vm = ref.read(contactViewModelProvider.notifier);
|
||||
|
||||
return CustomDropdown(
|
||||
items: countries.map(Text.new).toList(growable: false),
|
||||
onChanged: (x) {
|
||||
vm.setCountry(x);
|
||||
},
|
||||
onChanged: (value) => onChanged(value as String),
|
||||
hint: context.translate(I18n.selectCountry),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ChannelSection extends ConsumerWidget {
|
||||
const _ChannelSection();
|
||||
class _ChannelSection extends StatelessWidget {
|
||||
final ValueChanged<String> onChanged;
|
||||
|
||||
const _ChannelSection({required this.onChanged});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final List<String> channels = [
|
||||
Widget build(BuildContext context) {
|
||||
final channels = [
|
||||
context.translate(I18n.channelOnline),
|
||||
context.translate(I18n.channelAmazon),
|
||||
context.translate(I18n.channelStore),
|
||||
context.translate(I18n.other),
|
||||
];
|
||||
|
||||
final vm = ref.read(contactViewModelProvider.notifier);
|
||||
|
||||
return CustomDropdown(
|
||||
items: channels.map(Text.new).toList(growable: false),
|
||||
onChanged: (x) {
|
||||
vm.setChannel(x);
|
||||
},
|
||||
onChanged: (value) => onChanged(value as String),
|
||||
hint: context.translate(I18n.selectChannel),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _NameSection extends ConsumerWidget {
|
||||
const _NameSection();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final vm = ref.read(contactViewModelProvider.notifier);
|
||||
|
||||
return CustomTextField(
|
||||
controller: vm.nameController,
|
||||
hint: context.translate(I18n.enterName),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _EmailSection extends ConsumerWidget {
|
||||
const _EmailSection();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final vm = ref.read(contactViewModelProvider.notifier);
|
||||
|
||||
return CustomTextField(
|
||||
controller: vm.emailController,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
hint: context.translate(I18n.enterEmail),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SubjectSection extends ConsumerWidget {
|
||||
const _SubjectSection();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final vm = ref.read(contactViewModelProvider.notifier);
|
||||
|
||||
return CustomTextField(
|
||||
controller: vm.subjectController,
|
||||
hint: context.translate(I18n.enterSubject),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MessageSection extends ConsumerWidget {
|
||||
final VoidCallback onSubmit;
|
||||
|
||||
const _MessageSection({required this.onSubmit});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final vm = ref.read(contactViewModelProvider.notifier);
|
||||
|
||||
return CustomTextField(
|
||||
controller: vm.bodyController,
|
||||
keyboardType: TextInputType.multiline,
|
||||
hint: context.translate(I18n.enterMessage),
|
||||
lines: 8,
|
||||
onSubmitted: (_) => onSubmit(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ErrorMessageSection extends ConsumerWidget {
|
||||
const _ErrorMessageSection();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final viewState = ref.watch(contactViewModelProvider);
|
||||
|
||||
if (viewState.errorMessage.isNotEmpty) {
|
||||
return Column(
|
||||
children: [
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
viewState.errorMessage,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return SizedBox.shrink();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _SendSection extends ConsumerWidget {
|
||||
final VoidCallback onSend;
|
||||
|
||||
const _SendSection({required this.onSend});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
|
||||
return Container(
|
||||
padding: SizeUtils.getByScreen(
|
||||
small: EdgeInsets.symmetric(horizontal: 38, vertical: 14),
|
||||
big: EdgeInsets.symmetric(horizontal: 36, vertical: 12),
|
||||
),
|
||||
child: PrimaryButton(
|
||||
onPressed: onSend,
|
||||
text: context.translate(I18n.sendEmail),
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:sf_shared/sf_shared.dart';
|
||||
import 'package:sf_tracking/sf_tracking.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
part 'contact_controller.g.dart';
|
||||
|
||||
@riverpod
|
||||
class ContactController extends _$ContactController {
|
||||
@override
|
||||
FutureOr<void> build() {}
|
||||
|
||||
Future<void> sendEmail({
|
||||
required String country,
|
||||
required String channel,
|
||||
required String name,
|
||||
required String email,
|
||||
required String subject,
|
||||
required String message,
|
||||
}) async {
|
||||
state = const AsyncLoading();
|
||||
state = await AsyncValue.guard(() async {
|
||||
unawaited(
|
||||
ref.read(sfTrackingProvider).legacySupportContactInitiated(
|
||||
channel: 'email',
|
||||
country: country.isEmpty ? 'unknown' : country,
|
||||
),
|
||||
);
|
||||
|
||||
final body =
|
||||
'country:$country\n'
|
||||
'Purchase channel:$channel\n'
|
||||
'name:$name\n'
|
||||
'email:$email\n'
|
||||
'$message';
|
||||
|
||||
final url = Uri.parse(
|
||||
'mailto:${BrandLinks.supportEmail}'
|
||||
'?from=$email&subject=$subject&body=$body',
|
||||
);
|
||||
|
||||
if (!await launchUrl(url)) {
|
||||
throw Exception('Could not launch $url');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'contact_controller.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(ContactController)
|
||||
const contactControllerProvider = ContactControllerProvider._();
|
||||
|
||||
final class ContactControllerProvider
|
||||
extends $AsyncNotifierProvider<ContactController, void> {
|
||||
const ContactControllerProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'contactControllerProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$contactControllerHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
ContactController create() => ContactController();
|
||||
}
|
||||
|
||||
String _$contactControllerHash() => r'5c3ab6fe20cc95013f73788332a2707de7e0f6de';
|
||||
|
||||
abstract class _$ContactController extends $AsyncNotifier<void> {
|
||||
FutureOr<void> build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
build();
|
||||
final ref = this.ref as $Ref<AsyncValue<void>, void>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<AsyncValue<void>, void>,
|
||||
AsyncValue<void>,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleValue(ref, null);
|
||||
}
|
||||
}
|
||||
@@ -1,150 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
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:sf_shared/sf_shared.dart';
|
||||
import 'package:sf_tracking/sf_tracking.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
final contactViewModelProvider =
|
||||
NotifierProvider.autoDispose<ContactViewModel, ContactViewState>(
|
||||
ContactViewModel.new,
|
||||
);
|
||||
|
||||
class ContactViewModel extends Notifier<ContactViewState> {
|
||||
late final TextEditingController nameController;
|
||||
late final TextEditingController emailController;
|
||||
late final TextEditingController subjectController;
|
||||
late final TextEditingController bodyController;
|
||||
late final SfTrackingRepository _tracking;
|
||||
|
||||
static final RegExp _emailRegex = RegExp(
|
||||
r'^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$',
|
||||
caseSensitive: false,
|
||||
);
|
||||
|
||||
@override
|
||||
ContactViewState build() {
|
||||
_tracking = ref.read(sfTrackingProvider);
|
||||
|
||||
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.message) return;
|
||||
|
||||
state = state.copyWith(message: text, errorMessage: '');
|
||||
}
|
||||
|
||||
void sendEmail() async {
|
||||
final receiver = BrandLinks.supportEmail;
|
||||
|
||||
final name = state.name;
|
||||
final country = state.country;
|
||||
final channel = state.channel;
|
||||
final sender = state.email;
|
||||
final subject = state.subject;
|
||||
final message = state.message;
|
||||
|
||||
if (sender.isEmpty) {
|
||||
state = state.copyWith(errorMessage: I18n.errorEmailRequired);
|
||||
return;
|
||||
}
|
||||
if (!_isValidEmail(sender)) {
|
||||
state = state.copyWith(errorMessage: I18n.errorEmailInvalid);
|
||||
return;
|
||||
}
|
||||
|
||||
final body =
|
||||
'country:$country\n'
|
||||
'Purchase channel:$channel\n'
|
||||
'name:$name\n'
|
||||
'email:$sender\n'
|
||||
'$message';
|
||||
|
||||
final Uri url = Uri.parse(
|
||||
'mailto:$receiver?from=$sender&subject=$subject&body=$body',
|
||||
);
|
||||
|
||||
unawaited(
|
||||
_tracking.legacySupportContactInitiated(
|
||||
channel: 'email',
|
||||
country: country.isEmpty ? 'unknown' : country,
|
||||
),
|
||||
);
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
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 message,
|
||||
@Default('') String errorMessage,
|
||||
@Default('') String emailError,
|
||||
}) = _ContactViewState;
|
||||
}
|
||||
@@ -1,292 +0,0 @@
|
||||
// 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 message; 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.message, message) || other.message == message)&&(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,message,errorMessage,emailError);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ContactViewState(country: $country, channel: $channel, name: $name, email: $email, subject: $subject, message: $message, 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 message, 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? message = 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,message: null == message ? _self.message : message // 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 message, 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.message,_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 message, 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.message,_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 message, 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.message,_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.message = '', 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 message;
|
||||
@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.message, message) || other.message == message)&&(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,message,errorMessage,emailError);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ContactViewState(country: $country, channel: $channel, name: $name, email: $email, subject: $subject, message: $message, 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 message, 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? message = 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,message: null == message ? _self.message : message // 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
|
||||
@@ -53,6 +53,7 @@ dependencies:
|
||||
get_it: ^9.0.5
|
||||
go_router: ^17.0.0
|
||||
flutter_riverpod: ^3.0.3
|
||||
riverpod_annotation: ^3.0.3
|
||||
freezed_annotation: ^3.1.0
|
||||
freezed: ^3.2.3
|
||||
dio: ^5.9.2
|
||||
@@ -70,6 +71,9 @@ dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
flutter_lints: ^5.0.0
|
||||
riverpod_generator: ^3.0.3
|
||||
build_runner: ^2.7.1
|
||||
mocktail: ^1.0.4
|
||||
|
||||
# For information on the generic Dart part of this file, see the
|
||||
# following page: https://dart.dev/tools/pub/pubspec
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import 'package:customer_service/src/domain/email_validator.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:sf_localizations/sf_localizations.dart';
|
||||
|
||||
void main() {
|
||||
group('EmailValidator.validate', () {
|
||||
test('returns errorEmailRequired when value is empty', () {
|
||||
expect(EmailValidator.validate(''), I18n.errorEmailRequired);
|
||||
});
|
||||
|
||||
test('returns errorEmailRequired when value is only whitespace', () {
|
||||
expect(EmailValidator.validate(' '), I18n.errorEmailRequired);
|
||||
});
|
||||
|
||||
test('returns errorEmailInvalid when value lacks @ sign', () {
|
||||
expect(EmailValidator.validate('notanemail'), I18n.errorEmailInvalid);
|
||||
});
|
||||
|
||||
test('returns errorEmailInvalid when domain is missing', () {
|
||||
expect(EmailValidator.validate('user@'), I18n.errorEmailInvalid);
|
||||
});
|
||||
|
||||
test('returns errorEmailInvalid when TLD is missing', () {
|
||||
expect(
|
||||
EmailValidator.validate('user@example'),
|
||||
I18n.errorEmailInvalid,
|
||||
);
|
||||
});
|
||||
|
||||
test('returns null for a well-formed email', () {
|
||||
expect(EmailValidator.validate('user@example.com'), isNull);
|
||||
});
|
||||
|
||||
test('trims whitespace before validating', () {
|
||||
expect(EmailValidator.validate(' user@example.com '), isNull);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user