refactor(device_management): migrate contacts to Riverpod
This commit is contained in:
@@ -1,17 +1,19 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:legacy_theme/legacy_theme.dart';
|
||||
import 'package:device_management/src/features/contacts/presentation/providers/contacts_controller.dart';
|
||||
import 'package:device_management/src/features/contacts/presentation/providers/contacts_editing_mode_provider.dart';
|
||||
import 'package:device_management/src/features/contacts/presentation/providers/contacts_provider.dart';
|
||||
import 'package:device_management/src/features/contacts/presentation/widgets/contact_card.dart';
|
||||
import 'package:device_management/src/features/contacts/presentation/widgets/new_contact_dialog.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:legacy_device_state/legacy_device_state.dart';
|
||||
import 'package:legacy_theme/legacy_theme.dart';
|
||||
import 'package:legacy_ui/legacy_ui.dart';
|
||||
import 'package:navigation/navigation.dart';
|
||||
import 'package:sf_localizations/sf_localizations.dart';
|
||||
import 'package:sf_shared/sf_shared.dart';
|
||||
import 'package:utils/utils.dart';
|
||||
|
||||
import 'state/contacts_view_model.dart';
|
||||
import 'widgets/contact_card.dart';
|
||||
import 'widgets/new_contact_dialog.dart';
|
||||
|
||||
class ContactsScreen extends ConsumerWidget {
|
||||
final NavigationContract navigationContract;
|
||||
|
||||
@@ -19,61 +21,106 @@ class ContactsScreen extends ConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final vm = ref.read(contactsViewModelProvider.notifier);
|
||||
final state = ref.watch(contactsViewModelProvider);
|
||||
final primaryColor = context.sfColors.legacyPrimary;
|
||||
final isEditing = ref.watch(contactsEditingModeProvider);
|
||||
final userAsync = ref.watch(userInfoProvider);
|
||||
final device = ref.watch(selectedDeviceProvider).value;
|
||||
final maxContacts =
|
||||
device?.capabilities?.contacts?.maxForType('white') ?? 10;
|
||||
|
||||
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();
|
||||
ref.listen(contactsControllerProvider, (prev, next) async {
|
||||
if (prev == null || !prev.isLoading || next.isLoading) return;
|
||||
if (next.hasError) {
|
||||
final error = next.error;
|
||||
if (error is ContactsMinReachedException) {
|
||||
await showErrorDialog(context, I18n.errorContactsMin);
|
||||
} else if (error is ContactsMaxReachedException) {
|
||||
await showErrorDialog(context, I18n.errorContactsMax);
|
||||
} else {
|
||||
await next.showErrorOn(context);
|
||||
}
|
||||
return;
|
||||
}
|
||||
final action =
|
||||
ref.read(contactsControllerProvider.notifier).lastAction;
|
||||
final key = switch (action) {
|
||||
ContactsAction.create => I18n.contactAdded,
|
||||
ContactsAction.update => I18n.contactUpdated,
|
||||
ContactsAction.delete => I18n.contactDeleted,
|
||||
null => I18n.deviceUpdatedSuccess,
|
||||
};
|
||||
await showSuccessDialog(context, key);
|
||||
});
|
||||
|
||||
return LegacyPageLayout(
|
||||
title: context.translate(I18n.contactsAgendaTitle),
|
||||
showEdit: true,
|
||||
onEditChange: vm.toggleIsEditing,
|
||||
body: state.isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: state.contacts.isEmpty
|
||||
? const _EmptyState()
|
||||
: ListView.separated(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: SizeUtils.getByScreen(small: 22, big: 21),
|
||||
vertical: SizeUtils.getByScreen(small: 10, big: 8),
|
||||
),
|
||||
itemBuilder: (_, index) => ContactCard(
|
||||
contact: state.contacts[index],
|
||||
isEditing: state.isEditing,
|
||||
),
|
||||
separatorBuilder: (_, __) =>
|
||||
SizedBox(height: SizeUtils.getByScreen(small: 18, big: 17)),
|
||||
itemCount: state.contacts.length,
|
||||
onEditChange: ref.read(contactsEditingModeProvider.notifier).toggle,
|
||||
body: userAsync.when(
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (_, __) => Center(
|
||||
child: Text(context.translate(I18n.errorGeneric)),
|
||||
),
|
||||
data: (user) {
|
||||
final contactsAsync = ref.watch(contactsProvider(user.id));
|
||||
return contactsAsync.when(
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (_, __) => Center(
|
||||
child: Text(context.translate(I18n.errorGeneric)),
|
||||
),
|
||||
data: (contacts) {
|
||||
if (contacts.isEmpty) return const _EmptyState();
|
||||
return ListView.separated(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: SizeUtils.getByScreen(small: 22, big: 21),
|
||||
vertical: SizeUtils.getByScreen(small: 10, big: 8),
|
||||
),
|
||||
itemBuilder: (_, index) => ContactCard(
|
||||
contact: contacts[index],
|
||||
isEditing: isEditing,
|
||||
onDelete: () => _confirmDelete(
|
||||
context,
|
||||
ref,
|
||||
userId: user.id,
|
||||
contactId: contacts[index].id,
|
||||
currentCount: contacts.length,
|
||||
),
|
||||
),
|
||||
separatorBuilder: (_, __) => SizedBox(
|
||||
height: SizeUtils.getByScreen(small: 18, big: 17),
|
||||
),
|
||||
itemCount: contacts.length,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
footer: Material(
|
||||
color: context.sfColors.legacyPrimary,
|
||||
color: primaryColor,
|
||||
shape: const CircleBorder(),
|
||||
child: InkWell(
|
||||
customBorder: const CircleBorder(),
|
||||
onTap: () async {
|
||||
if (!await guardDeviceCommand(context, ref)) return;
|
||||
if (!context.mounted) return;
|
||||
if (state.contacts.length >= state.maxContacts) {
|
||||
showTopSnackbar(
|
||||
context,
|
||||
message: context.translate(I18n.errorContactsMax),
|
||||
);
|
||||
final user = userAsync.value;
|
||||
if (user == null) return;
|
||||
final contacts =
|
||||
ref.read(contactsProvider(user.id)).value ?? const [];
|
||||
if (contacts.length >= maxContacts) {
|
||||
await showErrorDialog(context, I18n.errorContactsMax);
|
||||
return;
|
||||
}
|
||||
|
||||
showDialog(
|
||||
if (!context.mounted) return;
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => const Dialog(
|
||||
builder: (_) => Dialog(
|
||||
backgroundColor: Colors.transparent,
|
||||
child: NewContactDialog(),
|
||||
child: NewContactDialog(
|
||||
userId: user.id,
|
||||
currentCount: contacts.length,
|
||||
maxContacts: maxContacts,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -90,10 +137,43 @@ class ContactsScreen extends ConsumerWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _confirmDelete(
|
||||
BuildContext context,
|
||||
WidgetRef ref, {
|
||||
required String userId,
|
||||
required String contactId,
|
||||
required int currentCount,
|
||||
}) async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: Text(context.translate(I18n.deleteContactMessage)),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(dialogContext, false),
|
||||
child: Text(context.translate(I18n.cancel)),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.pop(dialogContext, true),
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
child: Text(context.translate(I18n.delete)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (confirmed != true) return;
|
||||
ref.read(contactsControllerProvider.notifier).deleteContact(
|
||||
userId: userId,
|
||||
contactId: contactId,
|
||||
currentCount: currentCount,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _EmptyState extends StatelessWidget {
|
||||
|
||||
const _EmptyState();
|
||||
|
||||
@override
|
||||
@@ -105,8 +185,8 @@ class _EmptyState extends StatelessWidget {
|
||||
Icon(
|
||||
SFIcons.contactsCircle,
|
||||
size: SizeUtils.getByScreen(small: 64, big: 60),
|
||||
color: Theme.of(context).colorScheme.onSurface
|
||||
.withValues(alpha: 0.3),
|
||||
color:
|
||||
Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.3),
|
||||
),
|
||||
SizedBox(height: SizeUtils.getByScreen(small: 16, big: 14)),
|
||||
Text(
|
||||
@@ -122,7 +202,9 @@ class _EmptyState extends StatelessWidget {
|
||||
context.translate(I18n.contactsEmptyHint),
|
||||
style: TextStyle(
|
||||
fontSize: SizeUtils.getByScreen(small: 14, big: 13),
|
||||
color: Theme.of(context).colorScheme.onSurface
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface
|
||||
.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:legacy_theme/legacy_theme.dart';
|
||||
import 'package:device_management/src/features/contacts/domain/entities/contact_entity.dart';
|
||||
import 'package:device_management/src/features/contacts/presentation/providers/contacts_controller.dart';
|
||||
import 'package:device_management/src/features/contacts/presentation/providers/contacts_provider.dart';
|
||||
import 'package:device_management/src/features/contacts/presentation/providers/edit_contact_form_provider.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:legacy_theme/legacy_theme.dart';
|
||||
import 'package:navigation/navigation.dart';
|
||||
import 'package:sf_localizations/sf_localizations.dart';
|
||||
import 'package:sf_shared/sf_shared.dart';
|
||||
import 'package:utils/utils.dart';
|
||||
|
||||
import 'state/edit_contact_view_model.dart';
|
||||
|
||||
class EditContactScreen extends ConsumerWidget {
|
||||
final String contactId;
|
||||
|
||||
@@ -17,68 +20,168 @@ class EditContactScreen extends ConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final provider = editContactViewModelProvider(contactId);
|
||||
final vm = ref.read(provider.notifier);
|
||||
final state = ref.watch(provider);
|
||||
|
||||
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.of(context).colorScheme.surface,
|
||||
body: Center(child: Text(context.translate(I18n.errorGeneric))),
|
||||
);
|
||||
}
|
||||
final userAsync = ref.watch(userInfoProvider);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: SizeUtils.getByScreen(small: 22, big: 21),
|
||||
vertical: SizeUtils.getByScreen(small: 10, big: 8),
|
||||
child: userAsync.when(
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (_, __) => Center(
|
||||
child: Text(context.translate(I18n.errorGeneric)),
|
||||
),
|
||||
data: (user) {
|
||||
final contactsAsync = ref.watch(contactsProvider(user.id));
|
||||
return contactsAsync.when(
|
||||
loading: () =>
|
||||
const Center(child: CircularProgressIndicator()),
|
||||
error: (_, __) => Center(
|
||||
child: Text(context.translate(I18n.errorGeneric)),
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () => GetIt.I<NavigationContract>().goBack(),
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
),
|
||||
Center(
|
||||
child: Text(
|
||||
context.translate(I18n.editContact),
|
||||
style: TextStyle(
|
||||
fontSize: SizeUtils.getByScreen(small: 28, big: 27),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
data: (contacts) {
|
||||
final contact =
|
||||
contacts.where((c) => c.id == contactId).firstOrNull;
|
||||
if (contact == null) {
|
||||
return Center(
|
||||
child: Text(context.translate(I18n.errorGeneric)),
|
||||
);
|
||||
}
|
||||
return _EditContactForm(userId: user.id, contact: contact);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _EditContactForm extends ConsumerStatefulWidget {
|
||||
final String userId;
|
||||
final ContactEntity contact;
|
||||
|
||||
const _EditContactForm({required this.userId, required this.contact});
|
||||
|
||||
@override
|
||||
ConsumerState<_EditContactForm> createState() => _EditContactFormState();
|
||||
}
|
||||
|
||||
class _EditContactFormState extends ConsumerState<_EditContactForm> {
|
||||
late final TextEditingController _nameController;
|
||||
late final TextEditingController _phoneController;
|
||||
late final String _initialIsoCode;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final parsed = widget.contact.phone;
|
||||
_nameController = TextEditingController(text: widget.contact.name);
|
||||
_phoneController = TextEditingController(
|
||||
text: parsed?.nationalNumber ?? widget.contact.rawPhone,
|
||||
);
|
||||
_initialIsoCode = parsed?.isoCode ?? SfPhoneNumber.defaultIsoCode;
|
||||
|
||||
_nameController.addListener(_clearError);
|
||||
_phoneController.addListener(_clearError);
|
||||
|
||||
ref.listenManual(contactsControllerProvider, (prev, next) async {
|
||||
if (prev == null || !prev.isLoading || next.isLoading) return;
|
||||
final action =
|
||||
ref.read(contactsControllerProvider.notifier).lastAction;
|
||||
if (action != ContactsAction.update) return;
|
||||
if (next.hasError) {
|
||||
await next.showErrorOn(context);
|
||||
return;
|
||||
}
|
||||
if (mounted) GetIt.I<NavigationContract>().goBack();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nameController.removeListener(_clearError);
|
||||
_phoneController.removeListener(_clearError);
|
||||
_nameController.dispose();
|
||||
_phoneController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _clearError() {
|
||||
ref.read(editContactFormProvider(_initialIsoCode).notifier).clearError();
|
||||
}
|
||||
|
||||
void _submit() {
|
||||
final formNotifier =
|
||||
ref.read(editContactFormProvider(_initialIsoCode).notifier);
|
||||
final name = _nameController.text.trim();
|
||||
if (name.isEmpty) {
|
||||
formNotifier.setLocalError(I18n.errorFirstNameRequired);
|
||||
return;
|
||||
}
|
||||
|
||||
final isoCode =
|
||||
ref.read(editContactFormProvider(_initialIsoCode)).isoCode;
|
||||
final parsed = SfPhoneNumber.tryParse(
|
||||
_phoneController.text,
|
||||
defaultIsoCode: isoCode,
|
||||
);
|
||||
if (parsed == null) {
|
||||
formNotifier.setLocalError(I18n.errorMessagePhoneIsInvalid);
|
||||
return;
|
||||
}
|
||||
|
||||
formNotifier.clearError();
|
||||
ref.read(contactsControllerProvider.notifier).updateContact(
|
||||
userId: widget.userId,
|
||||
contactId: widget.contact.id,
|
||||
name: name,
|
||||
phoneE164: parsed.e164,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final primaryColor = context.sfColors.legacyPrimary;
|
||||
final formState = ref.watch(editContactFormProvider(_initialIsoCode));
|
||||
final isSubmitting =
|
||||
ref.watch(contactsControllerProvider.select((s) => s.isLoading));
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: SizeUtils.getByScreen(small: 22, big: 21),
|
||||
vertical: SizeUtils.getByScreen(small: 10, big: 8),
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () => GetIt.I<NavigationContract>().goBack(),
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
),
|
||||
),
|
||||
SizedBox(height: SizeUtils.getByScreen(small: 20, big: 18)),
|
||||
Expanded(
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: SizeUtils.getByScreen(small: 48, big: 47),
|
||||
vertical: SizeUtils.getByScreen(small: 10, big: 8),
|
||||
Center(
|
||||
child: Text(
|
||||
context.translate(I18n.editContact),
|
||||
style: TextStyle(
|
||||
fontSize: SizeUtils.getByScreen(small: 28, big: 27),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: SizeUtils.getByScreen(small: 20, big: 18)),
|
||||
Expanded(
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: SizeUtils.getByScreen(small: 48, big: 47),
|
||||
vertical: SizeUtils.getByScreen(small: 10, big: 8),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Center(
|
||||
@@ -90,7 +193,7 @@ class EditContactScreen extends ConsumerWidget {
|
||||
height: SizeUtils.getByScreen(small: 24, big: 22),
|
||||
),
|
||||
CustomTextField(
|
||||
controller: vm.nameController,
|
||||
controller: _nameController,
|
||||
label: context.translate(I18n.name),
|
||||
),
|
||||
SizedBox(
|
||||
@@ -102,10 +205,16 @@ class EditContactScreen extends ConsumerWidget {
|
||||
headerText: context.translate(
|
||||
I18n.selectYourCountry,
|
||||
),
|
||||
initialSelection: state.isoCode,
|
||||
initialSelection: formState.isoCode,
|
||||
onChanged: (country) {
|
||||
final code = country.code;
|
||||
if (code != null) vm.updateCountry(code);
|
||||
if (code != null) {
|
||||
ref
|
||||
.read(editContactFormProvider(
|
||||
_initialIsoCode)
|
||||
.notifier)
|
||||
.setIsoCode(code);
|
||||
}
|
||||
},
|
||||
width: 80,
|
||||
),
|
||||
@@ -118,37 +227,38 @@ class EditContactScreen extends ConsumerWidget {
|
||||
),
|
||||
Expanded(
|
||||
child: CustomTextField(
|
||||
controller: vm.phoneController,
|
||||
controller: _phoneController,
|
||||
keyboardType: TextInputType.number,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (formState.localError != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: Text(
|
||||
context.translate(formState.localError!),
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: SizeUtils.getByScreen(small: 16, big: 14)),
|
||||
PrimaryButton(
|
||||
onPressed: state.isSubmitting
|
||||
? null
|
||||
: () async {
|
||||
final success = await vm.submit();
|
||||
if (success && context.mounted) {
|
||||
GetIt.I<NavigationContract>().goBack();
|
||||
}
|
||||
},
|
||||
text: context.translate(I18n.save),
|
||||
color: context.sfColors.legacyPrimary,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: SizeUtils.getByScreen(small: 16, big: 14)),
|
||||
PrimaryButton(
|
||||
onPressed: isSubmitting ? null : _submit,
|
||||
text: context.translate(I18n.save),
|
||||
color: primaryColor,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:device_management/src/core/data/models/create_contact_request_dto.dart';
|
||||
import 'package:device_management/src/core/data/models/update_contact_request_dto.dart';
|
||||
import 'package:device_management/src/core/providers/contacts_repository_provider.dart';
|
||||
import 'package:device_management/src/features/contacts/presentation/providers/contacts_provider.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:sf_tracking/sf_tracking.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
part 'contacts_controller.g.dart';
|
||||
|
||||
class ContactsMinReachedException implements Exception {
|
||||
const ContactsMinReachedException();
|
||||
}
|
||||
|
||||
class ContactsMaxReachedException implements Exception {
|
||||
const ContactsMaxReachedException();
|
||||
}
|
||||
|
||||
enum ContactsAction { create, update, delete }
|
||||
|
||||
@riverpod
|
||||
class ContactsController extends _$ContactsController {
|
||||
static const _uuid = Uuid();
|
||||
|
||||
ContactsAction? _lastAction;
|
||||
|
||||
ContactsAction? get lastAction => _lastAction;
|
||||
|
||||
@override
|
||||
FutureOr<void> build() {}
|
||||
|
||||
Future<void> createContact({
|
||||
required String userId,
|
||||
required String name,
|
||||
required String phoneE164,
|
||||
required int currentCount,
|
||||
required int maxCount,
|
||||
}) async {
|
||||
_lastAction = ContactsAction.create;
|
||||
state = const AsyncLoading();
|
||||
if (currentCount >= maxCount) {
|
||||
state = AsyncError(
|
||||
const ContactsMaxReachedException(),
|
||||
StackTrace.current,
|
||||
);
|
||||
return;
|
||||
}
|
||||
state = await AsyncValue.guard(() async {
|
||||
await ref.read(contactsRepositoryProvider).createContact(
|
||||
request: CreateContactRequestDto(
|
||||
id: _uuid.v4(),
|
||||
name: name,
|
||||
phone: phoneE164,
|
||||
userId: userId,
|
||||
),
|
||||
);
|
||||
ref.invalidate(contactsProvider(userId));
|
||||
unawaited(
|
||||
ref
|
||||
.read(sfTrackingProvider)
|
||||
.legacyContactsAdded(totalCount: currentCount + 1),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> updateContact({
|
||||
required String userId,
|
||||
required String contactId,
|
||||
required String name,
|
||||
required String phoneE164,
|
||||
}) async {
|
||||
_lastAction = ContactsAction.update;
|
||||
state = const AsyncLoading();
|
||||
state = await AsyncValue.guard(() async {
|
||||
await ref.read(contactsRepositoryProvider).updateContact(
|
||||
request: UpdateContactRequestDto(
|
||||
id: contactId,
|
||||
name: name,
|
||||
phone: phoneE164,
|
||||
),
|
||||
);
|
||||
ref.invalidate(contactsProvider(userId));
|
||||
unawaited(ref.read(sfTrackingProvider).legacyContactsEdited());
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> deleteContact({
|
||||
required String userId,
|
||||
required String contactId,
|
||||
required int currentCount,
|
||||
}) async {
|
||||
_lastAction = ContactsAction.delete;
|
||||
state = const AsyncLoading();
|
||||
if (currentCount <= 1) {
|
||||
state = AsyncError(
|
||||
const ContactsMinReachedException(),
|
||||
StackTrace.current,
|
||||
);
|
||||
return;
|
||||
}
|
||||
state = await AsyncValue.guard(() async {
|
||||
await ref
|
||||
.read(contactsRepositoryProvider)
|
||||
.deleteContact(contactId: contactId);
|
||||
ref.invalidate(contactsProvider(userId));
|
||||
unawaited(
|
||||
ref
|
||||
.read(sfTrackingProvider)
|
||||
.legacyContactsDeleted(totalCount: currentCount - 1),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'contacts_controller.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(ContactsController)
|
||||
const contactsControllerProvider = ContactsControllerProvider._();
|
||||
|
||||
final class ContactsControllerProvider
|
||||
extends $AsyncNotifierProvider<ContactsController, void> {
|
||||
const ContactsControllerProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'contactsControllerProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$contactsControllerHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
ContactsController create() => ContactsController();
|
||||
}
|
||||
|
||||
String _$contactsControllerHash() =>
|
||||
r'a304d9260a03626ce75b0cf5e62bf2663ee9feec';
|
||||
|
||||
abstract class _$ContactsController extends $AsyncNotifier<void> {
|
||||
FutureOr<void> build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
build();
|
||||
final ref = this.ref as $Ref<AsyncValue<void>, void>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<AsyncValue<void>, void>,
|
||||
AsyncValue<void>,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleValue(ref, null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'contacts_editing_mode_provider.g.dart';
|
||||
|
||||
@riverpod
|
||||
class ContactsEditingMode extends _$ContactsEditingMode {
|
||||
@override
|
||||
bool build() => false;
|
||||
|
||||
void toggle() => state = !state;
|
||||
|
||||
void set(bool value) {
|
||||
if (value == state) return;
|
||||
state = value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'contacts_editing_mode_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(ContactsEditingMode)
|
||||
const contactsEditingModeProvider = ContactsEditingModeProvider._();
|
||||
|
||||
final class ContactsEditingModeProvider
|
||||
extends $NotifierProvider<ContactsEditingMode, bool> {
|
||||
const ContactsEditingModeProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'contactsEditingModeProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$contactsEditingModeHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
ContactsEditingMode create() => ContactsEditingMode();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(bool value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<bool>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$contactsEditingModeHash() =>
|
||||
r'b06c06615bcd5e5f3f74e82f9e4695150df25123';
|
||||
|
||||
abstract class _$ContactsEditingMode extends $Notifier<bool> {
|
||||
bool build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final created = build();
|
||||
final ref = this.ref as $Ref<bool, bool>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<bool, bool>,
|
||||
bool,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleValue(ref, created);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:device_management/src/core/providers/contacts_repository_provider.dart';
|
||||
import 'package:device_management/src/features/contacts/domain/entities/contact_entity.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:sf_shared/sf_shared.dart';
|
||||
|
||||
part 'contacts_provider.g.dart';
|
||||
|
||||
@riverpod
|
||||
Future<List<ContactEntity>> contacts(Ref ref, String userId) async {
|
||||
final repo = ref.read(contactsRepositoryProvider);
|
||||
final contacts = await repo.getContacts(userId: userId);
|
||||
|
||||
final device = ref.read(selectedDeviceProvider).value;
|
||||
if (device != null) {
|
||||
unawaited(
|
||||
repo.syncContactsToDevice(
|
||||
userId: userId,
|
||||
deviceId: device.id,
|
||||
contacts: contacts,
|
||||
),
|
||||
);
|
||||
}
|
||||
return contacts;
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'contacts_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(contacts)
|
||||
const contactsProvider = ContactsFamily._();
|
||||
|
||||
final class ContactsProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
AsyncValue<List<ContactEntity>>,
|
||||
List<ContactEntity>,
|
||||
FutureOr<List<ContactEntity>>
|
||||
>
|
||||
with
|
||||
$FutureModifier<List<ContactEntity>>,
|
||||
$FutureProvider<List<ContactEntity>> {
|
||||
const ContactsProvider._({
|
||||
required ContactsFamily super.from,
|
||||
required String super.argument,
|
||||
}) : super(
|
||||
retry: null,
|
||||
name: r'contactsProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$contactsHash();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return r'contactsProvider'
|
||||
''
|
||||
'($argument)';
|
||||
}
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<List<ContactEntity>> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<List<ContactEntity>> create(Ref ref) {
|
||||
final argument = this.argument as String;
|
||||
return contacts(ref, argument);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is ContactsProvider && other.argument == argument;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return argument.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
String _$contactsHash() => r'59d591cbb75107782b89807213671eb9302f8133';
|
||||
|
||||
final class ContactsFamily extends $Family
|
||||
with $FunctionalFamilyOverride<FutureOr<List<ContactEntity>>, String> {
|
||||
const ContactsFamily._()
|
||||
: super(
|
||||
retry: null,
|
||||
name: r'contactsProvider',
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
isAutoDispose: true,
|
||||
);
|
||||
|
||||
ContactsProvider call(String userId) =>
|
||||
ContactsProvider._(argument: userId, from: this);
|
||||
|
||||
@override
|
||||
String toString() => r'contactsProvider';
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:sf_shared/sf_shared.dart';
|
||||
|
||||
part 'edit_contact_form_provider.g.dart';
|
||||
|
||||
class EditContactFormState {
|
||||
const EditContactFormState({required this.isoCode, this.localError});
|
||||
|
||||
final String isoCode;
|
||||
final String? localError;
|
||||
|
||||
EditContactFormState copyWith({
|
||||
String? isoCode,
|
||||
String? localError,
|
||||
bool clearError = false,
|
||||
}) {
|
||||
return EditContactFormState(
|
||||
isoCode: isoCode ?? this.isoCode,
|
||||
localError: clearError ? null : (localError ?? this.localError),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@riverpod
|
||||
class EditContactForm extends _$EditContactForm {
|
||||
@override
|
||||
EditContactFormState build(String initialIsoCode) => EditContactFormState(
|
||||
isoCode: initialIsoCode.isNotEmpty
|
||||
? initialIsoCode
|
||||
: SfPhoneNumber.defaultIsoCode,
|
||||
);
|
||||
|
||||
void setIsoCode(String code) {
|
||||
if (code == state.isoCode) return;
|
||||
state = state.copyWith(isoCode: code);
|
||||
}
|
||||
|
||||
void setLocalError(String key) {
|
||||
state = state.copyWith(localError: key);
|
||||
}
|
||||
|
||||
void clearError() {
|
||||
if (state.localError == null) return;
|
||||
state = state.copyWith(clearError: true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'edit_contact_form_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(EditContactForm)
|
||||
const editContactFormProvider = EditContactFormFamily._();
|
||||
|
||||
final class EditContactFormProvider
|
||||
extends $NotifierProvider<EditContactForm, EditContactFormState> {
|
||||
const EditContactFormProvider._({
|
||||
required EditContactFormFamily super.from,
|
||||
required String super.argument,
|
||||
}) : super(
|
||||
retry: null,
|
||||
name: r'editContactFormProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$editContactFormHash();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return r'editContactFormProvider'
|
||||
''
|
||||
'($argument)';
|
||||
}
|
||||
|
||||
@$internal
|
||||
@override
|
||||
EditContactForm create() => EditContactForm();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(EditContactFormState value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<EditContactFormState>(value),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is EditContactFormProvider && other.argument == argument;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return argument.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
String _$editContactFormHash() => r'8a6ee15611761bd53cf1d335bd16ed5443d7400c';
|
||||
|
||||
final class EditContactFormFamily extends $Family
|
||||
with
|
||||
$ClassFamilyOverride<
|
||||
EditContactForm,
|
||||
EditContactFormState,
|
||||
EditContactFormState,
|
||||
EditContactFormState,
|
||||
String
|
||||
> {
|
||||
const EditContactFormFamily._()
|
||||
: super(
|
||||
retry: null,
|
||||
name: r'editContactFormProvider',
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
isAutoDispose: true,
|
||||
);
|
||||
|
||||
EditContactFormProvider call(String initialIsoCode) =>
|
||||
EditContactFormProvider._(argument: initialIsoCode, from: this);
|
||||
|
||||
@override
|
||||
String toString() => r'editContactFormProvider';
|
||||
}
|
||||
|
||||
abstract class _$EditContactForm extends $Notifier<EditContactFormState> {
|
||||
late final _$args = ref.$arg as String;
|
||||
String get initialIsoCode => _$args;
|
||||
|
||||
EditContactFormState build(String initialIsoCode);
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final created = build(_$args);
|
||||
final ref = this.ref as $Ref<EditContactFormState, EditContactFormState>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<EditContactFormState, EditContactFormState>,
|
||||
EditContactFormState,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleValue(ref, created);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'new_contact_form_provider.g.dart';
|
||||
|
||||
class NewContactFormState {
|
||||
const NewContactFormState({
|
||||
this.isoCode = 'ES',
|
||||
this.localError,
|
||||
});
|
||||
|
||||
final String isoCode;
|
||||
final String? localError;
|
||||
|
||||
NewContactFormState copyWith({
|
||||
String? isoCode,
|
||||
String? localError,
|
||||
bool clearError = false,
|
||||
}) {
|
||||
return NewContactFormState(
|
||||
isoCode: isoCode ?? this.isoCode,
|
||||
localError: clearError ? null : (localError ?? this.localError),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@riverpod
|
||||
class NewContactForm extends _$NewContactForm {
|
||||
@override
|
||||
NewContactFormState build() => const NewContactFormState();
|
||||
|
||||
void setIsoCode(String code) {
|
||||
if (code == state.isoCode) return;
|
||||
state = state.copyWith(isoCode: code);
|
||||
}
|
||||
|
||||
void setLocalError(String key) {
|
||||
state = state.copyWith(localError: key);
|
||||
}
|
||||
|
||||
void clearError() {
|
||||
if (state.localError == null) return;
|
||||
state = state.copyWith(clearError: true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'new_contact_form_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(NewContactForm)
|
||||
const newContactFormProvider = NewContactFormProvider._();
|
||||
|
||||
final class NewContactFormProvider
|
||||
extends $NotifierProvider<NewContactForm, NewContactFormState> {
|
||||
const NewContactFormProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'newContactFormProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$newContactFormHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
NewContactForm create() => NewContactForm();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(NewContactFormState value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<NewContactFormState>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$newContactFormHash() => r'fc9f3c830229b36ebafbe10fa788e8b02294c215';
|
||||
|
||||
abstract class _$NewContactForm extends $Notifier<NewContactFormState> {
|
||||
NewContactFormState build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final created = build();
|
||||
final ref = this.ref as $Ref<NewContactFormState, NewContactFormState>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<NewContactFormState, NewContactFormState>,
|
||||
NewContactFormState,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleValue(ref, created);
|
||||
}
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:sf_shared/sf_shared.dart';
|
||||
import 'package:sf_tracking/sf_tracking.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 =
|
||||
NotifierProvider.autoDispose<ContactsViewModel, ContactsViewState>(
|
||||
ContactsViewModel.new,
|
||||
);
|
||||
|
||||
class ContactsViewModel extends Notifier<ContactsViewState> {
|
||||
late final ContactsRepository _contactsRepository;
|
||||
late final SfTrackingRepository _tracking;
|
||||
|
||||
@override
|
||||
ContactsViewState build() {
|
||||
_contactsRepository = ref.read(contactsRepositoryProvider);
|
||||
_tracking = ref.read(sfTrackingProvider);
|
||||
|
||||
final capabilities = ref.read(selectedDeviceProvider).value?.capabilities;
|
||||
final maxContacts = capabilities?.contacts?.maxForType('white') ?? 10;
|
||||
|
||||
_load();
|
||||
return ContactsViewState(maxContacts: maxContacts);
|
||||
}
|
||||
|
||||
void toggleIsEditing() {
|
||||
state = state.copyWith(isEditing: !state.isEditing);
|
||||
}
|
||||
|
||||
void clearError() {
|
||||
if (state.error != null) state = state.copyWith(error: null);
|
||||
}
|
||||
|
||||
Future<void> refresh() async {
|
||||
try {
|
||||
final user = await ref.read(userInfoProvider.future);
|
||||
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.isSubmitting) return false;
|
||||
|
||||
if (state.contacts.length <= 1) {
|
||||
state = state.copyWith(error: ContactError.minReached);
|
||||
return false;
|
||||
}
|
||||
|
||||
state = state.copyWith(isSubmitting: true, error: null);
|
||||
|
||||
try {
|
||||
await _contactsRepository.deleteContact(contactId: contact.id);
|
||||
if (!ref.mounted) return false;
|
||||
|
||||
unawaited(
|
||||
_tracking.legacyContactsDeleted(
|
||||
totalCount: (state.contacts.length - 1).clamp(0, 999),
|
||||
),
|
||||
);
|
||||
|
||||
final user = await ref.read(userInfoProvider.future);
|
||||
if (!ref.mounted) return false;
|
||||
|
||||
await _reload(user.id);
|
||||
return true;
|
||||
} catch (_) {
|
||||
if (!ref.mounted) return false;
|
||||
state = state.copyWith(isSubmitting: false, error: ContactError.network);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _load() async {
|
||||
try {
|
||||
final user = await ref.read(userInfoProvider.future);
|
||||
if (!ref.mounted) return;
|
||||
await _reload(user.id);
|
||||
} catch (_) {
|
||||
if (!ref.mounted) return;
|
||||
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,
|
||||
) async {
|
||||
final device = ref.read(selectedDeviceProvider).value;
|
||||
if (device == null) return;
|
||||
|
||||
try {
|
||||
await _contactsRepository.syncContactsToDevice(
|
||||
userId: userId,
|
||||
deviceId: device.id,
|
||||
contacts: contacts,
|
||||
);
|
||||
} catch (_) {
|
||||
// Non-blocking: CRUD already committed server-side.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
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';
|
||||
|
||||
@freezed
|
||||
abstract class ContactsViewState with _$ContactsViewState {
|
||||
const factory ContactsViewState({
|
||||
@Default([]) List<ContactEntity> contacts,
|
||||
@Default(true) bool isLoading,
|
||||
@Default(false) bool isSubmitting,
|
||||
@Default(false) bool isEditing,
|
||||
@Default(10) int maxContacts,
|
||||
ContactError? error,
|
||||
}) = _ContactsViewState;
|
||||
}
|
||||
@@ -1,292 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'contacts_view_state.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
/// @nodoc
|
||||
mixin _$ContactsViewState {
|
||||
|
||||
List<ContactEntity> get contacts; bool get isLoading; bool get isSubmitting; bool get isEditing; int get maxContacts; ContactError? get error;
|
||||
/// Create a copy of ContactsViewState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$ContactsViewStateCopyWith<ContactsViewState> get copyWith => _$ContactsViewStateCopyWithImpl<ContactsViewState>(this as ContactsViewState, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
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.maxContacts, maxContacts) || other.maxContacts == maxContacts)&&(identical(other.error, error) || other.error == error));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(contacts),isLoading,isSubmitting,isEditing,maxContacts,error);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ContactsViewState(contacts: $contacts, isLoading: $isLoading, isSubmitting: $isSubmitting, isEditing: $isEditing, maxContacts: $maxContacts, error: $error)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $ContactsViewStateCopyWith<$Res> {
|
||||
factory $ContactsViewStateCopyWith(ContactsViewState value, $Res Function(ContactsViewState) _then) = _$ContactsViewStateCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
List<ContactEntity> contacts, bool isLoading, bool isSubmitting, bool isEditing, int maxContacts, ContactError? error
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$ContactsViewStateCopyWithImpl<$Res>
|
||||
implements $ContactsViewStateCopyWith<$Res> {
|
||||
_$ContactsViewStateCopyWithImpl(this._self, this._then);
|
||||
|
||||
final ContactsViewState _self;
|
||||
final $Res Function(ContactsViewState) _then;
|
||||
|
||||
/// 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? isLoading = null,Object? isSubmitting = null,Object? isEditing = null,Object? maxContacts = null,Object? error = freezed,}) {
|
||||
return _then(_self.copyWith(
|
||||
contacts: null == contacts ? _self.contacts : contacts // 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,maxContacts: null == maxContacts ? _self.maxContacts : maxContacts // ignore: cast_nullable_to_non_nullable
|
||||
as int,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable
|
||||
as ContactError?,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [ContactsViewState].
|
||||
extension ContactsViewStatePatterns on ContactsViewState {
|
||||
/// 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( _ContactsViewState value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _ContactsViewState() 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( _ContactsViewState value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _ContactsViewState():
|
||||
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( _ContactsViewState value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _ContactsViewState() 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( List<ContactEntity> contacts, bool isLoading, bool isSubmitting, bool isEditing, int maxContacts, ContactError? error)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _ContactsViewState() when $default != null:
|
||||
return $default(_that.contacts,_that.isLoading,_that.isSubmitting,_that.isEditing,_that.maxContacts,_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( List<ContactEntity> contacts, bool isLoading, bool isSubmitting, bool isEditing, int maxContacts, ContactError? error) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _ContactsViewState():
|
||||
return $default(_that.contacts,_that.isLoading,_that.isSubmitting,_that.isEditing,_that.maxContacts,_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( List<ContactEntity> contacts, bool isLoading, bool isSubmitting, bool isEditing, int maxContacts, ContactError? error)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _ContactsViewState() when $default != null:
|
||||
return $default(_that.contacts,_that.isLoading,_that.isSubmitting,_that.isEditing,_that.maxContacts,_that.error);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
|
||||
class _ContactsViewState implements ContactsViewState {
|
||||
const _ContactsViewState({final List<ContactEntity> contacts = const [], this.isLoading = true, this.isSubmitting = false, this.isEditing = false, this.maxContacts = 10, this.error}): _contacts = contacts;
|
||||
|
||||
|
||||
final List<ContactEntity> _contacts;
|
||||
@override@JsonKey() List<ContactEntity> get contacts {
|
||||
if (_contacts is EqualUnmodifiableListView) return _contacts;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_contacts);
|
||||
}
|
||||
|
||||
@override@JsonKey() final bool isLoading;
|
||||
@override@JsonKey() final bool isSubmitting;
|
||||
@override@JsonKey() final bool isEditing;
|
||||
@override@JsonKey() final int maxContacts;
|
||||
@override final ContactError? error;
|
||||
|
||||
/// Create a copy of ContactsViewState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$ContactsViewStateCopyWith<_ContactsViewState> get copyWith => __$ContactsViewStateCopyWithImpl<_ContactsViewState>(this, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
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.maxContacts, maxContacts) || other.maxContacts == maxContacts)&&(identical(other.error, error) || other.error == error));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_contacts),isLoading,isSubmitting,isEditing,maxContacts,error);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ContactsViewState(contacts: $contacts, isLoading: $isLoading, isSubmitting: $isSubmitting, isEditing: $isEditing, maxContacts: $maxContacts, error: $error)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$ContactsViewStateCopyWith<$Res> implements $ContactsViewStateCopyWith<$Res> {
|
||||
factory _$ContactsViewStateCopyWith(_ContactsViewState value, $Res Function(_ContactsViewState) _then) = __$ContactsViewStateCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
List<ContactEntity> contacts, bool isLoading, bool isSubmitting, bool isEditing, int maxContacts, ContactError? error
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$ContactsViewStateCopyWithImpl<$Res>
|
||||
implements _$ContactsViewStateCopyWith<$Res> {
|
||||
__$ContactsViewStateCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _ContactsViewState _self;
|
||||
final $Res Function(_ContactsViewState) _then;
|
||||
|
||||
/// 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? isLoading = null,Object? isSubmitting = null,Object? isEditing = null,Object? maxContacts = null,Object? error = freezed,}) {
|
||||
return _then(_ContactsViewState(
|
||||
contacts: null == contacts ? _self._contacts : contacts // 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,maxContacts: null == maxContacts ? _self.maxContacts : maxContacts // ignore: cast_nullable_to_non_nullable
|
||||
as int,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable
|
||||
as ContactError?,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// dart format on
|
||||
@@ -1,111 +0,0 @@
|
||||
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_dto.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: UpdateContactRequestDto(
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,307 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of '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
|
||||
@@ -1,139 +0,0 @@
|
||||
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_dto.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 '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 >= listState.maxContacts) {
|
||||
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: CreateContactRequestDto(
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,277 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of '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
|
||||
@@ -1,74 +0,0 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.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 'package:legacy_theme/legacy_theme.dart';
|
||||
|
||||
class ConfirmDeleteDialog extends ConsumerWidget {
|
||||
final ContactEntity contact;
|
||||
|
||||
const ConfirmDeleteDialog({super.key, required this.contact});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
|
||||
return Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: SizeUtils.getByScreen(small: 32, big: 30),
|
||||
vertical: SizeUtils.getByScreen(small: 30, big: 28),
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(
|
||||
SizeUtils.getByScreen(small: 12, big: 10),
|
||||
),
|
||||
),
|
||||
width: SizeUtils.getByScreen(small: 360, big: 350),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
context.translate(I18n.deleteContactMessage),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: SizeUtils.getByScreen(small: 19, big: 18),
|
||||
),
|
||||
),
|
||||
SizedBox(height: SizeUtils.getByScreen(small: 28, big: 27)),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: PrimaryButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
text: context.translate(I18n.cancel),
|
||||
color: context.sfColors.legacyPrimary,
|
||||
height: SizeUtils.getByScreen(small: 38, big: 36),
|
||||
radius: SizeUtils.getByScreen(small: 32, big: 34),
|
||||
),
|
||||
),
|
||||
SizedBox(width: SizeUtils.getByScreen(small: 4, big: 16)),
|
||||
Expanded(
|
||||
child: PrimaryButton(
|
||||
onPressed: () async {
|
||||
final vm = ref.read(contactsViewModelProvider.notifier);
|
||||
await vm.deleteContact(contact);
|
||||
if (context.mounted) Navigator.pop(context);
|
||||
},
|
||||
text: context.translate(I18n.delete),
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
height: SizeUtils.getByScreen(small: 38, big: 36),
|
||||
radius: SizeUtils.getByScreen(small: 32, big: 34),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,26 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:legacy_theme/legacy_theme.dart';
|
||||
import 'package:device_management/src/features/contacts/domain/entities/contact_entity.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:legacy_theme/legacy_theme.dart';
|
||||
import 'package:navigation/navigation.dart';
|
||||
import 'package:utils/utils.dart';
|
||||
|
||||
import '../../domain/entities/contact_entity.dart';
|
||||
import 'confirm_delete_dialog.dart';
|
||||
|
||||
class ContactCard extends ConsumerWidget {
|
||||
final ContactEntity contact;
|
||||
final bool isEditing;
|
||||
final VoidCallback onDelete;
|
||||
|
||||
const ContactCard({
|
||||
super.key,
|
||||
required this.contact,
|
||||
required this.isEditing,
|
||||
required this.onDelete,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
|
||||
return Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: SizeUtils.getByScreen(small: 22, big: 21),
|
||||
@@ -73,14 +72,10 @@ class ContactCard extends ConsumerWidget {
|
||||
DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
child: IconButton(
|
||||
onPressed: () => showDialog(
|
||||
context: context,
|
||||
builder: (_) =>
|
||||
Dialog(child: ConfirmDeleteDialog(contact: contact)),
|
||||
),
|
||||
onPressed: onDelete,
|
||||
icon: const Icon(Icons.close, color: Colors.white),
|
||||
),
|
||||
),
|
||||
@@ -88,7 +83,7 @@ class ContactCard extends ConsumerWidget {
|
||||
DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: context.sfColors.legacyPrimary,
|
||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
child: IconButton(
|
||||
onPressed: () {
|
||||
|
||||
@@ -1,43 +1,121 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:device_management/src/features/contacts/presentation/providers/contacts_controller.dart';
|
||||
import 'package:device_management/src/features/contacts/presentation/providers/new_contact_form_provider.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:legacy_theme/legacy_theme.dart';
|
||||
import 'package:sf_localizations/sf_localizations.dart';
|
||||
import 'package:sf_shared/sf_shared.dart';
|
||||
import 'package:utils/utils.dart';
|
||||
|
||||
import '../../domain/entities/contact_error.dart';
|
||||
import '../state/new_contact_view_model.dart';
|
||||
import 'package:legacy_theme/legacy_theme.dart';
|
||||
class NewContactDialog extends ConsumerStatefulWidget {
|
||||
final String userId;
|
||||
final int currentCount;
|
||||
final int maxContacts;
|
||||
|
||||
class NewContactDialog extends ConsumerWidget {
|
||||
const NewContactDialog({super.key});
|
||||
const NewContactDialog({
|
||||
super.key,
|
||||
required this.userId,
|
||||
required this.currentCount,
|
||||
required this.maxContacts,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final vm = ref.read(newContactViewModelProvider.notifier);
|
||||
final isoCode = ref.watch(
|
||||
newContactViewModelProvider.select((s) => s.isoCode),
|
||||
);
|
||||
final isSubmitting = ref.watch(
|
||||
newContactViewModelProvider.select((s) => s.isSubmitting),
|
||||
);
|
||||
ConsumerState<NewContactDialog> createState() => _NewContactDialogState();
|
||||
}
|
||||
|
||||
ref.listen(newContactViewModelProvider.select((s) => s.error), (_, next) {
|
||||
if (next == null) return;
|
||||
if (next == ContactError.contactsPermissionBlocked) {
|
||||
showContactsPermissionDialog(
|
||||
context,
|
||||
onOpenSettings: vm.openSystemSettings,
|
||||
class _NewContactDialogState extends ConsumerState<NewContactDialog> {
|
||||
final _nameController = TextEditingController();
|
||||
final _phoneController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_nameController.addListener(_clearErrorIfAny);
|
||||
_phoneController.addListener(_clearErrorIfAny);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nameController.removeListener(_clearErrorIfAny);
|
||||
_phoneController.removeListener(_clearErrorIfAny);
|
||||
_nameController.dispose();
|
||||
_phoneController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _clearErrorIfAny() {
|
||||
ref.read(newContactFormProvider.notifier).clearError();
|
||||
}
|
||||
|
||||
Future<void> _pickContact() async {
|
||||
final isoCode = ref.read(newContactFormProvider).isoCode;
|
||||
final response = await ref
|
||||
.read(deviceContactPickerProvider)
|
||||
.pick(hintIsoCode: isoCode);
|
||||
|
||||
if (response.outcome ==
|
||||
DeviceContactPickOutcome.permissionPermanentlyDenied) {
|
||||
if (!mounted) return;
|
||||
showContactsPermissionDialog(
|
||||
context,
|
||||
onOpenSettings:
|
||||
ref.read(deviceContactPickerProvider).openSystemSettings,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final data = response.data;
|
||||
if (data == null) return;
|
||||
|
||||
final parsed = data.parsedPhone;
|
||||
if (parsed != null) {
|
||||
ref.read(newContactFormProvider.notifier).setIsoCode(parsed.isoCode);
|
||||
_phoneController.text = parsed.nationalNumber;
|
||||
} else {
|
||||
_phoneController.text = data.rawNumber;
|
||||
}
|
||||
if (_nameController.text.trim().isEmpty) {
|
||||
_nameController.text = data.displayName;
|
||||
}
|
||||
}
|
||||
|
||||
void _submit() {
|
||||
final formNotifier = ref.read(newContactFormProvider.notifier);
|
||||
final name = _nameController.text.trim();
|
||||
if (name.isEmpty) {
|
||||
formNotifier.setLocalError(I18n.errorFirstNameRequired);
|
||||
return;
|
||||
}
|
||||
|
||||
final isoCode = ref.read(newContactFormProvider).isoCode;
|
||||
final parsed = SfPhoneNumber.tryParse(
|
||||
_phoneController.text,
|
||||
defaultIsoCode: isoCode,
|
||||
);
|
||||
if (parsed == null) {
|
||||
formNotifier.setLocalError(I18n.errorMessagePhoneIsInvalid);
|
||||
return;
|
||||
}
|
||||
|
||||
formNotifier.clearError();
|
||||
Navigator.of(context).pop();
|
||||
ref.read(contactsControllerProvider.notifier).createContact(
|
||||
userId: widget.userId,
|
||||
name: name,
|
||||
phoneE164: parsed.e164,
|
||||
currentCount: widget.currentCount,
|
||||
maxCount: widget.maxContacts,
|
||||
);
|
||||
} else {
|
||||
showTopSnackbar(
|
||||
context,
|
||||
message: context.translate(next.i18nKey),
|
||||
type: MessageType.error,
|
||||
);
|
||||
}
|
||||
vm.clearError();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final primaryColor = context.sfColors.legacyPrimary;
|
||||
final formState = ref.watch(newContactFormProvider);
|
||||
final isSubmitting =
|
||||
ref.watch(contactsControllerProvider.select((s) => s.isLoading));
|
||||
|
||||
return Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
@@ -61,17 +139,14 @@ class NewContactDialog extends ConsumerWidget {
|
||||
alignment: Alignment.topRight,
|
||||
child: IconButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
icon: Icon(
|
||||
Icons.close,
|
||||
color: context.sfColors.legacyPrimary,
|
||||
),
|
||||
icon: Icon(Icons.close, color: primaryColor),
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: Text(
|
||||
context.translate(I18n.newContact).toUpperCase(),
|
||||
style: TextStyle(
|
||||
color: context.sfColors.legacyPrimary,
|
||||
color: primaryColor,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
@@ -85,7 +160,7 @@ class NewContactDialog extends ConsumerWidget {
|
||||
SizedBox(height: SizeUtils.getByScreen(small: 14, big: 12)),
|
||||
CustomTextField(
|
||||
hint: context.translate(I18n.name),
|
||||
controller: vm.nameController,
|
||||
controller: _nameController,
|
||||
),
|
||||
SizedBox(height: SizeUtils.getByScreen(small: 14, big: 12)),
|
||||
Row(
|
||||
@@ -93,16 +168,20 @@ class NewContactDialog extends ConsumerWidget {
|
||||
children: [
|
||||
CountryPrefixPicker(
|
||||
headerText: context.translate(I18n.selectYourCountry),
|
||||
initialSelection: isoCode,
|
||||
initialSelection: formState.isoCode,
|
||||
onChanged: (country) {
|
||||
final code = country.code;
|
||||
if (code != null) vm.updateCountry(code);
|
||||
if (code != null) {
|
||||
ref
|
||||
.read(newContactFormProvider.notifier)
|
||||
.setIsoCode(code);
|
||||
}
|
||||
},
|
||||
width: 80,
|
||||
),
|
||||
Expanded(
|
||||
child: CustomTextField(
|
||||
controller: vm.phoneController,
|
||||
controller: _phoneController,
|
||||
hint: context.translate(I18n.phoneNumber),
|
||||
keyboardType: TextInputType.phone,
|
||||
),
|
||||
@@ -110,10 +189,10 @@ class NewContactDialog extends ConsumerWidget {
|
||||
DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: context.sfColors.legacyPrimary,
|
||||
color: primaryColor,
|
||||
),
|
||||
child: IconButton(
|
||||
onPressed: vm.pickContactFromDevice,
|
||||
onPressed: _pickContact,
|
||||
icon: Icon(
|
||||
SFIcons.contactsCircle,
|
||||
color: Colors.white,
|
||||
@@ -123,6 +202,17 @@ class NewContactDialog extends ConsumerWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
if (formState.localError != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: Text(
|
||||
context.translate(formState.localError!),
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: SizeUtils.getByScreen(small: 14, big: 12)),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
@@ -130,14 +220,9 @@ class NewContactDialog extends ConsumerWidget {
|
||||
vertical: SizeUtils.getByScreen(small: 10, big: 8),
|
||||
),
|
||||
child: PrimaryButton(
|
||||
onPressed: isSubmitting
|
||||
? null
|
||||
: () async {
|
||||
final success = await vm.submit();
|
||||
if (success && context.mounted) Navigator.pop(context);
|
||||
},
|
||||
onPressed: isSubmitting ? null : _submit,
|
||||
text: context.translate(I18n.save),
|
||||
color: context.sfColors.legacyPrimary,
|
||||
color: primaryColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -146,4 +231,3 @@ class NewContactDialog extends ConsumerWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
import 'package:device_management/src/core/data/models/create_contact_request_dto.dart';
|
||||
import 'package:device_management/src/core/data/models/update_contact_request_dto.dart';
|
||||
import 'package:device_management/src/core/domain/repositories/contacts_repository.dart';
|
||||
import 'package:device_management/src/core/providers/contacts_repository_provider.dart';
|
||||
import 'package:device_management/src/features/contacts/presentation/providers/contacts_controller.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:sf_infrastructure/sf_infrastructure.dart';
|
||||
import 'package:sf_shared/testing.dart';
|
||||
import 'package:sf_tracking/sf_tracking.dart';
|
||||
|
||||
class MockContactsRepository extends Mock implements ContactsRepository {}
|
||||
|
||||
void main() {
|
||||
setUpAll(() {
|
||||
registerFallbackValue(
|
||||
const CreateContactRequestDto(
|
||||
id: 'x',
|
||||
name: 'n',
|
||||
phone: 'p',
|
||||
userId: 'u',
|
||||
),
|
||||
);
|
||||
registerFallbackValue(
|
||||
const UpdateContactRequestDto(id: 'x', name: 'n', phone: 'p'),
|
||||
);
|
||||
});
|
||||
|
||||
ProviderContainer buildContainer(ContactsRepository repo) {
|
||||
return makeContainer(
|
||||
overrides: [
|
||||
contactsRepositoryProvider.overrideWithValue(repo),
|
||||
sfTrackingProvider.overrideWithValue(
|
||||
SfTrackingRepository(clients: const []),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
group('ContactsController.createContact', () {
|
||||
test('blocks when at max', () async {
|
||||
final repo = MockContactsRepository();
|
||||
final container = buildContainer(repo);
|
||||
addTearDown(container.dispose);
|
||||
|
||||
await container.read(contactsControllerProvider.notifier).createContact(
|
||||
userId: 'u1',
|
||||
name: 'N',
|
||||
phoneE164: '+34600000001',
|
||||
currentCount: 10,
|
||||
maxCount: 10,
|
||||
);
|
||||
|
||||
final state = container.read(contactsControllerProvider);
|
||||
expect(state, isA<AsyncError<void>>());
|
||||
expect(state.error, isA<ContactsMaxReachedException>());
|
||||
verifyNever(() => repo.createContact(request: any(named: 'request')));
|
||||
});
|
||||
|
||||
test('creates contact via repository on success', () async {
|
||||
final repo = MockContactsRepository();
|
||||
when(() => repo.createContact(request: any(named: 'request')))
|
||||
.thenAnswer((_) async {});
|
||||
|
||||
final container = buildContainer(repo);
|
||||
addTearDown(container.dispose);
|
||||
|
||||
await container.read(contactsControllerProvider.notifier).createContact(
|
||||
userId: 'u1',
|
||||
name: 'N',
|
||||
phoneE164: '+34600000001',
|
||||
currentCount: 5,
|
||||
maxCount: 10,
|
||||
);
|
||||
|
||||
expect(
|
||||
container.read(contactsControllerProvider),
|
||||
isA<AsyncData<void>>(),
|
||||
);
|
||||
verify(() => repo.createContact(request: any(named: 'request')))
|
||||
.called(1);
|
||||
});
|
||||
});
|
||||
|
||||
group('ContactsController.deleteContact', () {
|
||||
test('blocks when only one contact', () async {
|
||||
final repo = MockContactsRepository();
|
||||
final container = buildContainer(repo);
|
||||
addTearDown(container.dispose);
|
||||
|
||||
await container.read(contactsControllerProvider.notifier).deleteContact(
|
||||
userId: 'u1',
|
||||
contactId: 'c1',
|
||||
currentCount: 1,
|
||||
);
|
||||
|
||||
final state = container.read(contactsControllerProvider);
|
||||
expect(state, isA<AsyncError<void>>());
|
||||
expect(state.error, isA<ContactsMinReachedException>());
|
||||
verifyNever(() => repo.deleteContact(contactId: any(named: 'contactId')));
|
||||
});
|
||||
|
||||
test('deletes via repository on success', () async {
|
||||
final repo = MockContactsRepository();
|
||||
when(() => repo.deleteContact(contactId: any(named: 'contactId')))
|
||||
.thenAnswer((_) async {});
|
||||
|
||||
final container = buildContainer(repo);
|
||||
addTearDown(container.dispose);
|
||||
|
||||
await container.read(contactsControllerProvider.notifier).deleteContact(
|
||||
userId: 'u1',
|
||||
contactId: 'c1',
|
||||
currentCount: 3,
|
||||
);
|
||||
|
||||
expect(
|
||||
container.read(contactsControllerProvider),
|
||||
isA<AsyncData<void>>(),
|
||||
);
|
||||
verify(() => repo.deleteContact(contactId: 'c1')).called(1);
|
||||
});
|
||||
});
|
||||
|
||||
group('ContactsController.updateContact', () {
|
||||
test('updates via repository on success', () async {
|
||||
final repo = MockContactsRepository();
|
||||
when(() => repo.updateContact(request: any(named: 'request')))
|
||||
.thenAnswer((_) async {});
|
||||
|
||||
final container = buildContainer(repo);
|
||||
addTearDown(container.dispose);
|
||||
|
||||
await container.read(contactsControllerProvider.notifier).updateContact(
|
||||
userId: 'u1',
|
||||
contactId: 'c1',
|
||||
name: 'N',
|
||||
phoneE164: '+34600000001',
|
||||
);
|
||||
|
||||
expect(
|
||||
container.read(contactsControllerProvider),
|
||||
isA<AsyncData<void>>(),
|
||||
);
|
||||
expect(
|
||||
container.read(contactsControllerProvider.notifier).lastAction,
|
||||
ContactsAction.update,
|
||||
);
|
||||
});
|
||||
|
||||
test('exposes AsyncError when repository fails', () async {
|
||||
final repo = MockContactsRepository();
|
||||
when(() => repo.updateContact(request: any(named: 'request')))
|
||||
.thenThrow(const ApiException(message: 'boom', isNetworkError: true));
|
||||
|
||||
final container = buildContainer(repo);
|
||||
addTearDown(container.dispose);
|
||||
|
||||
await container.read(contactsControllerProvider.notifier).updateContact(
|
||||
userId: 'u1',
|
||||
contactId: 'c1',
|
||||
name: 'N',
|
||||
phoneE164: '+34600000001',
|
||||
);
|
||||
|
||||
expect(
|
||||
container.read(contactsControllerProvider),
|
||||
isA<AsyncError<void>>(),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -586,6 +586,9 @@
|
||||
"numberAdded": "Nummer erfolgreich hinzugefügt",
|
||||
"numberRemoved": "Nummer erfolgreich entfernt",
|
||||
"numberUpdated": "Nummer erfolgreich aktualisiert",
|
||||
"contactAdded": "Kontakt erfolgreich hinzugefügt",
|
||||
"contactUpdated": "Kontakt erfolgreich aktualisiert",
|
||||
"contactDeleted": "Kontakt erfolgreich gelöscht",
|
||||
"editAllowedNumber": "Erlaubte Nummer bearbeiten",
|
||||
"addSosContact": "SOS-Kontakt hinzufügen",
|
||||
"noSosContacts": "Keine SOS-Kontakte konfiguriert",
|
||||
|
||||
@@ -762,6 +762,9 @@
|
||||
"numberAdded": "Number added successfully",
|
||||
"numberRemoved": "Number removed successfully",
|
||||
"numberUpdated": "Number updated successfully",
|
||||
"contactAdded": "Contact added successfully",
|
||||
"contactUpdated": "Contact updated successfully",
|
||||
"contactDeleted": "Contact deleted successfully",
|
||||
"editAllowedNumber": "Edit allowed number",
|
||||
"addSosContact": "Add SOS contact",
|
||||
"noSosContacts": "No SOS contacts configured",
|
||||
|
||||
@@ -763,6 +763,9 @@
|
||||
"numberAdded": "Número añadido correctamente",
|
||||
"numberRemoved": "Número eliminado correctamente",
|
||||
"numberUpdated": "Número actualizado correctamente",
|
||||
"contactAdded": "Contacto añadido correctamente",
|
||||
"contactUpdated": "Contacto actualizado correctamente",
|
||||
"contactDeleted": "Contacto eliminado correctamente",
|
||||
"editAllowedNumber": "Editar número permitido",
|
||||
"addSosContact": "Añadir contacto SOS",
|
||||
"noSosContacts": "No hay contactos SOS configurados",
|
||||
|
||||
@@ -586,6 +586,9 @@
|
||||
"numberAdded": "Numéro ajouté avec succès",
|
||||
"numberRemoved": "Numéro supprimé avec succès",
|
||||
"numberUpdated": "Numéro mis à jour avec succès",
|
||||
"contactAdded": "Contact ajouté avec succès",
|
||||
"contactUpdated": "Contact mis à jour avec succès",
|
||||
"contactDeleted": "Contact supprimé avec succès",
|
||||
"editAllowedNumber": "Modifier le numéro autorisé",
|
||||
"addSosContact": "Ajouter un contact SOS",
|
||||
"noSosContacts": "Aucun contact SOS configuré",
|
||||
|
||||
@@ -586,6 +586,9 @@
|
||||
"numberAdded": "Numero aggiunto con successo",
|
||||
"numberRemoved": "Numero rimosso con successo",
|
||||
"numberUpdated": "Numero aggiornato con successo",
|
||||
"contactAdded": "Contatto aggiunto con successo",
|
||||
"contactUpdated": "Contatto aggiornato con successo",
|
||||
"contactDeleted": "Contatto eliminato con successo",
|
||||
"editAllowedNumber": "Modifica numero consentito",
|
||||
"addSosContact": "Aggiungi contatto SOS",
|
||||
"noSosContacts": "Nessun contatto SOS configurato",
|
||||
|
||||
@@ -586,6 +586,9 @@
|
||||
"numberAdded": "Número adicionado com sucesso",
|
||||
"numberRemoved": "Número removido com sucesso",
|
||||
"numberUpdated": "Número atualizado com sucesso",
|
||||
"contactAdded": "Contacto adicionado com sucesso",
|
||||
"contactUpdated": "Contacto atualizado com sucesso",
|
||||
"contactDeleted": "Contacto eliminado com sucesso",
|
||||
"editAllowedNumber": "Editar número permitido",
|
||||
"addSosContact": "Adicionar contacto SOS",
|
||||
"noSosContacts": "Nenhum contacto SOS configurado",
|
||||
|
||||
@@ -200,8 +200,11 @@ class I18n {
|
||||
static const String close = 'close';
|
||||
static const String confirm = 'confirm';
|
||||
static const String connect = 'connect';
|
||||
static const String contactAdded = 'contactAdded';
|
||||
static const String contactDeleted = 'contactDeleted';
|
||||
static const String contactName = 'contactName';
|
||||
static const String contactTitle = 'contactTitle';
|
||||
static const String contactUpdated = 'contactUpdated';
|
||||
static const String contactsAgendaTitle = 'contactsAgendaTitle';
|
||||
static const String contactsEmpty = 'contactsEmpty';
|
||||
static const String contactsEmptyHint = 'contactsEmptyHint';
|
||||
|
||||
Reference in New Issue
Block a user