refactor(device-management): split contacts feature into list/new/edit view models and migrate to SfPhoneNumber
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import 'package:device_management/src/features/contacts/domain/entities/contact_entity.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:sf_shared/sf_shared.dart';
|
||||
|
||||
part 'get_contacts_response_model.freezed.dart';
|
||||
part 'get_contacts_response_model.g.dart';
|
||||
@@ -39,7 +40,8 @@ extension GetContactsItemResponseModelMapper on GetContactsResponseModel {
|
||||
(GetContactsItemResponseModel item) => ContactEntity(
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
phone: item.phone,
|
||||
phone: SfPhoneNumber.tryParse(item.phone),
|
||||
rawPhone: item.phone,
|
||||
delegationId: item.delegationId,
|
||||
groupId: item.groupId,
|
||||
userId: item.userId,
|
||||
|
||||
@@ -13,7 +13,9 @@ class ContactsRepositoryImpl implements ContactsRepository {
|
||||
@override
|
||||
Future<List<ContactEntity>> getContacts({required String userId}) async {
|
||||
final model = await _remote.getContacts(userId: userId);
|
||||
return model.toEntity();
|
||||
final contacts = model.toEntity();
|
||||
contacts.sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
||||
return contacts;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -41,7 +43,7 @@ class ContactsRepositoryImpl implements ContactsRepository {
|
||||
userId: userId,
|
||||
deviceId: deviceId,
|
||||
contacts: contacts
|
||||
.map((c) => {'name': c.name, 'phone': c.phone})
|
||||
.map((c) => {'name': c.name, 'phone': c.phone?.e164 ?? c.rawPhone})
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:sf_shared/sf_shared.dart';
|
||||
|
||||
part 'contact_entity.freezed.dart';
|
||||
|
||||
@@ -7,7 +8,8 @@ abstract class ContactEntity with _$ContactEntity {
|
||||
const factory ContactEntity({
|
||||
required String id,
|
||||
required String name,
|
||||
required String phone,
|
||||
required SfPhoneNumber? phone,
|
||||
required String rawPhone,
|
||||
required String? delegationId,
|
||||
required String? groupId,
|
||||
required String? userId,
|
||||
|
||||
@@ -14,7 +14,7 @@ T _$identity<T>(T value) => value;
|
||||
/// @nodoc
|
||||
mixin _$ContactEntity {
|
||||
|
||||
String get id; String get name; String get phone; String? get delegationId; String? get groupId; String? get userId; int get createdAt; int? get updatedAt;
|
||||
String get id; String get name; SfPhoneNumber? get phone; String get rawPhone; String? get delegationId; String? get groupId; String? get userId; int get createdAt; int? get updatedAt;
|
||||
/// Create a copy of ContactEntity
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@@ -25,16 +25,16 @@ $ContactEntityCopyWith<ContactEntity> get copyWith => _$ContactEntityCopyWithImp
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is ContactEntity&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.phone, phone) || other.phone == phone)&&(identical(other.delegationId, delegationId) || other.delegationId == delegationId)&&(identical(other.groupId, groupId) || other.groupId == groupId)&&(identical(other.userId, userId) || other.userId == userId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is ContactEntity&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.phone, phone) || other.phone == phone)&&(identical(other.rawPhone, rawPhone) || other.rawPhone == rawPhone)&&(identical(other.delegationId, delegationId) || other.delegationId == delegationId)&&(identical(other.groupId, groupId) || other.groupId == groupId)&&(identical(other.userId, userId) || other.userId == userId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,id,name,phone,delegationId,groupId,userId,createdAt,updatedAt);
|
||||
int get hashCode => Object.hash(runtimeType,id,name,phone,rawPhone,delegationId,groupId,userId,createdAt,updatedAt);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ContactEntity(id: $id, name: $name, phone: $phone, delegationId: $delegationId, groupId: $groupId, userId: $userId, createdAt: $createdAt, updatedAt: $updatedAt)';
|
||||
return 'ContactEntity(id: $id, name: $name, phone: $phone, rawPhone: $rawPhone, delegationId: $delegationId, groupId: $groupId, userId: $userId, createdAt: $createdAt, updatedAt: $updatedAt)';
|
||||
}
|
||||
|
||||
|
||||
@@ -45,11 +45,11 @@ abstract mixin class $ContactEntityCopyWith<$Res> {
|
||||
factory $ContactEntityCopyWith(ContactEntity value, $Res Function(ContactEntity) _then) = _$ContactEntityCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String id, String name, String phone, String? delegationId, String? groupId, String? userId, int createdAt, int? updatedAt
|
||||
String id, String name, SfPhoneNumber? phone, String rawPhone, String? delegationId, String? groupId, String? userId, int createdAt, int? updatedAt
|
||||
});
|
||||
|
||||
|
||||
|
||||
$SfPhoneNumberCopyWith<$Res>? get phone;
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
@@ -62,11 +62,12 @@ class _$ContactEntityCopyWithImpl<$Res>
|
||||
|
||||
/// Create a copy of ContactEntity
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? name = null,Object? phone = null,Object? delegationId = freezed,Object? groupId = freezed,Object? userId = freezed,Object? createdAt = null,Object? updatedAt = freezed,}) {
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? name = null,Object? phone = freezed,Object? rawPhone = null,Object? delegationId = freezed,Object? groupId = freezed,Object? userId = freezed,Object? createdAt = null,Object? updatedAt = freezed,}) {
|
||||
return _then(_self.copyWith(
|
||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
||||
as String,phone: null == phone ? _self.phone : phone // ignore: cast_nullable_to_non_nullable
|
||||
as String,phone: freezed == phone ? _self.phone : phone // ignore: cast_nullable_to_non_nullable
|
||||
as SfPhoneNumber?,rawPhone: null == rawPhone ? _self.rawPhone : rawPhone // ignore: cast_nullable_to_non_nullable
|
||||
as String,delegationId: freezed == delegationId ? _self.delegationId : delegationId // ignore: cast_nullable_to_non_nullable
|
||||
as String?,groupId: freezed == groupId ? _self.groupId : groupId // ignore: cast_nullable_to_non_nullable
|
||||
as String?,userId: freezed == userId ? _self.userId : userId // ignore: cast_nullable_to_non_nullable
|
||||
@@ -75,7 +76,19 @@ as int,updatedAt: freezed == updatedAt ? _self.updatedAt : updatedAt // ignore:
|
||||
as int?,
|
||||
));
|
||||
}
|
||||
/// Create a copy of ContactEntity
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SfPhoneNumberCopyWith<$Res>? get phone {
|
||||
if (_self.phone == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $SfPhoneNumberCopyWith<$Res>(_self.phone!, (value) {
|
||||
return _then(_self.copyWith(phone: value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -157,10 +170,10 @@ return $default(_that);case _:
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String name, String phone, String? delegationId, String? groupId, String? userId, int createdAt, int? updatedAt)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String name, SfPhoneNumber? phone, String rawPhone, String? delegationId, String? groupId, String? userId, int createdAt, int? updatedAt)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _ContactEntity() when $default != null:
|
||||
return $default(_that.id,_that.name,_that.phone,_that.delegationId,_that.groupId,_that.userId,_that.createdAt,_that.updatedAt);case _:
|
||||
return $default(_that.id,_that.name,_that.phone,_that.rawPhone,_that.delegationId,_that.groupId,_that.userId,_that.createdAt,_that.updatedAt);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
@@ -178,10 +191,10 @@ return $default(_that.id,_that.name,_that.phone,_that.delegationId,_that.groupId
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String name, String phone, String? delegationId, String? groupId, String? userId, int createdAt, int? updatedAt) $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String name, SfPhoneNumber? phone, String rawPhone, String? delegationId, String? groupId, String? userId, int createdAt, int? updatedAt) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _ContactEntity():
|
||||
return $default(_that.id,_that.name,_that.phone,_that.delegationId,_that.groupId,_that.userId,_that.createdAt,_that.updatedAt);case _:
|
||||
return $default(_that.id,_that.name,_that.phone,_that.rawPhone,_that.delegationId,_that.groupId,_that.userId,_that.createdAt,_that.updatedAt);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
@@ -198,10 +211,10 @@ return $default(_that.id,_that.name,_that.phone,_that.delegationId,_that.groupId
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String name, String phone, String? delegationId, String? groupId, String? userId, int createdAt, int? updatedAt)? $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String name, SfPhoneNumber? phone, String rawPhone, String? delegationId, String? groupId, String? userId, int createdAt, int? updatedAt)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _ContactEntity() when $default != null:
|
||||
return $default(_that.id,_that.name,_that.phone,_that.delegationId,_that.groupId,_that.userId,_that.createdAt,_that.updatedAt);case _:
|
||||
return $default(_that.id,_that.name,_that.phone,_that.rawPhone,_that.delegationId,_that.groupId,_that.userId,_that.createdAt,_that.updatedAt);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
@@ -213,12 +226,13 @@ return $default(_that.id,_that.name,_that.phone,_that.delegationId,_that.groupId
|
||||
|
||||
|
||||
class _ContactEntity implements ContactEntity {
|
||||
const _ContactEntity({required this.id, required this.name, required this.phone, required this.delegationId, required this.groupId, required this.userId, required this.createdAt, required this.updatedAt});
|
||||
const _ContactEntity({required this.id, required this.name, required this.phone, required this.rawPhone, required this.delegationId, required this.groupId, required this.userId, required this.createdAt, required this.updatedAt});
|
||||
|
||||
|
||||
@override final String id;
|
||||
@override final String name;
|
||||
@override final String phone;
|
||||
@override final SfPhoneNumber? phone;
|
||||
@override final String rawPhone;
|
||||
@override final String? delegationId;
|
||||
@override final String? groupId;
|
||||
@override final String? userId;
|
||||
@@ -235,16 +249,16 @@ _$ContactEntityCopyWith<_ContactEntity> get copyWith => __$ContactEntityCopyWith
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ContactEntity&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.phone, phone) || other.phone == phone)&&(identical(other.delegationId, delegationId) || other.delegationId == delegationId)&&(identical(other.groupId, groupId) || other.groupId == groupId)&&(identical(other.userId, userId) || other.userId == userId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ContactEntity&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.phone, phone) || other.phone == phone)&&(identical(other.rawPhone, rawPhone) || other.rawPhone == rawPhone)&&(identical(other.delegationId, delegationId) || other.delegationId == delegationId)&&(identical(other.groupId, groupId) || other.groupId == groupId)&&(identical(other.userId, userId) || other.userId == userId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,id,name,phone,delegationId,groupId,userId,createdAt,updatedAt);
|
||||
int get hashCode => Object.hash(runtimeType,id,name,phone,rawPhone,delegationId,groupId,userId,createdAt,updatedAt);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ContactEntity(id: $id, name: $name, phone: $phone, delegationId: $delegationId, groupId: $groupId, userId: $userId, createdAt: $createdAt, updatedAt: $updatedAt)';
|
||||
return 'ContactEntity(id: $id, name: $name, phone: $phone, rawPhone: $rawPhone, delegationId: $delegationId, groupId: $groupId, userId: $userId, createdAt: $createdAt, updatedAt: $updatedAt)';
|
||||
}
|
||||
|
||||
|
||||
@@ -255,11 +269,11 @@ abstract mixin class _$ContactEntityCopyWith<$Res> implements $ContactEntityCopy
|
||||
factory _$ContactEntityCopyWith(_ContactEntity value, $Res Function(_ContactEntity) _then) = __$ContactEntityCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String id, String name, String phone, String? delegationId, String? groupId, String? userId, int createdAt, int? updatedAt
|
||||
String id, String name, SfPhoneNumber? phone, String rawPhone, String? delegationId, String? groupId, String? userId, int createdAt, int? updatedAt
|
||||
});
|
||||
|
||||
|
||||
|
||||
@override $SfPhoneNumberCopyWith<$Res>? get phone;
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
@@ -272,11 +286,12 @@ class __$ContactEntityCopyWithImpl<$Res>
|
||||
|
||||
/// Create a copy of ContactEntity
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? name = null,Object? phone = null,Object? delegationId = freezed,Object? groupId = freezed,Object? userId = freezed,Object? createdAt = null,Object? updatedAt = freezed,}) {
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? name = null,Object? phone = freezed,Object? rawPhone = null,Object? delegationId = freezed,Object? groupId = freezed,Object? userId = freezed,Object? createdAt = null,Object? updatedAt = freezed,}) {
|
||||
return _then(_ContactEntity(
|
||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
||||
as String,phone: null == phone ? _self.phone : phone // ignore: cast_nullable_to_non_nullable
|
||||
as String,phone: freezed == phone ? _self.phone : phone // ignore: cast_nullable_to_non_nullable
|
||||
as SfPhoneNumber?,rawPhone: null == rawPhone ? _self.rawPhone : rawPhone // ignore: cast_nullable_to_non_nullable
|
||||
as String,delegationId: freezed == delegationId ? _self.delegationId : delegationId // ignore: cast_nullable_to_non_nullable
|
||||
as String?,groupId: freezed == groupId ? _self.groupId : groupId // ignore: cast_nullable_to_non_nullable
|
||||
as String?,userId: freezed == userId ? _self.userId : userId // ignore: cast_nullable_to_non_nullable
|
||||
@@ -286,7 +301,19 @@ as int?,
|
||||
));
|
||||
}
|
||||
|
||||
/// Create a copy of ContactEntity
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SfPhoneNumberCopyWith<$Res>? get phone {
|
||||
if (_self.phone == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $SfPhoneNumberCopyWith<$Res>(_self.phone!, (value) {
|
||||
return _then(_self.copyWith(phone: value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// dart format on
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import 'package:sf_localizations/sf_localizations.dart';
|
||||
|
||||
enum ContactError {
|
||||
nameRequired,
|
||||
phoneInvalid,
|
||||
maxReached,
|
||||
minReached,
|
||||
network,
|
||||
contactsPermissionBlocked;
|
||||
|
||||
String get i18nKey => switch (this) {
|
||||
ContactError.nameRequired => I18n.errorFirstNameRequired,
|
||||
ContactError.phoneInvalid => I18n.errorMessagePhoneIsInvalid,
|
||||
ContactError.maxReached => I18n.errorContactsMax,
|
||||
ContactError.minReached => I18n.errorContactsMin,
|
||||
ContactError.network => I18n.errorGeneric,
|
||||
ContactError.contactsPermissionBlocked => I18n.contactsPermissionBlocked,
|
||||
};
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import 'package:sf_localizations/sf_localizations.dart';
|
||||
import 'package:utils/utils.dart';
|
||||
|
||||
import 'state/contacts_view_model.dart';
|
||||
import 'state/contacts_view_state.dart';
|
||||
import 'widgets/contact_card.dart';
|
||||
import 'widgets/new_contact_dialog.dart';
|
||||
|
||||
@@ -21,17 +22,14 @@ class ContactsScreen extends ConsumerWidget {
|
||||
final state = ref.watch(contactsViewModelProvider);
|
||||
final theme = ref.watch(themePortProvider);
|
||||
|
||||
ref.listen(contactsViewModelProvider.select((s) => s.errorMessage), (
|
||||
previous,
|
||||
next,
|
||||
) {
|
||||
if (next.isNotEmpty) {
|
||||
showTopSnackbar(
|
||||
context,
|
||||
message: context.translate(next),
|
||||
type: MessageType.error,
|
||||
);
|
||||
}
|
||||
ref.listen(contactsViewModelProvider.select((s) => s.error), (prev, next) {
|
||||
if (next == null) return;
|
||||
showTopSnackbar(
|
||||
context,
|
||||
message: context.translate(next.i18nKey),
|
||||
type: MessageType.error,
|
||||
);
|
||||
vm.clearError();
|
||||
});
|
||||
|
||||
return LegacyPageLayout(
|
||||
@@ -62,7 +60,7 @@ class ContactsScreen extends ConsumerWidget {
|
||||
child: InkWell(
|
||||
customBorder: const CircleBorder(),
|
||||
onTap: () {
|
||||
if (state.contacts.length == state.maxLimit) {
|
||||
if (state.contacts.length >= kContactsMaxLimit) {
|
||||
showTopSnackbar(
|
||||
context,
|
||||
message: context.translate(I18n.errorContactsMax),
|
||||
|
||||
@@ -7,46 +7,38 @@ import 'package:navigation/navigation.dart';
|
||||
import 'package:sf_localizations/sf_localizations.dart';
|
||||
import 'package:utils/utils.dart';
|
||||
|
||||
import '../domain/entities/contact_entity.dart';
|
||||
import 'state/contacts_view_model.dart';
|
||||
import 'state/edit_contact_view_model.dart';
|
||||
|
||||
class EditContactScreen extends ConsumerStatefulWidget {
|
||||
class EditContactScreen extends ConsumerWidget {
|
||||
final String contactId;
|
||||
|
||||
const EditContactScreen({super.key, required this.contactId});
|
||||
|
||||
@override
|
||||
ConsumerState<EditContactScreen> createState() => _EditContactScreenState();
|
||||
}
|
||||
|
||||
class _EditContactScreenState extends ConsumerState<EditContactScreen> {
|
||||
late final ContactEntity _contact;
|
||||
late final TextEditingController _nameController;
|
||||
late final TextEditingController _phoneController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_contact = ref
|
||||
.read(contactsViewModelProvider)
|
||||
.contacts
|
||||
.firstWhere((c) => c.id == widget.contactId);
|
||||
_nameController = TextEditingController(text: _contact.name);
|
||||
_phoneController = TextEditingController(text: _contact.phone);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nameController.dispose();
|
||||
_phoneController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = ref.watch(themePortProvider);
|
||||
final provider = editContactViewModelProvider(contactId);
|
||||
final vm = ref.read(provider.notifier);
|
||||
final state = ref.watch(provider);
|
||||
|
||||
final state = ref.watch(contactsViewModelProvider);
|
||||
ref.listen(provider.select((s) => s.error), (_, next) {
|
||||
if (next == null) return;
|
||||
showTopSnackbar(
|
||||
context,
|
||||
message: context.translate(next.i18nKey),
|
||||
type: MessageType.error,
|
||||
);
|
||||
vm.clearError();
|
||||
});
|
||||
|
||||
final contact = state.contact;
|
||||
|
||||
if (contact == null) {
|
||||
return Scaffold(
|
||||
backgroundColor: theme.getColorFor(ThemeCode.backgroundPrimary),
|
||||
body: Center(child: Text(context.translate(I18n.errorGeneric))),
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: theme.getColorFor(ThemeCode.backgroundPrimary),
|
||||
@@ -83,9 +75,10 @@ class _EditContactScreenState extends ConsumerState<EditContactScreen> {
|
||||
vertical: SizeUtils.getByScreen(small: 10, big: 8),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Center(
|
||||
@@ -97,7 +90,7 @@ class _EditContactScreenState extends ConsumerState<EditContactScreen> {
|
||||
height: SizeUtils.getByScreen(small: 24, big: 22),
|
||||
),
|
||||
CustomTextField(
|
||||
controller: _nameController,
|
||||
controller: vm.nameController,
|
||||
label: context.translate(I18n.name),
|
||||
),
|
||||
SizedBox(
|
||||
@@ -109,14 +102,10 @@ class _EditContactScreenState extends ConsumerState<EditContactScreen> {
|
||||
headerText: context.translate(
|
||||
I18n.selectYourCountry,
|
||||
),
|
||||
initialSelection: state.dialCode,
|
||||
initialSelection: state.isoCode,
|
||||
onChanged: (country) {
|
||||
final vm = ref.read(
|
||||
contactsViewModelProvider.notifier,
|
||||
);
|
||||
vm.updateDialCode(
|
||||
country.dialCode ?? state.dialCode,
|
||||
);
|
||||
final code = country.code;
|
||||
if (code != null) vm.updateCountry(code);
|
||||
},
|
||||
width: 80,
|
||||
),
|
||||
@@ -129,27 +118,26 @@ class _EditContactScreenState extends ConsumerState<EditContactScreen> {
|
||||
),
|
||||
Expanded(
|
||||
child: CustomTextField(
|
||||
controller: _phoneController,
|
||||
controller: vm.phoneController,
|
||||
keyboardType: TextInputType.number,
|
||||
label: context.translate(I18n.phoneNumber),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: SizeUtils.getByScreen(small: 16, big: 14)),
|
||||
PrimaryButton(
|
||||
onPressed: () async {
|
||||
final vm = ref.read(contactsViewModelProvider.notifier);
|
||||
final success = await vm.updateContact(
|
||||
contact: _contact,
|
||||
name: _nameController.text,
|
||||
phone: _phoneController.text,
|
||||
);
|
||||
if (success && mounted) {
|
||||
GetIt.I<NavigationContract>().goBack();
|
||||
}
|
||||
},
|
||||
onPressed: state.isSubmitting
|
||||
? null
|
||||
: () async {
|
||||
final success = await vm.submit();
|
||||
if (success && context.mounted) {
|
||||
GetIt.I<NavigationContract>().goBack();
|
||||
}
|
||||
},
|
||||
text: context.translate(I18n.save),
|
||||
color: theme.getColorFor(ThemeCode.legacyPrimary),
|
||||
),
|
||||
@@ -163,3 +151,4 @@ class _EditContactScreenState extends ConsumerState<EditContactScreen> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,16 +2,13 @@ import 'dart:async';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:legacy_shared/legacy_shared.dart';
|
||||
import 'package:sf_localizations/sf_localizations.dart';
|
||||
import 'package:sf_shared/sf_shared.dart';
|
||||
import 'package:sf_tracking/sf_tracking.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import '../../../../core/data/models/create_contact_request_model.dart';
|
||||
import '../../../../core/data/models/update_contact_request_model.dart';
|
||||
import '../../../../core/domain/repositories/contacts_repository.dart';
|
||||
import '../../../../core/providers/contacts_repository_provider.dart';
|
||||
import '../../domain/entities/contact_entity.dart';
|
||||
import '../../domain/entities/contact_error.dart';
|
||||
import 'contacts_view_state.dart';
|
||||
|
||||
final contactsViewModelProvider =
|
||||
@@ -20,8 +17,6 @@ final contactsViewModelProvider =
|
||||
);
|
||||
|
||||
class ContactsViewModel extends Notifier<ContactsViewState> {
|
||||
static const _uuid = Uuid();
|
||||
|
||||
late final ContactsRepository _contactsRepository;
|
||||
late final SfTrackingRepository _tracking;
|
||||
|
||||
@@ -29,126 +24,40 @@ class ContactsViewModel extends Notifier<ContactsViewState> {
|
||||
ContactsViewState build() {
|
||||
_contactsRepository = ref.read(contactsRepositoryProvider);
|
||||
_tracking = ref.read(sfTrackingProvider);
|
||||
_init();
|
||||
_load();
|
||||
return const ContactsViewState();
|
||||
}
|
||||
|
||||
Future<void> _init() async {
|
||||
try {
|
||||
final user = await ref.read(userInfoProvider.future);
|
||||
if (!ref.mounted) return;
|
||||
|
||||
final contacts = await _contactsRepository.getContacts(userId: user.id);
|
||||
if (!ref.mounted) return;
|
||||
|
||||
state = state.copyWith(contacts: contacts, isLoading: false);
|
||||
} catch (e) {
|
||||
if (!ref.mounted) return;
|
||||
state = state.copyWith(isLoading: false, errorMessage: _formatError(e));
|
||||
}
|
||||
}
|
||||
|
||||
void toggleIsEditing() {
|
||||
state = state.copyWith(isEditing: !state.isEditing);
|
||||
}
|
||||
|
||||
void updateDialCode(String value) {
|
||||
if (value == state.dialCode) return;
|
||||
|
||||
state = state.copyWith(dialCode: value, errorMessage: '');
|
||||
void clearError() {
|
||||
if (state.error != null) state = state.copyWith(error: null);
|
||||
}
|
||||
|
||||
Future<bool> createContact({
|
||||
required String name,
|
||||
required String phone,
|
||||
}) async {
|
||||
if (state.isLoading) return false;
|
||||
|
||||
if (phone.isEmpty) {
|
||||
state = state.copyWith(
|
||||
errorMessage: I18n.errorMessagePhoneIsInvalid.tr(),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<void> refresh() async {
|
||||
try {
|
||||
state = state.copyWith(isLoading: true, errorMessage: '');
|
||||
|
||||
final dialCode = state.dialCode;
|
||||
final fullPhone = dialCode + phone;
|
||||
|
||||
final user = await ref.read(userInfoProvider.future);
|
||||
if (!ref.mounted) return false;
|
||||
|
||||
final request = CreateContactRequestModel(
|
||||
id: _uuid.v4(),
|
||||
name: name,
|
||||
phone: fullPhone,
|
||||
userId: user.id,
|
||||
);
|
||||
|
||||
await _contactsRepository.createContact(request: request);
|
||||
if (!ref.mounted) return false;
|
||||
|
||||
unawaited(
|
||||
_tracking.legacyContactsAdded(totalCount: state.contacts.length + 1),
|
||||
);
|
||||
|
||||
await _reload();
|
||||
return true;
|
||||
} catch (e) {
|
||||
if (!ref.mounted) return false;
|
||||
state = state.copyWith(isLoading: false, errorMessage: _formatError(e));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> updateContact({
|
||||
required ContactEntity contact,
|
||||
required String name,
|
||||
required String phone,
|
||||
}) async {
|
||||
if (state.isLoading) return false;
|
||||
|
||||
if (name.isEmpty && phone.isEmpty) return false;
|
||||
|
||||
try {
|
||||
state = state.copyWith(isLoading: true, errorMessage: '');
|
||||
|
||||
final dialCode = state.dialCode;
|
||||
final fullPhone = phone.isEmpty ? contact.phone : dialCode + phone;
|
||||
|
||||
final request = UpdateContactRequestModel(
|
||||
id: contact.id,
|
||||
name: name.isEmpty ? contact.name : name,
|
||||
phone: fullPhone,
|
||||
);
|
||||
|
||||
await _contactsRepository.updateContact(request: request);
|
||||
if (!ref.mounted) return false;
|
||||
|
||||
unawaited(_tracking.legacyContactsEdited());
|
||||
|
||||
await _reload();
|
||||
return true;
|
||||
} catch (e) {
|
||||
if (!ref.mounted) return false;
|
||||
state = state.copyWith(isLoading: false, errorMessage: _formatError(e));
|
||||
return false;
|
||||
if (!ref.mounted) return;
|
||||
await _reload(user.id);
|
||||
} catch (_) {
|
||||
if (!ref.mounted) return;
|
||||
state = state.copyWith(error: ContactError.network);
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> deleteContact(ContactEntity contact) async {
|
||||
if (state.isLoading) return false;
|
||||
if (state.isSubmitting) return false;
|
||||
|
||||
if (state.contacts.length == 1) {
|
||||
state = state.copyWith(errorMessage: I18n.errorContactsMin);
|
||||
return true;
|
||||
if (state.contacts.length <= 1) {
|
||||
state = state.copyWith(error: ContactError.minReached);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
state = state.copyWith(isLoading: true, errorMessage: '');
|
||||
state = state.copyWith(isSubmitting: true, error: null);
|
||||
|
||||
try {
|
||||
await _contactsRepository.deleteContact(contactId: contact.id);
|
||||
if (!ref.mounted) return false;
|
||||
|
||||
@@ -158,32 +67,42 @@ class ContactsViewModel extends Notifier<ContactsViewState> {
|
||||
),
|
||||
);
|
||||
|
||||
await _reload();
|
||||
return true;
|
||||
} catch (e) {
|
||||
final user = await ref.read(userInfoProvider.future);
|
||||
if (!ref.mounted) return false;
|
||||
state = state.copyWith(isLoading: false, errorMessage: _formatError(e));
|
||||
|
||||
await _reload(user.id);
|
||||
return true;
|
||||
} catch (_) {
|
||||
if (!ref.mounted) return false;
|
||||
state = state.copyWith(isSubmitting: false, error: ContactError.network);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _reload() async {
|
||||
Future<void> _load() async {
|
||||
try {
|
||||
final user = await ref.read(userInfoProvider.future);
|
||||
if (!ref.mounted) return;
|
||||
|
||||
final contacts = await _contactsRepository.getContacts(userId: user.id);
|
||||
await _reload(user.id);
|
||||
} catch (_) {
|
||||
if (!ref.mounted) return;
|
||||
|
||||
state = state.copyWith(contacts: contacts, isLoading: false);
|
||||
|
||||
_syncToDevice(user.id, contacts);
|
||||
} catch (e) {
|
||||
if (!ref.mounted) return;
|
||||
state = state.copyWith(isLoading: false, errorMessage: _formatError(e));
|
||||
state = state.copyWith(isLoading: false, error: ContactError.network);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _reload(String userId) async {
|
||||
final contacts = await _contactsRepository.getContacts(userId: userId);
|
||||
if (!ref.mounted) return;
|
||||
|
||||
state = state.copyWith(
|
||||
contacts: contacts,
|
||||
isLoading: false,
|
||||
isSubmitting: false,
|
||||
);
|
||||
|
||||
unawaited(_syncToDevice(userId, contacts));
|
||||
}
|
||||
|
||||
Future<void> _syncToDevice(
|
||||
String userId,
|
||||
List<ContactEntity> contacts,
|
||||
@@ -198,12 +117,7 @@ class ContactsViewModel extends Notifier<ContactsViewState> {
|
||||
contacts: contacts,
|
||||
);
|
||||
} catch (_) {
|
||||
// Sync failure is non-blocking — contacts were already saved via CRUD
|
||||
// Non-blocking: CRUD already committed server-side.
|
||||
}
|
||||
}
|
||||
|
||||
String _formatError(Object e) {
|
||||
final msg = e.toString();
|
||||
return msg.startsWith('Exception: ') ? msg.substring(11) : msg;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
import '../../domain/entities/contact_entity.dart';
|
||||
import '../../domain/entities/contact_error.dart';
|
||||
|
||||
part 'contacts_view_state.freezed.dart';
|
||||
|
||||
const int kContactsMaxLimit = 10;
|
||||
|
||||
@freezed
|
||||
abstract class ContactsViewState with _$ContactsViewState {
|
||||
const factory ContactsViewState({
|
||||
@Default([]) List<ContactEntity> contacts,
|
||||
@Default(10) int maxLimit,
|
||||
@Default('+34') String dialCode,
|
||||
@Default(true) bool isLoading,
|
||||
@Default(false) bool isSubmitting,
|
||||
@Default(false) bool isEditing,
|
||||
@Default('') String errorMessage,
|
||||
ContactError? error,
|
||||
}) = _ContactsViewState;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ T _$identity<T>(T value) => value;
|
||||
/// @nodoc
|
||||
mixin _$ContactsViewState {
|
||||
|
||||
List<ContactEntity> get contacts; int get maxLimit; String get dialCode; bool get isLoading; bool get isEditing; String get errorMessage;
|
||||
List<ContactEntity> get contacts; bool get isLoading; bool get isSubmitting; bool get isEditing; ContactError? get error;
|
||||
/// Create a copy of ContactsViewState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@@ -25,16 +25,16 @@ $ContactsViewStateCopyWith<ContactsViewState> get copyWith => _$ContactsViewStat
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is ContactsViewState&&const DeepCollectionEquality().equals(other.contacts, contacts)&&(identical(other.maxLimit, maxLimit) || other.maxLimit == maxLimit)&&(identical(other.dialCode, dialCode) || other.dialCode == dialCode)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.isEditing, isEditing) || other.isEditing == isEditing)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is ContactsViewState&&const DeepCollectionEquality().equals(other.contacts, contacts)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.isSubmitting, isSubmitting) || other.isSubmitting == isSubmitting)&&(identical(other.isEditing, isEditing) || other.isEditing == isEditing)&&(identical(other.error, error) || other.error == error));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(contacts),maxLimit,dialCode,isLoading,isEditing,errorMessage);
|
||||
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(contacts),isLoading,isSubmitting,isEditing,error);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ContactsViewState(contacts: $contacts, maxLimit: $maxLimit, dialCode: $dialCode, isLoading: $isLoading, isEditing: $isEditing, errorMessage: $errorMessage)';
|
||||
return 'ContactsViewState(contacts: $contacts, isLoading: $isLoading, isSubmitting: $isSubmitting, isEditing: $isEditing, error: $error)';
|
||||
}
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ abstract mixin class $ContactsViewStateCopyWith<$Res> {
|
||||
factory $ContactsViewStateCopyWith(ContactsViewState value, $Res Function(ContactsViewState) _then) = _$ContactsViewStateCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
List<ContactEntity> contacts, int maxLimit, String dialCode, bool isLoading, bool isEditing, String errorMessage
|
||||
List<ContactEntity> contacts, bool isLoading, bool isSubmitting, bool isEditing, ContactError? error
|
||||
});
|
||||
|
||||
|
||||
@@ -62,15 +62,14 @@ class _$ContactsViewStateCopyWithImpl<$Res>
|
||||
|
||||
/// Create a copy of ContactsViewState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? contacts = null,Object? maxLimit = null,Object? dialCode = null,Object? isLoading = null,Object? isEditing = null,Object? errorMessage = null,}) {
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? contacts = null,Object? isLoading = null,Object? isSubmitting = null,Object? isEditing = null,Object? error = freezed,}) {
|
||||
return _then(_self.copyWith(
|
||||
contacts: null == contacts ? _self.contacts : contacts // ignore: cast_nullable_to_non_nullable
|
||||
as List<ContactEntity>,maxLimit: null == maxLimit ? _self.maxLimit : maxLimit // ignore: cast_nullable_to_non_nullable
|
||||
as int,dialCode: null == dialCode ? _self.dialCode : dialCode // ignore: cast_nullable_to_non_nullable
|
||||
as String,isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable
|
||||
as List<ContactEntity>,isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable
|
||||
as bool,isSubmitting: null == isSubmitting ? _self.isSubmitting : isSubmitting // ignore: cast_nullable_to_non_nullable
|
||||
as bool,isEditing: null == isEditing ? _self.isEditing : isEditing // ignore: cast_nullable_to_non_nullable
|
||||
as bool,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
as bool,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable
|
||||
as ContactError?,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -155,10 +154,10 @@ return $default(_that);case _:
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( List<ContactEntity> contacts, int maxLimit, String dialCode, bool isLoading, bool isEditing, String errorMessage)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( List<ContactEntity> contacts, bool isLoading, bool isSubmitting, bool isEditing, ContactError? error)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _ContactsViewState() when $default != null:
|
||||
return $default(_that.contacts,_that.maxLimit,_that.dialCode,_that.isLoading,_that.isEditing,_that.errorMessage);case _:
|
||||
return $default(_that.contacts,_that.isLoading,_that.isSubmitting,_that.isEditing,_that.error);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
@@ -176,10 +175,10 @@ return $default(_that.contacts,_that.maxLimit,_that.dialCode,_that.isLoading,_th
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( List<ContactEntity> contacts, int maxLimit, String dialCode, bool isLoading, bool isEditing, String errorMessage) $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( List<ContactEntity> contacts, bool isLoading, bool isSubmitting, bool isEditing, ContactError? error) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _ContactsViewState():
|
||||
return $default(_that.contacts,_that.maxLimit,_that.dialCode,_that.isLoading,_that.isEditing,_that.errorMessage);case _:
|
||||
return $default(_that.contacts,_that.isLoading,_that.isSubmitting,_that.isEditing,_that.error);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
@@ -196,10 +195,10 @@ return $default(_that.contacts,_that.maxLimit,_that.dialCode,_that.isLoading,_th
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( List<ContactEntity> contacts, int maxLimit, String dialCode, bool isLoading, bool isEditing, String errorMessage)? $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( List<ContactEntity> contacts, bool isLoading, bool isSubmitting, bool isEditing, ContactError? error)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _ContactsViewState() when $default != null:
|
||||
return $default(_that.contacts,_that.maxLimit,_that.dialCode,_that.isLoading,_that.isEditing,_that.errorMessage);case _:
|
||||
return $default(_that.contacts,_that.isLoading,_that.isSubmitting,_that.isEditing,_that.error);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
@@ -211,7 +210,7 @@ return $default(_that.contacts,_that.maxLimit,_that.dialCode,_that.isLoading,_th
|
||||
|
||||
|
||||
class _ContactsViewState implements ContactsViewState {
|
||||
const _ContactsViewState({final List<ContactEntity> contacts = const [], this.maxLimit = 10, this.dialCode = '+34', this.isLoading = true, this.isEditing = false, this.errorMessage = ''}): _contacts = contacts;
|
||||
const _ContactsViewState({final List<ContactEntity> contacts = const [], this.isLoading = true, this.isSubmitting = false, this.isEditing = false, this.error}): _contacts = contacts;
|
||||
|
||||
|
||||
final List<ContactEntity> _contacts;
|
||||
@@ -221,11 +220,10 @@ class _ContactsViewState implements ContactsViewState {
|
||||
return EqualUnmodifiableListView(_contacts);
|
||||
}
|
||||
|
||||
@override@JsonKey() final int maxLimit;
|
||||
@override@JsonKey() final String dialCode;
|
||||
@override@JsonKey() final bool isLoading;
|
||||
@override@JsonKey() final bool isSubmitting;
|
||||
@override@JsonKey() final bool isEditing;
|
||||
@override@JsonKey() final String errorMessage;
|
||||
@override final ContactError? error;
|
||||
|
||||
/// Create a copy of ContactsViewState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@@ -237,16 +235,16 @@ _$ContactsViewStateCopyWith<_ContactsViewState> get copyWith => __$ContactsViewS
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ContactsViewState&&const DeepCollectionEquality().equals(other._contacts, _contacts)&&(identical(other.maxLimit, maxLimit) || other.maxLimit == maxLimit)&&(identical(other.dialCode, dialCode) || other.dialCode == dialCode)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.isEditing, isEditing) || other.isEditing == isEditing)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ContactsViewState&&const DeepCollectionEquality().equals(other._contacts, _contacts)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.isSubmitting, isSubmitting) || other.isSubmitting == isSubmitting)&&(identical(other.isEditing, isEditing) || other.isEditing == isEditing)&&(identical(other.error, error) || other.error == error));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_contacts),maxLimit,dialCode,isLoading,isEditing,errorMessage);
|
||||
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_contacts),isLoading,isSubmitting,isEditing,error);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ContactsViewState(contacts: $contacts, maxLimit: $maxLimit, dialCode: $dialCode, isLoading: $isLoading, isEditing: $isEditing, errorMessage: $errorMessage)';
|
||||
return 'ContactsViewState(contacts: $contacts, isLoading: $isLoading, isSubmitting: $isSubmitting, isEditing: $isEditing, error: $error)';
|
||||
}
|
||||
|
||||
|
||||
@@ -257,7 +255,7 @@ abstract mixin class _$ContactsViewStateCopyWith<$Res> implements $ContactsViewS
|
||||
factory _$ContactsViewStateCopyWith(_ContactsViewState value, $Res Function(_ContactsViewState) _then) = __$ContactsViewStateCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
List<ContactEntity> contacts, int maxLimit, String dialCode, bool isLoading, bool isEditing, String errorMessage
|
||||
List<ContactEntity> contacts, bool isLoading, bool isSubmitting, bool isEditing, ContactError? error
|
||||
});
|
||||
|
||||
|
||||
@@ -274,15 +272,14 @@ class __$ContactsViewStateCopyWithImpl<$Res>
|
||||
|
||||
/// Create a copy of ContactsViewState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? contacts = null,Object? maxLimit = null,Object? dialCode = null,Object? isLoading = null,Object? isEditing = null,Object? errorMessage = null,}) {
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? contacts = null,Object? isLoading = null,Object? isSubmitting = null,Object? isEditing = null,Object? error = freezed,}) {
|
||||
return _then(_ContactsViewState(
|
||||
contacts: null == contacts ? _self._contacts : contacts // ignore: cast_nullable_to_non_nullable
|
||||
as List<ContactEntity>,maxLimit: null == maxLimit ? _self.maxLimit : maxLimit // ignore: cast_nullable_to_non_nullable
|
||||
as int,dialCode: null == dialCode ? _self.dialCode : dialCode // ignore: cast_nullable_to_non_nullable
|
||||
as String,isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable
|
||||
as List<ContactEntity>,isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable
|
||||
as bool,isSubmitting: null == isSubmitting ? _self.isSubmitting : isSubmitting // ignore: cast_nullable_to_non_nullable
|
||||
as bool,isEditing: null == isEditing ? _self.isEditing : isEditing // ignore: cast_nullable_to_non_nullable
|
||||
as bool,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
as bool,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable
|
||||
as ContactError?,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:sf_shared/sf_shared.dart';
|
||||
import 'package:sf_tracking/sf_tracking.dart';
|
||||
|
||||
import '../../../../core/data/models/update_contact_request_model.dart';
|
||||
import '../../../../core/domain/repositories/contacts_repository.dart';
|
||||
import '../../../../core/providers/contacts_repository_provider.dart';
|
||||
import '../../domain/entities/contact_error.dart';
|
||||
import 'contacts_view_model.dart';
|
||||
import 'edit_contact_view_state.dart';
|
||||
|
||||
final editContactViewModelProvider = NotifierProvider.autoDispose
|
||||
.family<EditContactViewModel, EditContactViewState, String>(
|
||||
EditContactViewModel.new,
|
||||
);
|
||||
|
||||
class EditContactViewModel extends Notifier<EditContactViewState> {
|
||||
EditContactViewModel(this._contactId);
|
||||
|
||||
final String _contactId;
|
||||
|
||||
late final TextEditingController nameController;
|
||||
late final TextEditingController phoneController;
|
||||
|
||||
late final ContactsRepository _contactsRepository;
|
||||
late final SfTrackingRepository _tracking;
|
||||
|
||||
@override
|
||||
EditContactViewState build() {
|
||||
_contactsRepository = ref.read(contactsRepositoryProvider);
|
||||
_tracking = ref.read(sfTrackingProvider);
|
||||
|
||||
final contact = ref
|
||||
.read(contactsViewModelProvider)
|
||||
.contacts
|
||||
.where((c) => c.id == _contactId)
|
||||
.firstOrNull;
|
||||
|
||||
final parsed = contact?.phone;
|
||||
|
||||
nameController = TextEditingController(text: contact?.name ?? '');
|
||||
phoneController = TextEditingController(
|
||||
text: parsed?.nationalNumber ?? contact?.rawPhone ?? '',
|
||||
);
|
||||
|
||||
ref.onDispose(() {
|
||||
nameController.dispose();
|
||||
phoneController.dispose();
|
||||
});
|
||||
|
||||
return EditContactViewState(
|
||||
contactId: _contactId,
|
||||
isoCode: parsed?.isoCode ?? SfPhoneNumber.defaultIsoCode,
|
||||
contact: contact,
|
||||
);
|
||||
}
|
||||
|
||||
void updateCountry(String isoCode) {
|
||||
if (isoCode == state.isoCode) return;
|
||||
state = state.copyWith(isoCode: isoCode);
|
||||
}
|
||||
|
||||
void clearError() {
|
||||
if (state.error != null) state = state.copyWith(error: null);
|
||||
}
|
||||
|
||||
Future<bool> submit() async {
|
||||
final contact = state.contact;
|
||||
if (contact == null || state.isSubmitting) return false;
|
||||
|
||||
final name = nameController.text.trim();
|
||||
if (name.isEmpty) {
|
||||
state = state.copyWith(error: ContactError.nameRequired);
|
||||
return false;
|
||||
}
|
||||
|
||||
final phone = SfPhoneNumber.tryParse(
|
||||
phoneController.text,
|
||||
defaultIsoCode: state.isoCode,
|
||||
);
|
||||
if (phone == null) {
|
||||
state = state.copyWith(error: ContactError.phoneInvalid);
|
||||
return false;
|
||||
}
|
||||
|
||||
state = state.copyWith(isSubmitting: true, error: null);
|
||||
|
||||
try {
|
||||
await _contactsRepository.updateContact(
|
||||
request: UpdateContactRequestModel(
|
||||
id: contact.id,
|
||||
name: name,
|
||||
phone: phone.e164,
|
||||
),
|
||||
);
|
||||
if (!ref.mounted) return false;
|
||||
|
||||
unawaited(_tracking.legacyContactsEdited());
|
||||
|
||||
await ref.read(contactsViewModelProvider.notifier).refresh();
|
||||
return true;
|
||||
} catch (_) {
|
||||
if (!ref.mounted) return false;
|
||||
state = state.copyWith(isSubmitting: false, error: ContactError.network);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
import '../../domain/entities/contact_entity.dart';
|
||||
import '../../domain/entities/contact_error.dart';
|
||||
|
||||
part 'edit_contact_view_state.freezed.dart';
|
||||
|
||||
@freezed
|
||||
abstract class EditContactViewState with _$EditContactViewState {
|
||||
const factory EditContactViewState({
|
||||
required String contactId,
|
||||
required String isoCode,
|
||||
ContactEntity? contact,
|
||||
@Default(false) bool isSubmitting,
|
||||
ContactError? error,
|
||||
}) = _EditContactViewState;
|
||||
}
|
||||
@@ -0,0 +1,307 @@
|
||||
// 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 'edit_contact_view_state.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
/// @nodoc
|
||||
mixin _$EditContactViewState {
|
||||
|
||||
String get contactId; String get isoCode; ContactEntity? get contact; bool get isSubmitting; ContactError? get error;
|
||||
/// Create a copy of EditContactViewState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$EditContactViewStateCopyWith<EditContactViewState> get copyWith => _$EditContactViewStateCopyWithImpl<EditContactViewState>(this as EditContactViewState, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is EditContactViewState&&(identical(other.contactId, contactId) || other.contactId == contactId)&&(identical(other.isoCode, isoCode) || other.isoCode == isoCode)&&(identical(other.contact, contact) || other.contact == contact)&&(identical(other.isSubmitting, isSubmitting) || other.isSubmitting == isSubmitting)&&(identical(other.error, error) || other.error == error));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,contactId,isoCode,contact,isSubmitting,error);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'EditContactViewState(contactId: $contactId, isoCode: $isoCode, contact: $contact, isSubmitting: $isSubmitting, error: $error)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $EditContactViewStateCopyWith<$Res> {
|
||||
factory $EditContactViewStateCopyWith(EditContactViewState value, $Res Function(EditContactViewState) _then) = _$EditContactViewStateCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String contactId, String isoCode, ContactEntity? contact, bool isSubmitting, ContactError? error
|
||||
});
|
||||
|
||||
|
||||
$ContactEntityCopyWith<$Res>? get contact;
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$EditContactViewStateCopyWithImpl<$Res>
|
||||
implements $EditContactViewStateCopyWith<$Res> {
|
||||
_$EditContactViewStateCopyWithImpl(this._self, this._then);
|
||||
|
||||
final EditContactViewState _self;
|
||||
final $Res Function(EditContactViewState) _then;
|
||||
|
||||
/// Create a copy of EditContactViewState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? contactId = null,Object? isoCode = null,Object? contact = freezed,Object? isSubmitting = null,Object? error = freezed,}) {
|
||||
return _then(_self.copyWith(
|
||||
contactId: null == contactId ? _self.contactId : contactId // ignore: cast_nullable_to_non_nullable
|
||||
as String,isoCode: null == isoCode ? _self.isoCode : isoCode // ignore: cast_nullable_to_non_nullable
|
||||
as String,contact: freezed == contact ? _self.contact : contact // ignore: cast_nullable_to_non_nullable
|
||||
as ContactEntity?,isSubmitting: null == isSubmitting ? _self.isSubmitting : isSubmitting // ignore: cast_nullable_to_non_nullable
|
||||
as bool,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable
|
||||
as ContactError?,
|
||||
));
|
||||
}
|
||||
/// Create a copy of EditContactViewState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$ContactEntityCopyWith<$Res>? get contact {
|
||||
if (_self.contact == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $ContactEntityCopyWith<$Res>(_self.contact!, (value) {
|
||||
return _then(_self.copyWith(contact: value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [EditContactViewState].
|
||||
extension EditContactViewStatePatterns on EditContactViewState {
|
||||
/// 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( _EditContactViewState value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _EditContactViewState() 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( _EditContactViewState value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _EditContactViewState():
|
||||
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( _EditContactViewState value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _EditContactViewState() 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 contactId, String isoCode, ContactEntity? contact, bool isSubmitting, ContactError? error)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _EditContactViewState() when $default != null:
|
||||
return $default(_that.contactId,_that.isoCode,_that.contact,_that.isSubmitting,_that.error);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 contactId, String isoCode, ContactEntity? contact, bool isSubmitting, ContactError? error) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _EditContactViewState():
|
||||
return $default(_that.contactId,_that.isoCode,_that.contact,_that.isSubmitting,_that.error);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 contactId, String isoCode, ContactEntity? contact, bool isSubmitting, ContactError? error)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _EditContactViewState() when $default != null:
|
||||
return $default(_that.contactId,_that.isoCode,_that.contact,_that.isSubmitting,_that.error);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
|
||||
class _EditContactViewState implements EditContactViewState {
|
||||
const _EditContactViewState({required this.contactId, required this.isoCode, this.contact, this.isSubmitting = false, this.error});
|
||||
|
||||
|
||||
@override final String contactId;
|
||||
@override final String isoCode;
|
||||
@override final ContactEntity? contact;
|
||||
@override@JsonKey() final bool isSubmitting;
|
||||
@override final ContactError? error;
|
||||
|
||||
/// Create a copy of EditContactViewState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$EditContactViewStateCopyWith<_EditContactViewState> get copyWith => __$EditContactViewStateCopyWithImpl<_EditContactViewState>(this, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _EditContactViewState&&(identical(other.contactId, contactId) || other.contactId == contactId)&&(identical(other.isoCode, isoCode) || other.isoCode == isoCode)&&(identical(other.contact, contact) || other.contact == contact)&&(identical(other.isSubmitting, isSubmitting) || other.isSubmitting == isSubmitting)&&(identical(other.error, error) || other.error == error));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,contactId,isoCode,contact,isSubmitting,error);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'EditContactViewState(contactId: $contactId, isoCode: $isoCode, contact: $contact, isSubmitting: $isSubmitting, error: $error)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$EditContactViewStateCopyWith<$Res> implements $EditContactViewStateCopyWith<$Res> {
|
||||
factory _$EditContactViewStateCopyWith(_EditContactViewState value, $Res Function(_EditContactViewState) _then) = __$EditContactViewStateCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String contactId, String isoCode, ContactEntity? contact, bool isSubmitting, ContactError? error
|
||||
});
|
||||
|
||||
|
||||
@override $ContactEntityCopyWith<$Res>? get contact;
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$EditContactViewStateCopyWithImpl<$Res>
|
||||
implements _$EditContactViewStateCopyWith<$Res> {
|
||||
__$EditContactViewStateCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _EditContactViewState _self;
|
||||
final $Res Function(_EditContactViewState) _then;
|
||||
|
||||
/// Create a copy of EditContactViewState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? contactId = null,Object? isoCode = null,Object? contact = freezed,Object? isSubmitting = null,Object? error = freezed,}) {
|
||||
return _then(_EditContactViewState(
|
||||
contactId: null == contactId ? _self.contactId : contactId // ignore: cast_nullable_to_non_nullable
|
||||
as String,isoCode: null == isoCode ? _self.isoCode : isoCode // ignore: cast_nullable_to_non_nullable
|
||||
as String,contact: freezed == contact ? _self.contact : contact // ignore: cast_nullable_to_non_nullable
|
||||
as ContactEntity?,isSubmitting: null == isSubmitting ? _self.isSubmitting : isSubmitting // ignore: cast_nullable_to_non_nullable
|
||||
as bool,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable
|
||||
as ContactError?,
|
||||
));
|
||||
}
|
||||
|
||||
/// Create a copy of EditContactViewState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$ContactEntityCopyWith<$Res>? get contact {
|
||||
if (_self.contact == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $ContactEntityCopyWith<$Res>(_self.contact!, (value) {
|
||||
return _then(_self.copyWith(contact: value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// dart format on
|
||||
@@ -0,0 +1,140 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:sf_shared/sf_shared.dart';
|
||||
import 'package:sf_tracking/sf_tracking.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import '../../../../core/data/models/create_contact_request_model.dart';
|
||||
import '../../../../core/domain/repositories/contacts_repository.dart';
|
||||
import '../../../../core/providers/contacts_repository_provider.dart';
|
||||
import '../../domain/entities/contact_error.dart';
|
||||
import 'contacts_view_model.dart';
|
||||
import 'contacts_view_state.dart';
|
||||
import 'new_contact_view_state.dart';
|
||||
|
||||
final newContactViewModelProvider =
|
||||
NotifierProvider.autoDispose<NewContactViewModel, NewContactViewState>(
|
||||
NewContactViewModel.new,
|
||||
);
|
||||
|
||||
class NewContactViewModel extends Notifier<NewContactViewState> {
|
||||
static const _uuid = Uuid();
|
||||
|
||||
late final TextEditingController nameController;
|
||||
late final TextEditingController phoneController;
|
||||
|
||||
late final ContactsRepository _contactsRepository;
|
||||
late final SfTrackingRepository _tracking;
|
||||
|
||||
@override
|
||||
NewContactViewState build() {
|
||||
_contactsRepository = ref.read(contactsRepositoryProvider);
|
||||
_tracking = ref.read(sfTrackingProvider);
|
||||
|
||||
nameController = TextEditingController();
|
||||
phoneController = TextEditingController();
|
||||
|
||||
ref.onDispose(() {
|
||||
nameController.dispose();
|
||||
phoneController.dispose();
|
||||
});
|
||||
|
||||
return const NewContactViewState();
|
||||
}
|
||||
|
||||
void updateCountry(String isoCode) {
|
||||
if (isoCode == state.isoCode) return;
|
||||
state = state.copyWith(isoCode: isoCode);
|
||||
}
|
||||
|
||||
void clearError() {
|
||||
if (state.error != null) state = state.copyWith(error: null);
|
||||
}
|
||||
|
||||
Future<void> pickContactFromDevice() async {
|
||||
final response = await ref
|
||||
.read(deviceContactPickerProvider)
|
||||
.pick(hintIsoCode: state.isoCode);
|
||||
|
||||
if (response.outcome ==
|
||||
DeviceContactPickOutcome.permissionPermanentlyDenied) {
|
||||
state = state.copyWith(error: ContactError.contactsPermissionBlocked);
|
||||
return;
|
||||
}
|
||||
|
||||
final data = response.data;
|
||||
if (data == null) return;
|
||||
|
||||
final parsed = data.parsedPhone;
|
||||
if (parsed != null) {
|
||||
state = state.copyWith(isoCode: parsed.isoCode, error: null);
|
||||
phoneController.text = parsed.nationalNumber;
|
||||
} else {
|
||||
phoneController.text = data.rawNumber;
|
||||
}
|
||||
|
||||
if (nameController.text.trim().isEmpty) {
|
||||
nameController.text = data.displayName;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> openSystemSettings() =>
|
||||
ref.read(deviceContactPickerProvider).openSystemSettings();
|
||||
|
||||
Future<bool> submit() async {
|
||||
if (state.isSubmitting) return false;
|
||||
|
||||
final listState = ref.read(contactsViewModelProvider);
|
||||
if (listState.contacts.length >= kContactsMaxLimit) {
|
||||
state = state.copyWith(error: ContactError.maxReached);
|
||||
return false;
|
||||
}
|
||||
|
||||
final name = nameController.text.trim();
|
||||
if (name.isEmpty) {
|
||||
state = state.copyWith(error: ContactError.nameRequired);
|
||||
return false;
|
||||
}
|
||||
|
||||
final phone = SfPhoneNumber.tryParse(
|
||||
phoneController.text,
|
||||
defaultIsoCode: state.isoCode,
|
||||
);
|
||||
if (phone == null) {
|
||||
state = state.copyWith(error: ContactError.phoneInvalid);
|
||||
return false;
|
||||
}
|
||||
|
||||
state = state.copyWith(isSubmitting: true, error: null);
|
||||
|
||||
try {
|
||||
final user = await ref.read(userInfoProvider.future);
|
||||
if (!ref.mounted) return false;
|
||||
|
||||
await _contactsRepository.createContact(
|
||||
request: CreateContactRequestModel(
|
||||
id: _uuid.v4(),
|
||||
name: name,
|
||||
phone: phone.e164,
|
||||
userId: user.id,
|
||||
),
|
||||
);
|
||||
if (!ref.mounted) return false;
|
||||
|
||||
unawaited(
|
||||
_tracking.legacyContactsAdded(
|
||||
totalCount: listState.contacts.length + 1,
|
||||
),
|
||||
);
|
||||
|
||||
await ref.read(contactsViewModelProvider.notifier).refresh();
|
||||
return true;
|
||||
} catch (_) {
|
||||
if (!ref.mounted) return false;
|
||||
state = state.copyWith(isSubmitting: false, error: ContactError.network);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
import '../../domain/entities/contact_error.dart';
|
||||
|
||||
part 'new_contact_view_state.freezed.dart';
|
||||
|
||||
@freezed
|
||||
abstract class NewContactViewState with _$NewContactViewState {
|
||||
const factory NewContactViewState({
|
||||
@Default('ES') String isoCode,
|
||||
@Default(false) bool isSubmitting,
|
||||
ContactError? error,
|
||||
}) = _NewContactViewState;
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
// 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 'new_contact_view_state.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
/// @nodoc
|
||||
mixin _$NewContactViewState {
|
||||
|
||||
String get isoCode; bool get isSubmitting; ContactError? get error;
|
||||
/// Create a copy of NewContactViewState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$NewContactViewStateCopyWith<NewContactViewState> get copyWith => _$NewContactViewStateCopyWithImpl<NewContactViewState>(this as NewContactViewState, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is NewContactViewState&&(identical(other.isoCode, isoCode) || other.isoCode == isoCode)&&(identical(other.isSubmitting, isSubmitting) || other.isSubmitting == isSubmitting)&&(identical(other.error, error) || other.error == error));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,isoCode,isSubmitting,error);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'NewContactViewState(isoCode: $isoCode, isSubmitting: $isSubmitting, error: $error)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $NewContactViewStateCopyWith<$Res> {
|
||||
factory $NewContactViewStateCopyWith(NewContactViewState value, $Res Function(NewContactViewState) _then) = _$NewContactViewStateCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String isoCode, bool isSubmitting, ContactError? error
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$NewContactViewStateCopyWithImpl<$Res>
|
||||
implements $NewContactViewStateCopyWith<$Res> {
|
||||
_$NewContactViewStateCopyWithImpl(this._self, this._then);
|
||||
|
||||
final NewContactViewState _self;
|
||||
final $Res Function(NewContactViewState) _then;
|
||||
|
||||
/// Create a copy of NewContactViewState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? isoCode = null,Object? isSubmitting = null,Object? error = freezed,}) {
|
||||
return _then(_self.copyWith(
|
||||
isoCode: null == isoCode ? _self.isoCode : isoCode // ignore: cast_nullable_to_non_nullable
|
||||
as String,isSubmitting: null == isSubmitting ? _self.isSubmitting : isSubmitting // ignore: cast_nullable_to_non_nullable
|
||||
as bool,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable
|
||||
as ContactError?,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [NewContactViewState].
|
||||
extension NewContactViewStatePatterns on NewContactViewState {
|
||||
/// 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( _NewContactViewState value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _NewContactViewState() 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( _NewContactViewState value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _NewContactViewState():
|
||||
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( _NewContactViewState value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _NewContactViewState() 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 isoCode, bool isSubmitting, ContactError? error)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _NewContactViewState() when $default != null:
|
||||
return $default(_that.isoCode,_that.isSubmitting,_that.error);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 isoCode, bool isSubmitting, ContactError? error) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _NewContactViewState():
|
||||
return $default(_that.isoCode,_that.isSubmitting,_that.error);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 isoCode, bool isSubmitting, ContactError? error)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _NewContactViewState() when $default != null:
|
||||
return $default(_that.isoCode,_that.isSubmitting,_that.error);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
|
||||
class _NewContactViewState implements NewContactViewState {
|
||||
const _NewContactViewState({this.isoCode = 'ES', this.isSubmitting = false, this.error});
|
||||
|
||||
|
||||
@override@JsonKey() final String isoCode;
|
||||
@override@JsonKey() final bool isSubmitting;
|
||||
@override final ContactError? error;
|
||||
|
||||
/// Create a copy of NewContactViewState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$NewContactViewStateCopyWith<_NewContactViewState> get copyWith => __$NewContactViewStateCopyWithImpl<_NewContactViewState>(this, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _NewContactViewState&&(identical(other.isoCode, isoCode) || other.isoCode == isoCode)&&(identical(other.isSubmitting, isSubmitting) || other.isSubmitting == isSubmitting)&&(identical(other.error, error) || other.error == error));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,isoCode,isSubmitting,error);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'NewContactViewState(isoCode: $isoCode, isSubmitting: $isSubmitting, error: $error)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$NewContactViewStateCopyWith<$Res> implements $NewContactViewStateCopyWith<$Res> {
|
||||
factory _$NewContactViewStateCopyWith(_NewContactViewState value, $Res Function(_NewContactViewState) _then) = __$NewContactViewStateCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String isoCode, bool isSubmitting, ContactError? error
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$NewContactViewStateCopyWithImpl<$Res>
|
||||
implements _$NewContactViewStateCopyWith<$Res> {
|
||||
__$NewContactViewStateCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _NewContactViewState _self;
|
||||
final $Res Function(_NewContactViewState) _then;
|
||||
|
||||
/// Create a copy of NewContactViewState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? isoCode = null,Object? isSubmitting = null,Object? error = freezed,}) {
|
||||
return _then(_NewContactViewState(
|
||||
isoCode: null == isoCode ? _self.isoCode : isoCode // ignore: cast_nullable_to_non_nullable
|
||||
as String,isSubmitting: null == isSubmitting ? _self.isSubmitting : isSubmitting // ignore: cast_nullable_to_non_nullable
|
||||
as bool,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable
|
||||
as ContactError?,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// dart format on
|
||||
@@ -56,8 +56,8 @@ class ConfirmDeleteDialog extends ConsumerWidget {
|
||||
child: PrimaryButton(
|
||||
onPressed: () async {
|
||||
final vm = ref.read(contactsViewModelProvider.notifier);
|
||||
final success = await vm.deleteContact(contact);
|
||||
if (success && context.mounted) Navigator.pop(context);
|
||||
await vm.deleteContact(contact);
|
||||
if (context.mounted) Navigator.pop(context);
|
||||
},
|
||||
text: context.translate(I18n.delete),
|
||||
color: const Color(0xFFFF5D52),
|
||||
|
||||
@@ -61,7 +61,7 @@ class ContactCard extends ConsumerWidget {
|
||||
),
|
||||
),
|
||||
Text(
|
||||
contact.phone,
|
||||
contact.phone?.format() ?? contact.rawPhone,
|
||||
style: TextStyle(
|
||||
fontSize: SizeUtils.getByScreen(small: 14, big: 13),
|
||||
),
|
||||
|
||||
@@ -1,38 +1,43 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_contacts/flutter_contacts.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:sf_localizations/sf_localizations.dart';
|
||||
import 'package:utils/utils.dart';
|
||||
|
||||
import '../state/contacts_view_model.dart';
|
||||
import '../../domain/entities/contact_error.dart';
|
||||
import '../state/new_contact_view_model.dart';
|
||||
|
||||
class NewContactDialog extends ConsumerStatefulWidget {
|
||||
class NewContactDialog extends ConsumerWidget {
|
||||
const NewContactDialog({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<NewContactDialog> createState() => _NewContactDialogState();
|
||||
}
|
||||
|
||||
class _NewContactDialogState extends ConsumerState<NewContactDialog> {
|
||||
final _nameController = TextEditingController();
|
||||
final _phoneController = TextEditingController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nameController.dispose();
|
||||
_phoneController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = ref.watch(themePortProvider);
|
||||
|
||||
final dialCode = ref.read(
|
||||
contactsViewModelProvider.select((s) => s.dialCode),
|
||||
final vm = ref.read(newContactViewModelProvider.notifier);
|
||||
final isoCode = ref.watch(
|
||||
newContactViewModelProvider.select((s) => s.isoCode),
|
||||
);
|
||||
final isSubmitting = ref.watch(
|
||||
newContactViewModelProvider.select((s) => s.isSubmitting),
|
||||
);
|
||||
|
||||
ref.listen(newContactViewModelProvider.select((s) => s.error), (_, next) {
|
||||
if (next == null) return;
|
||||
if (next == ContactError.contactsPermissionBlocked) {
|
||||
showContactsPermissionDialog(
|
||||
context,
|
||||
onOpenSettings: vm.openSystemSettings,
|
||||
);
|
||||
} else {
|
||||
showTopSnackbar(
|
||||
context,
|
||||
message: context.translate(next.i18nKey),
|
||||
type: MessageType.error,
|
||||
);
|
||||
}
|
||||
vm.clearError();
|
||||
});
|
||||
|
||||
return Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
@@ -80,7 +85,7 @@ class _NewContactDialogState extends ConsumerState<NewContactDialog> {
|
||||
SizedBox(height: SizeUtils.getByScreen(small: 14, big: 12)),
|
||||
CustomTextField(
|
||||
hint: context.translate(I18n.name),
|
||||
controller: _nameController,
|
||||
controller: vm.nameController,
|
||||
),
|
||||
SizedBox(height: SizeUtils.getByScreen(small: 14, big: 12)),
|
||||
Row(
|
||||
@@ -88,16 +93,16 @@ class _NewContactDialogState extends ConsumerState<NewContactDialog> {
|
||||
children: [
|
||||
CountryPrefixPicker(
|
||||
headerText: context.translate(I18n.selectYourCountry),
|
||||
initialSelection: dialCode,
|
||||
initialSelection: isoCode,
|
||||
onChanged: (country) {
|
||||
final vm = ref.read(contactsViewModelProvider.notifier);
|
||||
vm.updateDialCode(country.dialCode ?? dialCode);
|
||||
final code = country.code;
|
||||
if (code != null) vm.updateCountry(code);
|
||||
},
|
||||
width: 80,
|
||||
),
|
||||
Expanded(
|
||||
child: CustomTextField(
|
||||
controller: _phoneController,
|
||||
controller: vm.phoneController,
|
||||
hint: context.translate(I18n.phoneNumber),
|
||||
keyboardType: TextInputType.phone,
|
||||
),
|
||||
@@ -108,7 +113,7 @@ class _NewContactDialogState extends ConsumerState<NewContactDialog> {
|
||||
color: theme.getColorFor(ThemeCode.legacyPrimary),
|
||||
),
|
||||
child: IconButton(
|
||||
onPressed: _pickContact,
|
||||
onPressed: vm.pickContactFromDevice,
|
||||
icon: Icon(
|
||||
SFIcons.contactsCircle,
|
||||
color: Colors.white,
|
||||
@@ -125,14 +130,12 @@ class _NewContactDialogState extends ConsumerState<NewContactDialog> {
|
||||
vertical: SizeUtils.getByScreen(small: 10, big: 8),
|
||||
),
|
||||
child: PrimaryButton(
|
||||
onPressed: () async {
|
||||
final vm = ref.read(contactsViewModelProvider.notifier);
|
||||
final success = await vm.createContact(
|
||||
name: _nameController.text,
|
||||
phone: _phoneController.text,
|
||||
);
|
||||
if (success && context.mounted) Navigator.pop(context);
|
||||
},
|
||||
onPressed: isSubmitting
|
||||
? null
|
||||
: () async {
|
||||
final success = await vm.submit();
|
||||
if (success && context.mounted) Navigator.pop(context);
|
||||
},
|
||||
text: context.translate(I18n.save),
|
||||
color: theme.getColorFor(ThemeCode.legacyPrimary),
|
||||
),
|
||||
@@ -142,17 +145,5 @@ class _NewContactDialogState extends ConsumerState<NewContactDialog> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _pickContact() async {
|
||||
final contact = await FlutterContacts.openExternalPick();
|
||||
if (contact == null || !mounted) return;
|
||||
|
||||
final fullContact = await FlutterContacts.getContact(
|
||||
contact.id,
|
||||
withProperties: true,
|
||||
);
|
||||
if (fullContact == null || fullContact.phones.isEmpty) return;
|
||||
|
||||
_phoneController.text = fullContact.phones.first.number;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user