refactor(device-management): split contacts feature into list/new/edit view models and migrate to SfPhoneNumber

This commit is contained in:
2026-04-15 17:05:56 +02:00
parent 85be483c4e
commit 88c1111bd5
19 changed files with 1117 additions and 308 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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