fix(contacts): validate duplicate phone numbers before adding

This commit is contained in:
2026-04-26 05:12:23 +02:00
parent c7fefe2a8b
commit 51901cc639
5 changed files with 37 additions and 8 deletions

View File

@@ -112,7 +112,7 @@ class ContactsScreen extends ConsumerWidget {
return; return;
} }
if (!context.mounted) return; if (!context.mounted) return;
showDialog<void>( showLegacyDialog<void>(
context: context, context: context,
builder: (_) => Dialog( builder: (_) => Dialog(
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
@@ -145,7 +145,7 @@ class ContactsScreen extends ConsumerWidget {
required String contactId, required String contactId,
required int currentCount, required int currentCount,
}) async { }) async {
final confirmed = await showDialog<bool>( final confirmed = await showLegacyDialog<bool>(
context: context, context: context,
builder: (dialogContext) => AlertDialog( builder: (dialogContext) => AlertDialog(
title: Text(context.translate(I18n.deleteContactMessage)), title: Text(context.translate(I18n.deleteContactMessage)),

View File

@@ -1,5 +1,6 @@
import 'package:design_system/design_system.dart'; 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/contacts_controller.dart';
import 'package:device_management/src/features/contacts/presentation/providers/contacts_provider.dart';
import 'package:device_management/src/features/contacts/presentation/providers/new_contact_form_provider.dart'; import 'package:device_management/src/features/contacts/presentation/providers/new_contact_form_provider.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
@@ -99,6 +100,16 @@ class _NewContactDialogState extends ConsumerState<NewContactDialog> {
return; return;
} }
final existingContacts =
ref.read(contactsProvider(widget.userId)).value ?? [];
final isDuplicate = existingContacts.any(
(c) => c.phone?.e164 == parsed.e164 || c.rawPhone == parsed.e164,
);
if (isDuplicate) {
formNotifier.setLocalError(I18n.errorContactDuplicate);
return;
}
formNotifier.clearError(); formNotifier.clearError();
Navigator.of(context).pop(); Navigator.of(context).pop();
ref.read(contactsControllerProvider.notifier).createContact( ref.read(contactsControllerProvider.notifier).createContact(

View File

@@ -162,6 +162,16 @@ class _ContactFormSheetWrapperState
return; return;
} }
if (!_isEdit) {
final isDuplicate = widget.currentContacts.any(
(c) => c.phone == parsed.e164,
);
if (isDuplicate) {
formNotifier.setPhoneError(I18n.errorContactDuplicate);
return;
}
}
final contact = ContactListContactEntity( final contact = ContactListContactEntity(
name: _nameController.text.trim(), name: _nameController.text.trim(),
phone: parsed.e164, phone: parsed.e164,

View File

@@ -1,3 +1,4 @@
import 'package:legacy_ui/legacy_ui.dart';
import 'package:design_system/design_system.dart'; import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
@@ -21,8 +22,8 @@ class SosContactsScreen extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final device = ref.watch(selectedDeviceProvider).value; final device = ref.watch(selectedDeviceProvider).value;
final primaryColor = context.sfColors.legacyPrimary; final primaryColor = context.sfColors.legacyPrimary;
final maxContacts = device?.capabilities?.contacts final maxContacts =
?.maxForType('emergency', fallback: 3) ?? device?.capabilities?.contacts?.maxForType('emergency', fallback: 3) ??
3; 3;
ref.listen(sosContactsControllerProvider, (prev, next) async { ref.listen(sosContactsControllerProvider, (prev, next) async {
@@ -128,7 +129,7 @@ class SosContactsScreen extends ConsumerWidget {
), ),
), ),
title: Text( title: Text(
context.translate(I18n.sosContacts).toUpperCase(), context.translate(I18n.sosContacts),
style: TextStyle( style: TextStyle(
fontSize: SizeUtils.getByScreen(small: 20, big: 19), fontSize: SizeUtils.getByScreen(small: 20, big: 19),
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
@@ -163,7 +164,7 @@ class SosContactsScreen extends ConsumerWidget {
Future<bool?> _confirmDelete(BuildContext context, String name) { Future<bool?> _confirmDelete(BuildContext context, String name) {
final theme = Theme.of(context); final theme = Theme.of(context);
return showDialog<bool>( return showLegacyDialog<bool>(
context: context, context: context,
builder: (dialogContext) => AlertDialog( builder: (dialogContext) => AlertDialog(
icon: Icon( icon: Icon(
@@ -268,8 +269,7 @@ class _ContactList extends StatelessWidget {
context.translate(I18n.sosDescription), context.translate(I18n.sosDescription),
style: TextStyle( style: TextStyle(
fontSize: SizeUtils.getByScreen(small: 14, big: 15), fontSize: SizeUtils.getByScreen(small: 14, big: 15),
color: color: Theme.of(context).colorScheme.onSurface.withAlpha(178),
Theme.of(context).colorScheme.onSurface.withAlpha(178),
), ),
), ),
), ),

View File

@@ -120,6 +120,14 @@ class _AddSosContactSheetState extends ConsumerState<_AddSosContactSheet> {
return; return;
} }
final isDuplicate = widget.currentContacts.any(
(c) => c.phone == parsed.e164,
);
if (isDuplicate) {
formNotifier.setPhoneError(I18n.errorContactDuplicate);
return;
}
await ref.read(sosContactsControllerProvider.notifier).addContact( await ref.read(sosContactsControllerProvider.notifier).addContact(
deviceId: widget.deviceId, deviceId: widget.deviceId,
userId: widget.userId, userId: widget.userId,