From ad0b8d209a2c7db5b44b3b4b6dc50ca2a5a9c3f1 Mon Sep 17 00:00:00 2001 From: JulianAlcala Date: Wed, 22 Apr 2026 02:19:33 +0200 Subject: [PATCH] refactor(settings): migrate block_phone to Riverpod CRUD pattern --- .../presentation/block_phone_screen.dart | 308 ++++++++++-------- .../block_phone_contact_form_provider.dart | 72 ++++ .../block_phone_contact_form_provider.g.dart | 116 +++++++ .../block_phone_contacts_provider.dart | 13 + .../block_phone_contacts_provider.g.dart | 92 ++++++ .../providers/block_phone_controller.dart | 92 ++++++ .../providers/block_phone_controller.g.dart | 56 ++++ .../state/block_phone_view_model.dart | 153 --------- .../state/block_phone_view_state.dart | 15 - .../state/block_phone_view_state.freezed.dart | 289 ---------------- .../new_block_phone_contact_view_model.dart | 121 ------- .../new_block_phone_contact_view_state.dart | 15 - ...lock_phone_contact_view_state.freezed.dart | 283 ---------------- .../widgets/add_contact_sheet.dart | 275 ++++++++++------ .../providers/sos_contacts_controller.dart | 2 +- .../block_phone_controller_test.dart | 252 ++++++++++++++ packages/sf_localizations/assets/l10n/de.json | 1 + packages/sf_localizations/assets/l10n/en.json | 1 + packages/sf_localizations/assets/l10n/es.json | 1 + packages/sf_localizations/assets/l10n/fr.json | 1 + packages/sf_localizations/assets/l10n/it.json | 1 + packages/sf_localizations/assets/l10n/pt.json | 1 + .../lib/src/generated/i18n.dart | 1 + 23 files changed, 1055 insertions(+), 1106 deletions(-) create mode 100644 modules/legacy/modules/settings/lib/src/features/block_phone/presentation/providers/block_phone_contact_form_provider.dart create mode 100644 modules/legacy/modules/settings/lib/src/features/block_phone/presentation/providers/block_phone_contact_form_provider.g.dart create mode 100644 modules/legacy/modules/settings/lib/src/features/block_phone/presentation/providers/block_phone_contacts_provider.dart create mode 100644 modules/legacy/modules/settings/lib/src/features/block_phone/presentation/providers/block_phone_contacts_provider.g.dart create mode 100644 modules/legacy/modules/settings/lib/src/features/block_phone/presentation/providers/block_phone_controller.dart create mode 100644 modules/legacy/modules/settings/lib/src/features/block_phone/presentation/providers/block_phone_controller.g.dart delete mode 100644 modules/legacy/modules/settings/lib/src/features/block_phone/presentation/state/block_phone_view_model.dart delete mode 100644 modules/legacy/modules/settings/lib/src/features/block_phone/presentation/state/block_phone_view_state.dart delete mode 100644 modules/legacy/modules/settings/lib/src/features/block_phone/presentation/state/block_phone_view_state.freezed.dart delete mode 100644 modules/legacy/modules/settings/lib/src/features/block_phone/presentation/state/new_block_phone_contact_view_model.dart delete mode 100644 modules/legacy/modules/settings/lib/src/features/block_phone/presentation/state/new_block_phone_contact_view_state.dart delete mode 100644 modules/legacy/modules/settings/lib/src/features/block_phone/presentation/state/new_block_phone_contact_view_state.freezed.dart create mode 100644 modules/legacy/modules/settings/test/features/block_phone/block_phone_controller_test.dart diff --git a/modules/legacy/modules/settings/lib/src/features/block_phone/presentation/block_phone_screen.dart b/modules/legacy/modules/settings/lib/src/features/block_phone/presentation/block_phone_screen.dart index ee34f3e5..1f8bc332 100644 --- a/modules/legacy/modules/settings/lib/src/features/block_phone/presentation/block_phone_screen.dart +++ b/modules/legacy/modules/settings/lib/src/features/block_phone/presentation/block_phone_screen.dart @@ -1,16 +1,17 @@ -import 'package:design_system/design_system.dart'; -import 'package:legacy_theme/legacy_theme.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:navigation/navigation.dart'; +import 'package:settings/src/core/domain/entities/contact_list_contact_entity.dart'; +import 'package:settings/src/core/presentation/widgets/contact_list_contact_card.dart'; +import 'package:settings/src/features/block_phone/presentation/providers/block_phone_contacts_provider.dart'; +import 'package:settings/src/features/block_phone/presentation/providers/block_phone_controller.dart'; +import 'package:settings/src/features/block_phone/presentation/widgets/add_contact_sheet.dart'; import 'package:sf_localizations/sf_localizations.dart'; +import 'package:sf_shared/sf_shared.dart'; import 'package:utils/utils.dart'; -import 'state/block_phone_view_model.dart'; -import '../../../core/presentation/widgets/contact_list_contact_card.dart'; -import 'widgets/add_contact_sheet.dart'; - class BlockPhoneScreen extends ConsumerWidget { final NavigationContract navigationContract; @@ -18,62 +19,133 @@ class BlockPhoneScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final state = ref.watch(blockPhoneViewModelProvider); + final device = ref.watch(selectedDeviceProvider).value; final primaryColor = context.sfColors.legacyPrimary; - ref.listen(blockPhoneViewModelProvider.select((s) => s.errorMessage), ( - _, - errorMessage, - ) { - if (errorMessage.isNotEmpty) { - showTopSnackbar( - context, - message: errorMessage, - type: MessageType.error, - ); + ref.listen(blockPhoneControllerProvider, (prev, next) async { + if (prev == null || !prev.isLoading || next.isLoading) return; + if (next.hasError) { + final error = next.error; + if (error is BlockPhoneMinimumOneContactException) { + await showErrorDialog(context, I18n.whitelistMinimumOneContact); + } else { + await next.showErrorOn(context); + } } }); - ref.listen(blockPhoneViewModelProvider.select((s) => s.successMessage), ( - _, - successMessage, - ) { - if (successMessage.isNotEmpty) { - showTopSnackbar( - context, - message: context.translate(successMessage), - type: MessageType.success, - ); - ref.read(blockPhoneViewModelProvider.notifier).clearSuccess(); - } - }); + if (device == null) { + return Scaffold( + appBar: _appBar(context, navigationContract, primaryColor), + body: const SizedBox.shrink(), + ); + } - return Scaffold( + final contactsAsync = ref.watch(blockPhoneContactsProvider(device.id)); + + return contactsAsync.when( + loading: () => Scaffold( + appBar: _appBar(context, navigationContract, primaryColor), + body: const Center(child: CircularProgressIndicator()), + ), + error: (_, __) => Scaffold( + appBar: _appBar(context, navigationContract, primaryColor), + body: Center(child: Text(context.translate(I18n.errorGeneric))), + ), + data: (contacts) { + return Scaffold( + backgroundColor: Theme.of(context).colorScheme.surface, + appBar: _appBar( + context, + navigationContract, + primaryColor, + onAdd: () async { + if (!await guardDeviceCommand(context, ref)) return; + if (!context.mounted) return; + showAddContactSheet( + context, + deviceId: device.id, + userId: device.userId ?? '', + currentContacts: contacts, + ); + }, + ), + body: SafeArea( + top: false, + child: contacts.isEmpty + ? _EmptyState(primaryColor: primaryColor) + : _ContactList( + contacts: contacts, + primaryColor: primaryColor, + onEdit: (index, contact) async { + if (!await guardDeviceCommand(context, ref)) return; + if (!context.mounted) return; + showEditContactSheet( + context, + deviceId: device.id, + userId: device.userId ?? '', + currentContacts: contacts, + index: index, + contact: contact, + ); + }, + onDelete: (index, name) async { + if (!await guardDeviceCommand(context, ref)) return; + if (!context.mounted) return; + final confirmed = await _confirmDelete(context, name); + if (confirmed != true) return; + if (!context.mounted) return; + await ref + .read(blockPhoneControllerProvider.notifier) + .removeContact( + deviceId: device.id, + userId: device.userId ?? '', + current: contacts, + index: index, + ); + if (!context.mounted) return; + final state = ref.read(blockPhoneControllerProvider); + if (state.hasError) return; + await showSuccessDialog(context, I18n.numberRemoved); + }, + ), + ), + ); + }, + ); + } + + PreferredSizeWidget _appBar( + BuildContext context, + NavigationContract nav, + Color primaryColor, { + VoidCallback? onAdd, + }) { + return AppBar( backgroundColor: Theme.of(context).colorScheme.surface, - appBar: AppBar( - backgroundColor: Theme.of(context).colorScheme.surface, - surfaceTintColor: Colors.transparent, - elevation: 0, - centerTitle: true, - automaticallyImplyLeading: false, - leading: IconButton( - onPressed: () => navigationContract.goBack(), - icon: Icon( - Icons.adaptive.arrow_back, - color: primaryColor, - size: SizeUtils.getByScreen(small: 32, big: 28), - ), + surfaceTintColor: Colors.transparent, + elevation: 0, + centerTitle: true, + automaticallyImplyLeading: false, + leading: IconButton( + onPressed: () => nav.goBack(), + icon: Icon( + Icons.adaptive.arrow_back, + color: primaryColor, + size: SizeUtils.getByScreen(small: 32, big: 28), ), - title: Text( - context.translate(I18n.blockPhone).toUpperCase(), - style: TextStyle( - fontSize: SizeUtils.getByScreen(small: 20, big: 19), - fontWeight: FontWeight.w500, - letterSpacing: 0, - color: primaryColor, - ), + ), + title: Text( + context.translate(I18n.blockPhone).toUpperCase(), + style: TextStyle( + fontSize: SizeUtils.getByScreen(small: 20, big: 19), + fontWeight: FontWeight.w500, + letterSpacing: 0, + color: primaryColor, ), - actions: [ + ), + actions: [ + if (onAdd != null) Padding( padding: EdgeInsets.only( right: SizeUtils.getByScreen(small: 16, big: 14), @@ -84,11 +156,7 @@ class BlockPhoneScreen extends ConsumerWidget { shape: BoxShape.circle, ), child: IconButton( - onPressed: () async { - if (!await guardDeviceCommand(context, ref)) return; - if (!context.mounted) return; - showAddContactSheet(context); - }, + onPressed: onAdd, icon: Icon( Icons.add, color: Colors.white, @@ -97,16 +165,41 @@ class BlockPhoneScreen extends ConsumerWidget { ), ), ), + ], + ); + } + + Future _confirmDelete(BuildContext context, String name) { + final theme = Theme.of(context); + return showDialog( + context: context, + builder: (dialogContext) => AlertDialog( + icon: Icon( + Icons.warning_amber_rounded, + color: theme.colorScheme.error, + size: 40, + ), + title: Text(context.translate(I18n.removeAllowedNumber)), + content: Text( + context.translate( + I18n.removeAllowedNumberConfirm, + args: {'name': name}, + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(dialogContext).pop(false), + child: Text(context.translate(I18n.cancel)), + ), + FilledButton( + onPressed: () => Navigator.of(dialogContext).pop(true), + style: FilledButton.styleFrom( + backgroundColor: theme.colorScheme.error, + ), + child: Text(context.translate(I18n.delete)), + ), ], ), - body: SafeArea( - top: false, - child: state.isLoading - ? const Center(child: CircularProgressIndicator()) - : state.contacts.isEmpty - ? _EmptyState(primaryColor: primaryColor) - : _ContactList(), - ), ); } } @@ -156,21 +249,26 @@ class _EmptyState extends StatelessWidget { } } -class _ContactList extends ConsumerWidget { - const _ContactList(); +class _ContactList extends StatelessWidget { + final List contacts; + final Color primaryColor; + final void Function(int index, ContactListContactEntity contact) onEdit; + final void Function(int index, String name) onDelete; + + const _ContactList({ + required this.contacts, + required this.primaryColor, + required this.onEdit, + required this.onDelete, + }); @override - Widget build(BuildContext context, WidgetRef ref) { - final contacts = ref.watch( - blockPhoneViewModelProvider.select((s) => s.contacts), - ); - final primaryColor = context.sfColors.legacyPrimary; - + Widget build(BuildContext context) { return SingleChildScrollView( child: Padding( padding: SizeUtils.getByScreen( - small: EdgeInsets.symmetric(horizontal: 22, vertical: 10), - big: EdgeInsets.symmetric(horizontal: 21, vertical: 8), + small: const EdgeInsets.symmetric(horizontal: 22, vertical: 10), + big: const EdgeInsets.symmetric(horizontal: 21, vertical: 8), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -183,8 +281,8 @@ class _ContactList extends ConsumerWidget { context.translate(I18n.whitelistDescription), style: TextStyle( fontSize: SizeUtils.getByScreen(small: 14, big: 15), - color: Theme.of(context).colorScheme.onSurface - .withAlpha(178), + color: + Theme.of(context).colorScheme.onSurface.withAlpha(178), ), ), ), @@ -192,13 +290,8 @@ class _ContactList extends ConsumerWidget { final contact = contacts[index]; return ContactListContactCard( contact: contact, - onEdit: () => showEditContactSheet( - context, - index: index, - contact: contact, - ), - onDelete: () => - _confirmDelete(context, ref, index, contact.name), + onEdit: () => onEdit(index, contact), + onDelete: () => onDelete(index, contact.name), ); }), SizedBox(height: SizeUtils.getByScreen(small: 12, big: 10)), @@ -218,49 +311,4 @@ class _ContactList extends ConsumerWidget { ), ); } - - Future _confirmDelete( - BuildContext context, - WidgetRef ref, - int index, - String name, - ) async { - if (!await guardDeviceCommand(context, ref)) return; - if (!context.mounted) return; - final primaryColor = context.sfColors.legacyPrimary; - - showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text(context.translate(I18n.removeAllowedNumber)), - content: Text( - context.translate( - I18n.removeAllowedNumberConfirm, - args: {'name': name}, - ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text( - context.translate(I18n.cancel), - style: TextStyle(color: primaryColor), - ), - ), - TextButton( - onPressed: () { - ref - .read(blockPhoneViewModelProvider.notifier) - .removeContact(index); - Navigator.pop(context); - }, - child: Text( - context.translate(I18n.delete), - style: TextStyle(color: Theme.of(context).colorScheme.error), - ), - ), - ], - ), - ); - } } diff --git a/modules/legacy/modules/settings/lib/src/features/block_phone/presentation/providers/block_phone_contact_form_provider.dart b/modules/legacy/modules/settings/lib/src/features/block_phone/presentation/providers/block_phone_contact_form_provider.dart new file mode 100644 index 00000000..fe447409 --- /dev/null +++ b/modules/legacy/modules/settings/lib/src/features/block_phone/presentation/providers/block_phone_contact_form_provider.dart @@ -0,0 +1,72 @@ +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:settings/src/core/domain/entities/contact_list_contact_entity.dart'; +import 'package:sf_shared/sf_shared.dart'; + +part 'block_phone_contact_form_provider.g.dart'; + +class BlockPhoneContactFormState { + const BlockPhoneContactFormState({ + required this.isoCode, + this.canSave = false, + this.permissionBlocked = false, + this.phoneError, + }); + + final String isoCode; + final bool canSave; + final bool permissionBlocked; + final String? phoneError; + + BlockPhoneContactFormState copyWith({ + String? isoCode, + bool? canSave, + bool? permissionBlocked, + String? phoneError, + bool clearPhoneError = false, + }) { + return BlockPhoneContactFormState( + isoCode: isoCode ?? this.isoCode, + canSave: canSave ?? this.canSave, + permissionBlocked: permissionBlocked ?? this.permissionBlocked, + phoneError: clearPhoneError ? null : (phoneError ?? this.phoneError), + ); + } +} + +@riverpod +class BlockPhoneContactForm extends _$BlockPhoneContactForm { + @override + BlockPhoneContactFormState build(ContactListContactEntity? initial) { + if (initial == null) { + return const BlockPhoneContactFormState(isoCode: 'ES'); + } + final parsed = SfPhoneNumber.tryParse(initial.phone); + return BlockPhoneContactFormState( + isoCode: parsed?.isoCode ?? SfPhoneNumber.defaultIsoCode, + canSave: true, + ); + } + + void setCanSave(bool value) { + if (value == state.canSave) return; + state = state.copyWith(canSave: value); + } + + void setIsoCode(String code) { + if (code == state.isoCode) return; + state = state.copyWith(isoCode: code, clearPhoneError: true); + } + + void setPhoneError(String? error) { + state = state.copyWith(phoneError: error); + } + + void clearPhoneError() { + if (state.phoneError == null) return; + state = state.copyWith(clearPhoneError: true); + } + + void setPermissionBlocked(bool value) { + state = state.copyWith(permissionBlocked: value); + } +} diff --git a/modules/legacy/modules/settings/lib/src/features/block_phone/presentation/providers/block_phone_contact_form_provider.g.dart b/modules/legacy/modules/settings/lib/src/features/block_phone/presentation/providers/block_phone_contact_form_provider.g.dart new file mode 100644 index 00000000..7505c307 --- /dev/null +++ b/modules/legacy/modules/settings/lib/src/features/block_phone/presentation/providers/block_phone_contact_form_provider.g.dart @@ -0,0 +1,116 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'block_phone_contact_form_provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(BlockPhoneContactForm) +const blockPhoneContactFormProvider = BlockPhoneContactFormFamily._(); + +final class BlockPhoneContactFormProvider + extends + $NotifierProvider { + const BlockPhoneContactFormProvider._({ + required BlockPhoneContactFormFamily super.from, + required ContactListContactEntity? super.argument, + }) : super( + retry: null, + name: r'blockPhoneContactFormProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$blockPhoneContactFormHash(); + + @override + String toString() { + return r'blockPhoneContactFormProvider' + '' + '($argument)'; + } + + @$internal + @override + BlockPhoneContactForm create() => BlockPhoneContactForm(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(BlockPhoneContactFormState value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } + + @override + bool operator ==(Object other) { + return other is BlockPhoneContactFormProvider && other.argument == argument; + } + + @override + int get hashCode { + return argument.hashCode; + } +} + +String _$blockPhoneContactFormHash() => + r'6282fe5b19436963fa1e8febe8d05daf68ecee40'; + +final class BlockPhoneContactFormFamily extends $Family + with + $ClassFamilyOverride< + BlockPhoneContactForm, + BlockPhoneContactFormState, + BlockPhoneContactFormState, + BlockPhoneContactFormState, + ContactListContactEntity? + > { + const BlockPhoneContactFormFamily._() + : super( + retry: null, + name: r'blockPhoneContactFormProvider', + dependencies: null, + $allTransitiveDependencies: null, + isAutoDispose: true, + ); + + BlockPhoneContactFormProvider call(ContactListContactEntity? initial) => + BlockPhoneContactFormProvider._(argument: initial, from: this); + + @override + String toString() => r'blockPhoneContactFormProvider'; +} + +abstract class _$BlockPhoneContactForm + extends $Notifier { + late final _$args = ref.$arg as ContactListContactEntity?; + ContactListContactEntity? get initial => _$args; + + BlockPhoneContactFormState build(ContactListContactEntity? initial); + @$mustCallSuper + @override + void runBuild() { + final created = build(_$args); + final ref = + this.ref + as $Ref; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier< + BlockPhoneContactFormState, + BlockPhoneContactFormState + >, + BlockPhoneContactFormState, + Object?, + Object? + >; + element.handleValue(ref, created); + } +} diff --git a/modules/legacy/modules/settings/lib/src/features/block_phone/presentation/providers/block_phone_contacts_provider.dart b/modules/legacy/modules/settings/lib/src/features/block_phone/presentation/providers/block_phone_contacts_provider.dart new file mode 100644 index 00000000..ca39bcca --- /dev/null +++ b/modules/legacy/modules/settings/lib/src/features/block_phone/presentation/providers/block_phone_contacts_provider.dart @@ -0,0 +1,13 @@ +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:settings/src/core/domain/entities/contact_list_contact_entity.dart'; +import 'package:settings/src/core/providers/block_phone_repository_provider.dart'; + +part 'block_phone_contacts_provider.g.dart'; + +@riverpod +Future> blockPhoneContacts( + Ref ref, + String deviceId, +) async { + return ref.watch(blockPhoneRepositoryProvider).getWhitelist(deviceId: deviceId); +} diff --git a/modules/legacy/modules/settings/lib/src/features/block_phone/presentation/providers/block_phone_contacts_provider.g.dart b/modules/legacy/modules/settings/lib/src/features/block_phone/presentation/providers/block_phone_contacts_provider.g.dart new file mode 100644 index 00000000..0e74f7c0 --- /dev/null +++ b/modules/legacy/modules/settings/lib/src/features/block_phone/presentation/providers/block_phone_contacts_provider.g.dart @@ -0,0 +1,92 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'block_phone_contacts_provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(blockPhoneContacts) +const blockPhoneContactsProvider = BlockPhoneContactsFamily._(); + +final class BlockPhoneContactsProvider + extends + $FunctionalProvider< + AsyncValue>, + List, + FutureOr> + > + with + $FutureModifier>, + $FutureProvider> { + const BlockPhoneContactsProvider._({ + required BlockPhoneContactsFamily super.from, + required String super.argument, + }) : super( + retry: null, + name: r'blockPhoneContactsProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$blockPhoneContactsHash(); + + @override + String toString() { + return r'blockPhoneContactsProvider' + '' + '($argument)'; + } + + @$internal + @override + $FutureProviderElement> $createElement( + $ProviderPointer pointer, + ) => $FutureProviderElement(pointer); + + @override + FutureOr> create(Ref ref) { + final argument = this.argument as String; + return blockPhoneContacts(ref, argument); + } + + @override + bool operator ==(Object other) { + return other is BlockPhoneContactsProvider && other.argument == argument; + } + + @override + int get hashCode { + return argument.hashCode; + } +} + +String _$blockPhoneContactsHash() => + r'936920ff6848e41a463c91821af2618f40f0f68e'; + +final class BlockPhoneContactsFamily extends $Family + with + $FunctionalFamilyOverride< + FutureOr>, + String + > { + const BlockPhoneContactsFamily._() + : super( + retry: null, + name: r'blockPhoneContactsProvider', + dependencies: null, + $allTransitiveDependencies: null, + isAutoDispose: true, + ); + + BlockPhoneContactsProvider call(String deviceId) => + BlockPhoneContactsProvider._(argument: deviceId, from: this); + + @override + String toString() => r'blockPhoneContactsProvider'; +} diff --git a/modules/legacy/modules/settings/lib/src/features/block_phone/presentation/providers/block_phone_controller.dart b/modules/legacy/modules/settings/lib/src/features/block_phone/presentation/providers/block_phone_controller.dart new file mode 100644 index 00000000..035b40d4 --- /dev/null +++ b/modules/legacy/modules/settings/lib/src/features/block_phone/presentation/providers/block_phone_controller.dart @@ -0,0 +1,92 @@ +import 'dart:async'; + +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:settings/src/core/domain/entities/contact_list_contact_entity.dart'; +import 'package:settings/src/core/providers/block_phone_repository_provider.dart'; +import 'package:settings/src/features/block_phone/presentation/providers/block_phone_contacts_provider.dart'; +import 'package:sf_tracking/sf_tracking.dart'; + +part 'block_phone_controller.g.dart'; + +class BlockPhoneMinimumOneContactException implements Exception { + const BlockPhoneMinimumOneContactException(); +} + +@riverpod +class BlockPhoneController extends _$BlockPhoneController { + @override + FutureOr build() {} + + Future addContact({ + required String deviceId, + required String userId, + required List current, + required ContactListContactEntity contact, + }) async { + state = const AsyncLoading(); + state = await AsyncValue.guard(() async { + final updated = [...current, contact]; + await ref.read(blockPhoneRepositoryProvider).upsertWhitelist( + userId: userId, + deviceId: deviceId, + contacts: updated, + ); + ref.invalidate(blockPhoneContactsProvider(deviceId)); + unawaited( + ref + .read(sfTrackingProvider) + .legacySettingsBlockPhoneContactAdded(totalCount: updated.length), + ); + }); + } + + Future updateContact({ + required String deviceId, + required String userId, + required List current, + required int index, + required ContactListContactEntity contact, + }) async { + state = const AsyncLoading(); + state = await AsyncValue.guard(() async { + final updated = [...current]; + updated[index] = contact; + await ref.read(blockPhoneRepositoryProvider).upsertWhitelist( + userId: userId, + deviceId: deviceId, + contacts: updated, + ); + ref.invalidate(blockPhoneContactsProvider(deviceId)); + }); + } + + Future removeContact({ + required String deviceId, + required String userId, + required List current, + required int index, + }) async { + state = const AsyncLoading(); + if (current.length <= 1) { + state = AsyncError( + const BlockPhoneMinimumOneContactException(), + StackTrace.current, + ); + return; + } + state = await AsyncValue.guard(() async { + final updated = [...current]..removeAt(index); + await ref.read(blockPhoneRepositoryProvider).upsertWhitelist( + userId: userId, + deviceId: deviceId, + contacts: updated, + ); + ref.invalidate(blockPhoneContactsProvider(deviceId)); + unawaited( + ref + .read(sfTrackingProvider) + .legacySettingsBlockPhoneContactRemoved(totalCount: updated.length), + ); + }); + } +} diff --git a/modules/legacy/modules/settings/lib/src/features/block_phone/presentation/providers/block_phone_controller.g.dart b/modules/legacy/modules/settings/lib/src/features/block_phone/presentation/providers/block_phone_controller.g.dart new file mode 100644 index 00000000..549de320 --- /dev/null +++ b/modules/legacy/modules/settings/lib/src/features/block_phone/presentation/providers/block_phone_controller.g.dart @@ -0,0 +1,56 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'block_phone_controller.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(BlockPhoneController) +const blockPhoneControllerProvider = BlockPhoneControllerProvider._(); + +final class BlockPhoneControllerProvider + extends $AsyncNotifierProvider { + const BlockPhoneControllerProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'blockPhoneControllerProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$blockPhoneControllerHash(); + + @$internal + @override + BlockPhoneController create() => BlockPhoneController(); +} + +String _$blockPhoneControllerHash() => + r'9992feb7188fa92f3b6fdb569bef89e750a81b0e'; + +abstract class _$BlockPhoneController extends $AsyncNotifier { + FutureOr build(); + @$mustCallSuper + @override + void runBuild() { + build(); + final ref = this.ref as $Ref, void>; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, void>, + AsyncValue, + Object?, + Object? + >; + element.handleValue(ref, null); + } +} diff --git a/modules/legacy/modules/settings/lib/src/features/block_phone/presentation/state/block_phone_view_model.dart b/modules/legacy/modules/settings/lib/src/features/block_phone/presentation/state/block_phone_view_model.dart deleted file mode 100644 index 3b42e79a..00000000 --- a/modules/legacy/modules/settings/lib/src/features/block_phone/presentation/state/block_phone_view_model.dart +++ /dev/null @@ -1,153 +0,0 @@ -import 'dart:async'; - -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:sf_infrastructure/sf_infrastructure.dart'; -import 'package:sf_shared/sf_shared.dart'; -import 'package:settings/src/core/domain/entities/contact_list_contact_entity.dart'; -import 'package:settings/src/core/domain/repositories/block_phone_repository.dart'; -import 'package:settings/src/core/providers/block_phone_repository_provider.dart'; -import 'package:settings/src/features/block_phone/presentation/state/block_phone_view_state.dart'; -import 'package:sf_localizations/sf_localizations.dart'; -import 'package:sf_tracking/sf_tracking.dart'; - -final blockPhoneViewModelProvider = - NotifierProvider.autoDispose( - BlockPhoneViewModel.new, - ); - -class BlockPhoneViewModel extends Notifier { - late final BlockPhoneRepository _repository; - late final SfTrackingRepository _tracking; - - @override - BlockPhoneViewState build() { - _repository = ref.read(blockPhoneRepositoryProvider); - _tracking = ref.read(sfTrackingProvider); - Future.microtask(_load); - return const BlockPhoneViewState(); - } - - Future _load() async { - try { - final device = ref.read(selectedDeviceProvider).value; - if (device == null) return; - - final contacts = await _repository.getWhitelist(deviceId: device.id); - if (!ref.mounted) return; - state = state.copyWith(contacts: contacts, isLoading: false); - } catch (e) { - if (!ref.mounted) return; - state = state.copyWith( - isLoading: false, - errorMessage: formatErrorMessage(e), - ); - } - } - - Future addContact(ContactListContactEntity contact) async { - state = state.copyWith(isSaving: true, errorMessage: ''); - - try { - final device = ref.read(selectedDeviceProvider).value; - if (device == null) return; - - final updatedContacts = [...state.contacts, contact]; - - await _repository.upsertWhitelist( - userId: device.userId ?? '', - deviceId: device.id, - contacts: updatedContacts, - ); - if (!ref.mounted) return; - - unawaited( - _tracking.legacySettingsBlockPhoneContactAdded( - totalCount: updatedContacts.length, - ), - ); - - state = state.copyWith( - contacts: updatedContacts, - isSaving: false, - successMessage: I18n.numberAdded, - ); - } catch (e) { - if (!ref.mounted) return; - state = state.copyWith( - isSaving: false, - errorMessage: formatErrorMessage(e), - ); - } - } - - Future updateContact(int index, ContactListContactEntity contact) async { - state = state.copyWith(isSaving: true, errorMessage: ''); - - try { - final device = ref.read(selectedDeviceProvider).value; - if (device == null) return; - - final updatedContacts = [...state.contacts]; - updatedContacts[index] = contact; - - await _repository.upsertWhitelist( - userId: device.userId ?? '', - deviceId: device.id, - contacts: updatedContacts, - ); - if (!ref.mounted) return; - - state = state.copyWith( - contacts: updatedContacts, - isSaving: false, - successMessage: I18n.numberUpdated, - ); - } catch (e) { - if (!ref.mounted) return; - state = state.copyWith( - isSaving: false, - errorMessage: formatErrorMessage(e), - ); - } - } - - Future removeContact(int index) async { - state = state.copyWith(isSaving: true, errorMessage: ''); - - try { - final device = ref.read(selectedDeviceProvider).value; - if (device == null) return; - - final updatedContacts = [...state.contacts]..removeAt(index); - - await _repository.upsertWhitelist( - userId: device.userId ?? '', - deviceId: device.id, - contacts: updatedContacts, - ); - if (!ref.mounted) return; - - unawaited( - _tracking.legacySettingsBlockPhoneContactRemoved( - totalCount: updatedContacts.length, - ), - ); - - state = state.copyWith( - contacts: updatedContacts, - isSaving: false, - successMessage: I18n.numberRemoved, - ); - } catch (e) { - if (!ref.mounted) return; - state = state.copyWith( - isSaving: false, - errorMessage: formatErrorMessage(e), - ); - } - } - - void clearSuccess() { - state = state.copyWith(successMessage: ''); - } -} diff --git a/modules/legacy/modules/settings/lib/src/features/block_phone/presentation/state/block_phone_view_state.dart b/modules/legacy/modules/settings/lib/src/features/block_phone/presentation/state/block_phone_view_state.dart deleted file mode 100644 index 18ef077a..00000000 --- a/modules/legacy/modules/settings/lib/src/features/block_phone/presentation/state/block_phone_view_state.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:settings/src/core/domain/entities/contact_list_contact_entity.dart'; - -part 'block_phone_view_state.freezed.dart'; - -@freezed -abstract class BlockPhoneViewState with _$BlockPhoneViewState { - const factory BlockPhoneViewState({ - @Default([]) List contacts, - @Default(true) bool isLoading, - @Default(false) bool isSaving, - @Default('') String successMessage, - @Default('') String errorMessage, - }) = _BlockPhoneViewState; -} diff --git a/modules/legacy/modules/settings/lib/src/features/block_phone/presentation/state/block_phone_view_state.freezed.dart b/modules/legacy/modules/settings/lib/src/features/block_phone/presentation/state/block_phone_view_state.freezed.dart deleted file mode 100644 index a11757f4..00000000 --- a/modules/legacy/modules/settings/lib/src/features/block_phone/presentation/state/block_phone_view_state.freezed.dart +++ /dev/null @@ -1,289 +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 'block_phone_view_state.dart'; - -// ************************************************************************** -// FreezedGenerator -// ************************************************************************** - -// dart format off -T _$identity(T value) => value; -/// @nodoc -mixin _$BlockPhoneViewState { - - List get contacts; bool get isLoading; bool get isSaving; String get successMessage; String get errorMessage; -/// Create a copy of BlockPhoneViewState -/// with the given fields replaced by the non-null parameter values. -@JsonKey(includeFromJson: false, includeToJson: false) -@pragma('vm:prefer-inline') -$BlockPhoneViewStateCopyWith get copyWith => _$BlockPhoneViewStateCopyWithImpl(this as BlockPhoneViewState, _$identity); - - - -@override -bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is BlockPhoneViewState&&const DeepCollectionEquality().equals(other.contacts, contacts)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.isSaving, isSaving) || other.isSaving == isSaving)&&(identical(other.successMessage, successMessage) || other.successMessage == successMessage)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)); -} - - -@override -int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(contacts),isLoading,isSaving,successMessage,errorMessage); - -@override -String toString() { - return 'BlockPhoneViewState(contacts: $contacts, isLoading: $isLoading, isSaving: $isSaving, successMessage: $successMessage, errorMessage: $errorMessage)'; -} - - -} - -/// @nodoc -abstract mixin class $BlockPhoneViewStateCopyWith<$Res> { - factory $BlockPhoneViewStateCopyWith(BlockPhoneViewState value, $Res Function(BlockPhoneViewState) _then) = _$BlockPhoneViewStateCopyWithImpl; -@useResult -$Res call({ - List contacts, bool isLoading, bool isSaving, String successMessage, String errorMessage -}); - - - - -} -/// @nodoc -class _$BlockPhoneViewStateCopyWithImpl<$Res> - implements $BlockPhoneViewStateCopyWith<$Res> { - _$BlockPhoneViewStateCopyWithImpl(this._self, this._then); - - final BlockPhoneViewState _self; - final $Res Function(BlockPhoneViewState) _then; - -/// Create a copy of BlockPhoneViewState -/// 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? isSaving = null,Object? successMessage = null,Object? errorMessage = null,}) { - return _then(_self.copyWith( -contacts: null == contacts ? _self.contacts : contacts // ignore: cast_nullable_to_non_nullable -as List,isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable -as bool,isSaving: null == isSaving ? _self.isSaving : isSaving // ignore: cast_nullable_to_non_nullable -as bool,successMessage: null == successMessage ? _self.successMessage : successMessage // ignore: cast_nullable_to_non_nullable -as String,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable -as String, - )); -} - -} - - -/// Adds pattern-matching-related methods to [BlockPhoneViewState]. -extension BlockPhoneViewStatePatterns on BlockPhoneViewState { -/// 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 Function( _BlockPhoneViewState value)? $default,{required TResult orElse(),}){ -final _that = this; -switch (_that) { -case _BlockPhoneViewState() 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 Function( _BlockPhoneViewState value) $default,){ -final _that = this; -switch (_that) { -case _BlockPhoneViewState(): -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? Function( _BlockPhoneViewState value)? $default,){ -final _that = this; -switch (_that) { -case _BlockPhoneViewState() 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 Function( List contacts, bool isLoading, bool isSaving, String successMessage, String errorMessage)? $default,{required TResult orElse(),}) {final _that = this; -switch (_that) { -case _BlockPhoneViewState() when $default != null: -return $default(_that.contacts,_that.isLoading,_that.isSaving,_that.successMessage,_that.errorMessage);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 Function( List contacts, bool isLoading, bool isSaving, String successMessage, String errorMessage) $default,) {final _that = this; -switch (_that) { -case _BlockPhoneViewState(): -return $default(_that.contacts,_that.isLoading,_that.isSaving,_that.successMessage,_that.errorMessage);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? Function( List contacts, bool isLoading, bool isSaving, String successMessage, String errorMessage)? $default,) {final _that = this; -switch (_that) { -case _BlockPhoneViewState() when $default != null: -return $default(_that.contacts,_that.isLoading,_that.isSaving,_that.successMessage,_that.errorMessage);case _: - return null; - -} -} - -} - -/// @nodoc - - -class _BlockPhoneViewState implements BlockPhoneViewState { - const _BlockPhoneViewState({final List contacts = const [], this.isLoading = true, this.isSaving = false, this.successMessage = '', this.errorMessage = ''}): _contacts = contacts; - - - final List _contacts; -@override@JsonKey() List get contacts { - if (_contacts is EqualUnmodifiableListView) return _contacts; - // ignore: implicit_dynamic_type - return EqualUnmodifiableListView(_contacts); -} - -@override@JsonKey() final bool isLoading; -@override@JsonKey() final bool isSaving; -@override@JsonKey() final String successMessage; -@override@JsonKey() final String errorMessage; - -/// Create a copy of BlockPhoneViewState -/// with the given fields replaced by the non-null parameter values. -@override @JsonKey(includeFromJson: false, includeToJson: false) -@pragma('vm:prefer-inline') -_$BlockPhoneViewStateCopyWith<_BlockPhoneViewState> get copyWith => __$BlockPhoneViewStateCopyWithImpl<_BlockPhoneViewState>(this, _$identity); - - - -@override -bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _BlockPhoneViewState&&const DeepCollectionEquality().equals(other._contacts, _contacts)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.isSaving, isSaving) || other.isSaving == isSaving)&&(identical(other.successMessage, successMessage) || other.successMessage == successMessage)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)); -} - - -@override -int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_contacts),isLoading,isSaving,successMessage,errorMessage); - -@override -String toString() { - return 'BlockPhoneViewState(contacts: $contacts, isLoading: $isLoading, isSaving: $isSaving, successMessage: $successMessage, errorMessage: $errorMessage)'; -} - - -} - -/// @nodoc -abstract mixin class _$BlockPhoneViewStateCopyWith<$Res> implements $BlockPhoneViewStateCopyWith<$Res> { - factory _$BlockPhoneViewStateCopyWith(_BlockPhoneViewState value, $Res Function(_BlockPhoneViewState) _then) = __$BlockPhoneViewStateCopyWithImpl; -@override @useResult -$Res call({ - List contacts, bool isLoading, bool isSaving, String successMessage, String errorMessage -}); - - - - -} -/// @nodoc -class __$BlockPhoneViewStateCopyWithImpl<$Res> - implements _$BlockPhoneViewStateCopyWith<$Res> { - __$BlockPhoneViewStateCopyWithImpl(this._self, this._then); - - final _BlockPhoneViewState _self; - final $Res Function(_BlockPhoneViewState) _then; - -/// Create a copy of BlockPhoneViewState -/// 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? isSaving = null,Object? successMessage = null,Object? errorMessage = null,}) { - return _then(_BlockPhoneViewState( -contacts: null == contacts ? _self._contacts : contacts // ignore: cast_nullable_to_non_nullable -as List,isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable -as bool,isSaving: null == isSaving ? _self.isSaving : isSaving // ignore: cast_nullable_to_non_nullable -as bool,successMessage: null == successMessage ? _self.successMessage : successMessage // ignore: cast_nullable_to_non_nullable -as String,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable -as String, - )); -} - - -} - -// dart format on diff --git a/modules/legacy/modules/settings/lib/src/features/block_phone/presentation/state/new_block_phone_contact_view_model.dart b/modules/legacy/modules/settings/lib/src/features/block_phone/presentation/state/new_block_phone_contact_view_model.dart deleted file mode 100644 index 76654f11..00000000 --- a/modules/legacy/modules/settings/lib/src/features/block_phone/presentation/state/new_block_phone_contact_view_model.dart +++ /dev/null @@ -1,121 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:sf_localizations/sf_localizations.dart'; -import 'package:sf_shared/sf_shared.dart'; - -import 'package:settings/src/core/domain/entities/contact_list_contact_entity.dart'; -import 'package:settings/src/features/block_phone/presentation/state/block_phone_view_model.dart'; -import 'package:settings/src/features/block_phone/presentation/state/new_block_phone_contact_view_state.dart'; - -final newBlockPhoneContactViewModelProvider = - NotifierProvider.autoDispose< - NewBlockPhoneContactViewModel, - NewBlockPhoneContactViewState - >(NewBlockPhoneContactViewModel.new); - -class NewBlockPhoneContactViewModel - extends Notifier { - late final TextEditingController nameController; - late final TextEditingController phoneController; - - @override - NewBlockPhoneContactViewState build() { - nameController = TextEditingController(); - phoneController = TextEditingController(); - - nameController.addListener(_refreshCanSave); - phoneController.addListener(_onPhoneChanged); - - ref.onDispose(() { - nameController.removeListener(_refreshCanSave); - phoneController.removeListener(_onPhoneChanged); - nameController.dispose(); - phoneController.dispose(); - }); - - return const NewBlockPhoneContactViewState(); - } - - void _refreshCanSave() { - final canSave = - nameController.text.trim().isNotEmpty && - phoneController.text.trim().isNotEmpty; - if (canSave == state.canSave) return; - state = state.copyWith(canSave: canSave); - } - - void _onPhoneChanged() { - _refreshCanSave(); - if (state.phoneError != null) state = state.copyWith(phoneError: null); - } - - void updateCountry(String isoCode) { - if (isoCode == state.isoCode) return; - state = state.copyWith(isoCode: isoCode, phoneError: null); - } - - void clearPermissionBlocked() { - if (state.permissionBlocked) { - state = state.copyWith(permissionBlocked: false); - } - } - - Future openSystemSettings() => - ref.read(deviceContactPickerProvider).openSystemSettings(); - - Future pickContactFromDevice() async { - final response = await ref - .read(deviceContactPickerProvider) - .pick(hintIsoCode: state.isoCode); - - if (response.outcome == - DeviceContactPickOutcome.permissionPermanentlyDenied) { - state = state.copyWith(permissionBlocked: true); - return; - } - - final data = response.data; - if (data == null) return; - - final parsed = data.parsedPhone; - if (parsed != null) { - state = state.copyWith(isoCode: parsed.isoCode, phoneError: null); - phoneController.text = parsed.nationalNumber; - } else { - phoneController.text = data.rawNumber; - state = state.copyWith(phoneError: null); - } - - if (nameController.text.trim().isEmpty) { - nameController.text = data.displayName; - } - } - - Future submit() async { - if (state.isSubmitting || !state.canSave) return false; - - final parsed = SfPhoneNumber.tryParse( - phoneController.text, - defaultIsoCode: state.isoCode, - ); - if (parsed == null) { - state = state.copyWith(phoneError: I18n.errorMessagePhoneIsInvalid); - return false; - } - - state = state.copyWith(isSubmitting: true); - - await ref - .read(blockPhoneViewModelProvider.notifier) - .addContact( - ContactListContactEntity( - name: nameController.text.trim(), - phone: parsed.e164, - ), - ); - - if (!ref.mounted) return false; - state = state.copyWith(isSubmitting: false); - return true; - } -} diff --git a/modules/legacy/modules/settings/lib/src/features/block_phone/presentation/state/new_block_phone_contact_view_state.dart b/modules/legacy/modules/settings/lib/src/features/block_phone/presentation/state/new_block_phone_contact_view_state.dart deleted file mode 100644 index 2be6f525..00000000 --- a/modules/legacy/modules/settings/lib/src/features/block_phone/presentation/state/new_block_phone_contact_view_state.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'new_block_phone_contact_view_state.freezed.dart'; - -@freezed -abstract class NewBlockPhoneContactViewState - with _$NewBlockPhoneContactViewState { - const factory NewBlockPhoneContactViewState({ - @Default('ES') String isoCode, - @Default(false) bool canSave, - @Default(false) bool isSubmitting, - @Default(false) bool permissionBlocked, - String? phoneError, - }) = _NewBlockPhoneContactViewState; -} diff --git a/modules/legacy/modules/settings/lib/src/features/block_phone/presentation/state/new_block_phone_contact_view_state.freezed.dart b/modules/legacy/modules/settings/lib/src/features/block_phone/presentation/state/new_block_phone_contact_view_state.freezed.dart deleted file mode 100644 index fc335a94..00000000 --- a/modules/legacy/modules/settings/lib/src/features/block_phone/presentation/state/new_block_phone_contact_view_state.freezed.dart +++ /dev/null @@ -1,283 +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_block_phone_contact_view_state.dart'; - -// ************************************************************************** -// FreezedGenerator -// ************************************************************************** - -// dart format off -T _$identity(T value) => value; -/// @nodoc -mixin _$NewBlockPhoneContactViewState { - - String get isoCode; bool get canSave; bool get isSubmitting; bool get permissionBlocked; String? get phoneError; -/// Create a copy of NewBlockPhoneContactViewState -/// with the given fields replaced by the non-null parameter values. -@JsonKey(includeFromJson: false, includeToJson: false) -@pragma('vm:prefer-inline') -$NewBlockPhoneContactViewStateCopyWith get copyWith => _$NewBlockPhoneContactViewStateCopyWithImpl(this as NewBlockPhoneContactViewState, _$identity); - - - -@override -bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is NewBlockPhoneContactViewState&&(identical(other.isoCode, isoCode) || other.isoCode == isoCode)&&(identical(other.canSave, canSave) || other.canSave == canSave)&&(identical(other.isSubmitting, isSubmitting) || other.isSubmitting == isSubmitting)&&(identical(other.permissionBlocked, permissionBlocked) || other.permissionBlocked == permissionBlocked)&&(identical(other.phoneError, phoneError) || other.phoneError == phoneError)); -} - - -@override -int get hashCode => Object.hash(runtimeType,isoCode,canSave,isSubmitting,permissionBlocked,phoneError); - -@override -String toString() { - return 'NewBlockPhoneContactViewState(isoCode: $isoCode, canSave: $canSave, isSubmitting: $isSubmitting, permissionBlocked: $permissionBlocked, phoneError: $phoneError)'; -} - - -} - -/// @nodoc -abstract mixin class $NewBlockPhoneContactViewStateCopyWith<$Res> { - factory $NewBlockPhoneContactViewStateCopyWith(NewBlockPhoneContactViewState value, $Res Function(NewBlockPhoneContactViewState) _then) = _$NewBlockPhoneContactViewStateCopyWithImpl; -@useResult -$Res call({ - String isoCode, bool canSave, bool isSubmitting, bool permissionBlocked, String? phoneError -}); - - - - -} -/// @nodoc -class _$NewBlockPhoneContactViewStateCopyWithImpl<$Res> - implements $NewBlockPhoneContactViewStateCopyWith<$Res> { - _$NewBlockPhoneContactViewStateCopyWithImpl(this._self, this._then); - - final NewBlockPhoneContactViewState _self; - final $Res Function(NewBlockPhoneContactViewState) _then; - -/// Create a copy of NewBlockPhoneContactViewState -/// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? isoCode = null,Object? canSave = null,Object? isSubmitting = null,Object? permissionBlocked = null,Object? phoneError = freezed,}) { - return _then(_self.copyWith( -isoCode: null == isoCode ? _self.isoCode : isoCode // ignore: cast_nullable_to_non_nullable -as String,canSave: null == canSave ? _self.canSave : canSave // ignore: cast_nullable_to_non_nullable -as bool,isSubmitting: null == isSubmitting ? _self.isSubmitting : isSubmitting // ignore: cast_nullable_to_non_nullable -as bool,permissionBlocked: null == permissionBlocked ? _self.permissionBlocked : permissionBlocked // ignore: cast_nullable_to_non_nullable -as bool,phoneError: freezed == phoneError ? _self.phoneError : phoneError // ignore: cast_nullable_to_non_nullable -as String?, - )); -} - -} - - -/// Adds pattern-matching-related methods to [NewBlockPhoneContactViewState]. -extension NewBlockPhoneContactViewStatePatterns on NewBlockPhoneContactViewState { -/// 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 Function( _NewBlockPhoneContactViewState value)? $default,{required TResult orElse(),}){ -final _that = this; -switch (_that) { -case _NewBlockPhoneContactViewState() 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 Function( _NewBlockPhoneContactViewState value) $default,){ -final _that = this; -switch (_that) { -case _NewBlockPhoneContactViewState(): -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? Function( _NewBlockPhoneContactViewState value)? $default,){ -final _that = this; -switch (_that) { -case _NewBlockPhoneContactViewState() 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 Function( String isoCode, bool canSave, bool isSubmitting, bool permissionBlocked, String? phoneError)? $default,{required TResult orElse(),}) {final _that = this; -switch (_that) { -case _NewBlockPhoneContactViewState() when $default != null: -return $default(_that.isoCode,_that.canSave,_that.isSubmitting,_that.permissionBlocked,_that.phoneError);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 Function( String isoCode, bool canSave, bool isSubmitting, bool permissionBlocked, String? phoneError) $default,) {final _that = this; -switch (_that) { -case _NewBlockPhoneContactViewState(): -return $default(_that.isoCode,_that.canSave,_that.isSubmitting,_that.permissionBlocked,_that.phoneError);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? Function( String isoCode, bool canSave, bool isSubmitting, bool permissionBlocked, String? phoneError)? $default,) {final _that = this; -switch (_that) { -case _NewBlockPhoneContactViewState() when $default != null: -return $default(_that.isoCode,_that.canSave,_that.isSubmitting,_that.permissionBlocked,_that.phoneError);case _: - return null; - -} -} - -} - -/// @nodoc - - -class _NewBlockPhoneContactViewState implements NewBlockPhoneContactViewState { - const _NewBlockPhoneContactViewState({this.isoCode = 'ES', this.canSave = false, this.isSubmitting = false, this.permissionBlocked = false, this.phoneError}); - - -@override@JsonKey() final String isoCode; -@override@JsonKey() final bool canSave; -@override@JsonKey() final bool isSubmitting; -@override@JsonKey() final bool permissionBlocked; -@override final String? phoneError; - -/// Create a copy of NewBlockPhoneContactViewState -/// with the given fields replaced by the non-null parameter values. -@override @JsonKey(includeFromJson: false, includeToJson: false) -@pragma('vm:prefer-inline') -_$NewBlockPhoneContactViewStateCopyWith<_NewBlockPhoneContactViewState> get copyWith => __$NewBlockPhoneContactViewStateCopyWithImpl<_NewBlockPhoneContactViewState>(this, _$identity); - - - -@override -bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _NewBlockPhoneContactViewState&&(identical(other.isoCode, isoCode) || other.isoCode == isoCode)&&(identical(other.canSave, canSave) || other.canSave == canSave)&&(identical(other.isSubmitting, isSubmitting) || other.isSubmitting == isSubmitting)&&(identical(other.permissionBlocked, permissionBlocked) || other.permissionBlocked == permissionBlocked)&&(identical(other.phoneError, phoneError) || other.phoneError == phoneError)); -} - - -@override -int get hashCode => Object.hash(runtimeType,isoCode,canSave,isSubmitting,permissionBlocked,phoneError); - -@override -String toString() { - return 'NewBlockPhoneContactViewState(isoCode: $isoCode, canSave: $canSave, isSubmitting: $isSubmitting, permissionBlocked: $permissionBlocked, phoneError: $phoneError)'; -} - - -} - -/// @nodoc -abstract mixin class _$NewBlockPhoneContactViewStateCopyWith<$Res> implements $NewBlockPhoneContactViewStateCopyWith<$Res> { - factory _$NewBlockPhoneContactViewStateCopyWith(_NewBlockPhoneContactViewState value, $Res Function(_NewBlockPhoneContactViewState) _then) = __$NewBlockPhoneContactViewStateCopyWithImpl; -@override @useResult -$Res call({ - String isoCode, bool canSave, bool isSubmitting, bool permissionBlocked, String? phoneError -}); - - - - -} -/// @nodoc -class __$NewBlockPhoneContactViewStateCopyWithImpl<$Res> - implements _$NewBlockPhoneContactViewStateCopyWith<$Res> { - __$NewBlockPhoneContactViewStateCopyWithImpl(this._self, this._then); - - final _NewBlockPhoneContactViewState _self; - final $Res Function(_NewBlockPhoneContactViewState) _then; - -/// Create a copy of NewBlockPhoneContactViewState -/// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? isoCode = null,Object? canSave = null,Object? isSubmitting = null,Object? permissionBlocked = null,Object? phoneError = freezed,}) { - return _then(_NewBlockPhoneContactViewState( -isoCode: null == isoCode ? _self.isoCode : isoCode // ignore: cast_nullable_to_non_nullable -as String,canSave: null == canSave ? _self.canSave : canSave // ignore: cast_nullable_to_non_nullable -as bool,isSubmitting: null == isSubmitting ? _self.isSubmitting : isSubmitting // ignore: cast_nullable_to_non_nullable -as bool,permissionBlocked: null == permissionBlocked ? _self.permissionBlocked : permissionBlocked // ignore: cast_nullable_to_non_nullable -as bool,phoneError: freezed == phoneError ? _self.phoneError : phoneError // ignore: cast_nullable_to_non_nullable -as String?, - )); -} - - -} - -// dart format on diff --git a/modules/legacy/modules/settings/lib/src/features/block_phone/presentation/widgets/add_contact_sheet.dart b/modules/legacy/modules/settings/lib/src/features/block_phone/presentation/widgets/add_contact_sheet.dart index 11234ada..20151e20 100644 --- a/modules/legacy/modules/settings/lib/src/features/block_phone/presentation/widgets/add_contact_sheet.dart +++ b/modules/legacy/modules/settings/lib/src/features/block_phone/presentation/widgets/add_contact_sheet.dart @@ -1,26 +1,37 @@ import 'package:design_system/design_system.dart'; -import 'package:legacy_theme/legacy_theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:legacy_theme/legacy_theme.dart'; +import 'package:settings/src/core/domain/entities/contact_list_contact_entity.dart'; +import 'package:settings/src/core/presentation/widgets/contact_form_sheet.dart'; +import 'package:settings/src/features/block_phone/presentation/providers/block_phone_contact_form_provider.dart'; +import 'package:settings/src/features/block_phone/presentation/providers/block_phone_controller.dart'; import 'package:sf_localizations/sf_localizations.dart'; import 'package:sf_shared/sf_shared.dart'; -import 'package:settings/src/core/domain/entities/contact_list_contact_entity.dart'; -import 'package:settings/src/core/presentation/widgets/contact_form_sheet.dart'; -import 'package:settings/src/features/block_phone/presentation/state/block_phone_view_model.dart'; -import 'package:settings/src/features/block_phone/presentation/state/new_block_phone_contact_view_model.dart'; - -void showAddContactSheet(BuildContext context) { +void showAddContactSheet( + BuildContext context, { + required String deviceId, + required String userId, + required List currentContacts, +}) { showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, - builder: (_) => const _AddContactSheet(), + builder: (_) => _ContactFormSheetWrapper( + deviceId: deviceId, + userId: userId, + currentContacts: currentContacts, + ), ); } void showEditContactSheet( BuildContext context, { + required String deviceId, + required String userId, + required List currentContacts, required int index, required ContactListContactEntity contact, }) { @@ -28,140 +39,206 @@ void showEditContactSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, - builder: (_) => _EditContactSheet(index: index, contact: contact), + builder: (_) => _ContactFormSheetWrapper( + deviceId: deviceId, + userId: userId, + currentContacts: currentContacts, + index: index, + initialContact: contact, + ), ); } -class _AddContactSheet extends ConsumerWidget { - const _AddContactSheet(); +class _ContactFormSheetWrapper extends ConsumerStatefulWidget { + final String deviceId; + final String userId; + final List currentContacts; + final int? index; + final ContactListContactEntity? initialContact; + + const _ContactFormSheetWrapper({ + required this.deviceId, + required this.userId, + required this.currentContacts, + this.index, + this.initialContact, + }); @override - Widget build(BuildContext context, WidgetRef ref) { - final vm = ref.read(newBlockPhoneContactViewModelProvider.notifier); - final state = ref.watch(newBlockPhoneContactViewModelProvider); - - ref.listen( - newBlockPhoneContactViewModelProvider.select((s) => s.permissionBlocked), - (_, blocked) { - if (blocked == true) { - showContactsPermissionDialog( - context, - onOpenSettings: vm.openSystemSettings, - ); - vm.clearPermissionBlocked(); - } - }, - ); - - return ContactFormSheet( - title: context.translate(I18n.addAllowedNumber), - primaryColor: context.sfColors.legacyPrimary, - nameController: vm.nameController, - phoneController: vm.phoneController, - isoCode: state.isoCode, - canSave: state.canSave, - isSubmitting: state.isSubmitting, - phoneError: state.phoneError, - onCountryChanged: vm.updateCountry, - onPickContact: vm.pickContactFromDevice, - onSubmit: () async { - final success = await vm.submit(); - if (success && context.mounted) Navigator.pop(context); - }, - ); - } + ConsumerState<_ContactFormSheetWrapper> createState() => + _ContactFormSheetWrapperState(); } -class _EditContactSheet extends ConsumerStatefulWidget { - final int index; - final ContactListContactEntity contact; - - const _EditContactSheet({required this.index, required this.contact}); - - @override - ConsumerState<_EditContactSheet> createState() => _EditContactSheetState(); -} - -class _EditContactSheetState extends ConsumerState<_EditContactSheet> { +class _ContactFormSheetWrapperState + extends ConsumerState<_ContactFormSheetWrapper> { late final TextEditingController _nameController; late final TextEditingController _phoneController; - String _isoCode = SfPhoneNumber.defaultIsoCode; - bool _canSave = true; - bool _isSubmitting = false; - String? _phoneError; + + bool get _isEdit => widget.initialContact != null; @override void initState() { super.initState(); - _nameController = TextEditingController(text: widget.contact.name); - - final parsed = SfPhoneNumber.tryParse(widget.contact.phone); + final initial = widget.initialContact; + _nameController = TextEditingController(text: initial?.name ?? ''); + final parsed = + initial != null ? SfPhoneNumber.tryParse(initial.phone) : null; _phoneController = TextEditingController( - text: parsed?.nationalNumber ?? widget.contact.phone, + text: parsed?.nationalNumber ?? initial?.phone ?? '', ); - if (parsed != null) _isoCode = parsed.isoCode; - _nameController.addListener(_refreshCanSave); - _phoneController.addListener(_refreshCanSave); + _phoneController.addListener(_onPhoneChanged); } @override void dispose() { + _nameController.removeListener(_refreshCanSave); + _phoneController.removeListener(_onPhoneChanged); _nameController.dispose(); _phoneController.dispose(); super.dispose(); } void _refreshCanSave() { - final canSave = - _nameController.text.trim().isNotEmpty && + final canSave = _nameController.text.trim().isNotEmpty && _phoneController.text.trim().isNotEmpty; - if (canSave != _canSave) setState(() => _canSave = canSave); - if (_phoneError != null) setState(() => _phoneError = null); + ref + .read(blockPhoneContactFormProvider(widget.initialContact).notifier) + .setCanSave(canSave); } - Future _submit() async { - if (_isSubmitting || !_canSave) return; + void _onPhoneChanged() { + _refreshCanSave(); + ref + .read(blockPhoneContactFormProvider(widget.initialContact).notifier) + .clearPhoneError(); + } - final parsed = SfPhoneNumber.tryParse( - _phoneController.text, - defaultIsoCode: _isoCode, - ); - if (parsed == null) { - setState(() => _phoneError = I18n.errorMessagePhoneIsInvalid); + Future _pickContactFromDevice() async { + final formNotifier = ref + .read(blockPhoneContactFormProvider(widget.initialContact).notifier); + final isoCode = + ref.read(blockPhoneContactFormProvider(widget.initialContact)).isoCode; + final response = await ref + .read(deviceContactPickerProvider) + .pick(hintIsoCode: isoCode); + + if (response.outcome == + DeviceContactPickOutcome.permissionPermanentlyDenied) { + formNotifier.setPermissionBlocked(true); return; } - setState(() => _isSubmitting = true); + final data = response.data; + if (data == null) return; - await ref - .read(blockPhoneViewModelProvider.notifier) - .updateContact( - widget.index, - ContactListContactEntity( - name: _nameController.text.trim(), - phone: parsed.e164, - ), - ); + final parsed = data.parsedPhone; + if (parsed != null) { + formNotifier.setIsoCode(parsed.isoCode); + _phoneController.text = parsed.nationalNumber; + } else { + _phoneController.text = data.rawNumber; + formNotifier.clearPhoneError(); + } - if (!mounted) return; - setState(() => _isSubmitting = false); - Navigator.pop(context); + if (_nameController.text.trim().isEmpty) { + _nameController.text = data.displayName; + } + } + + Future _submit() async { + final formNotifier = ref + .read(blockPhoneContactFormProvider(widget.initialContact).notifier); + final formState = + ref.read(blockPhoneContactFormProvider(widget.initialContact)); + if (!formState.canSave) return; + + final parsed = SfPhoneNumber.tryParse( + _phoneController.text, + defaultIsoCode: formState.isoCode, + ); + if (parsed == null) { + formNotifier.setPhoneError(I18n.errorMessagePhoneIsInvalid); + return; + } + + final contact = ContactListContactEntity( + name: _nameController.text.trim(), + phone: parsed.e164, + ); + + final controller = ref.read(blockPhoneControllerProvider.notifier); + if (_isEdit) { + await controller.updateContact( + deviceId: widget.deviceId, + userId: widget.userId, + current: widget.currentContacts, + index: widget.index!, + contact: contact, + ); + } else { + await controller.addContact( + deviceId: widget.deviceId, + userId: widget.userId, + current: widget.currentContacts, + contact: contact, + ); + } + + if (!context.mounted) return; + final state = ref.read(blockPhoneControllerProvider); + if (state.hasError) return; + Navigator.of(context).pop(); + await showSuccessDialog( + context, + _isEdit ? I18n.numberUpdated : I18n.numberAdded, + ); } @override Widget build(BuildContext context) { + ref.listen( + blockPhoneContactFormProvider( + widget.initialContact, + ).select((s) => s.permissionBlocked), + (_, blocked) { + if (blocked == true) { + showContactsPermissionDialog( + context, + onOpenSettings: ref + .read(deviceContactPickerProvider) + .openSystemSettings, + ); + ref + .read( + blockPhoneContactFormProvider(widget.initialContact).notifier, + ) + .setPermissionBlocked(false); + } + }, + ); + + final formState = + ref.watch(blockPhoneContactFormProvider(widget.initialContact)); + final isSubmitting = ref.watch( + blockPhoneControllerProvider.select((s) => s.isLoading), + ); + return ContactFormSheet( - title: context.translate(I18n.editAllowedNumber), + title: context.translate( + _isEdit ? I18n.editAllowedNumber : I18n.addAllowedNumber, + ), primaryColor: context.sfColors.legacyPrimary, nameController: _nameController, phoneController: _phoneController, - isoCode: _isoCode, - canSave: _canSave, - isSubmitting: _isSubmitting, - phoneError: _phoneError, - onCountryChanged: (isoCode) => setState(() => _isoCode = isoCode), - onPickContact: () async {}, + isoCode: formState.isoCode, + canSave: formState.canSave, + isSubmitting: isSubmitting, + phoneError: formState.phoneError, + onCountryChanged: ref + .read(blockPhoneContactFormProvider(widget.initialContact).notifier) + .setIsoCode, + onPickContact: _pickContactFromDevice, onSubmit: _submit, ); } diff --git a/modules/legacy/modules/settings/lib/src/features/sos_contacts/presentation/providers/sos_contacts_controller.dart b/modules/legacy/modules/settings/lib/src/features/sos_contacts/presentation/providers/sos_contacts_controller.dart index 329455c9..64fc0eb6 100644 --- a/modules/legacy/modules/settings/lib/src/features/sos_contacts/presentation/providers/sos_contacts_controller.dart +++ b/modules/legacy/modules/settings/lib/src/features/sos_contacts/presentation/providers/sos_contacts_controller.dart @@ -46,6 +46,7 @@ class SosContactsController extends _$SosContactsController { required List current, required int index, }) async { + state = const AsyncLoading(); if (current.length <= 1) { state = AsyncError( const SosMinimumOneContactException(), @@ -53,7 +54,6 @@ class SosContactsController extends _$SosContactsController { ); return; } - state = const AsyncLoading(); state = await AsyncValue.guard(() async { final updated = [...current]..removeAt(index); await ref.read(sosContactsRepositoryProvider).upsertEmergencyContacts( diff --git a/modules/legacy/modules/settings/test/features/block_phone/block_phone_controller_test.dart b/modules/legacy/modules/settings/test/features/block_phone/block_phone_controller_test.dart new file mode 100644 index 00000000..a421e04a --- /dev/null +++ b/modules/legacy/modules/settings/test/features/block_phone/block_phone_controller_test.dart @@ -0,0 +1,252 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:settings/src/core/domain/entities/contact_list_contact_entity.dart'; +import 'package:settings/src/core/domain/repositories/block_phone_repository.dart'; +import 'package:settings/src/core/providers/block_phone_repository_provider.dart'; +import 'package:settings/src/features/block_phone/presentation/providers/block_phone_controller.dart'; +import 'package:sf_infrastructure/sf_infrastructure.dart'; +import 'package:sf_shared/testing.dart'; +import 'package:sf_tracking/sf_tracking.dart'; + +class MockBlockPhoneRepository extends Mock implements BlockPhoneRepository {} + +const _a = ContactListContactEntity(name: 'Alice', phone: '+34600000001'); +const _b = ContactListContactEntity(name: 'Bob', phone: '+34600000002'); +const _c = ContactListContactEntity(name: 'Carol', phone: '+34600000003'); + +void main() { + setUpAll(() { + registerFallbackValue(const []); + }); + + ProviderContainer buildContainer(BlockPhoneRepository repo) { + return makeContainer( + overrides: [ + blockPhoneRepositoryProvider.overrideWithValue(repo), + sfTrackingProvider.overrideWithValue( + SfTrackingRepository(clients: const []), + ), + ], + ); + } + + group('BlockPhoneController.addContact', () { + test('appends contact and persists via repository', () async { + final repo = MockBlockPhoneRepository(); + when( + () => repo.upsertWhitelist( + userId: any(named: 'userId'), + deviceId: any(named: 'deviceId'), + contacts: any(named: 'contacts'), + ), + ).thenAnswer((_) async {}); + + final container = buildContainer(repo); + addTearDown(container.dispose); + + await container.read(blockPhoneControllerProvider.notifier).addContact( + deviceId: 'd', + userId: 'u', + current: const [_a], + contact: _b, + ); + + expect( + container.read(blockPhoneControllerProvider), + isA>(), + ); + verify( + () => repo.upsertWhitelist( + userId: 'u', + deviceId: 'd', + contacts: [_a, _b], + ), + ).called(1); + }); + + test('exposes AsyncError when repository fails', () async { + final repo = MockBlockPhoneRepository(); + when( + () => repo.upsertWhitelist( + userId: any(named: 'userId'), + deviceId: any(named: 'deviceId'), + contacts: any(named: 'contacts'), + ), + ).thenThrow(const ApiException(message: 'boom', isNetworkError: true)); + + final container = buildContainer(repo); + addTearDown(container.dispose); + + await container.read(blockPhoneControllerProvider.notifier).addContact( + deviceId: 'd', + userId: 'u', + current: const [], + contact: _a, + ); + + expect( + container.read(blockPhoneControllerProvider), + isA>(), + ); + }); + }); + + group('BlockPhoneController.updateContact', () { + test('replaces contact at index and persists via repository', () async { + final repo = MockBlockPhoneRepository(); + when( + () => repo.upsertWhitelist( + userId: any(named: 'userId'), + deviceId: any(named: 'deviceId'), + contacts: any(named: 'contacts'), + ), + ).thenAnswer((_) async {}); + + final container = buildContainer(repo); + addTearDown(container.dispose); + + await container + .read(blockPhoneControllerProvider.notifier) + .updateContact( + deviceId: 'd', + userId: 'u', + current: const [_a, _b], + index: 1, + contact: _c, + ); + + expect( + container.read(blockPhoneControllerProvider), + isA>(), + ); + verify( + () => repo.upsertWhitelist( + userId: 'u', + deviceId: 'd', + contacts: [_a, _c], + ), + ).called(1); + }); + + test('exposes AsyncError when repository fails', () async { + final repo = MockBlockPhoneRepository(); + when( + () => repo.upsertWhitelist( + userId: any(named: 'userId'), + deviceId: any(named: 'deviceId'), + contacts: any(named: 'contacts'), + ), + ).thenThrow(const ApiException(message: 'boom', isNetworkError: true)); + + final container = buildContainer(repo); + addTearDown(container.dispose); + + await container + .read(blockPhoneControllerProvider.notifier) + .updateContact( + deviceId: 'd', + userId: 'u', + current: const [_a], + index: 0, + contact: _b, + ); + + expect( + container.read(blockPhoneControllerProvider), + isA>(), + ); + }); + }); + + group('BlockPhoneController.removeContact', () { + test('removes contact at index and persists via repository', () async { + final repo = MockBlockPhoneRepository(); + when( + () => repo.upsertWhitelist( + userId: any(named: 'userId'), + deviceId: any(named: 'deviceId'), + contacts: any(named: 'contacts'), + ), + ).thenAnswer((_) async {}); + + final container = buildContainer(repo); + addTearDown(container.dispose); + + await container + .read(blockPhoneControllerProvider.notifier) + .removeContact( + deviceId: 'd', + userId: 'u', + current: const [_a, _b], + index: 0, + ); + + expect( + container.read(blockPhoneControllerProvider), + isA>(), + ); + verify( + () => repo.upsertWhitelist( + userId: 'u', + deviceId: 'd', + contacts: [_b], + ), + ).called(1); + }); + + test('blocks removal when only one contact remains', () async { + final repo = MockBlockPhoneRepository(); + final container = buildContainer(repo); + addTearDown(container.dispose); + + await container + .read(blockPhoneControllerProvider.notifier) + .removeContact( + deviceId: 'd', + userId: 'u', + current: const [_a], + index: 0, + ); + + final state = container.read(blockPhoneControllerProvider); + expect(state, isA>()); + expect(state.error, isA()); + verifyNever( + () => repo.upsertWhitelist( + userId: any(named: 'userId'), + deviceId: any(named: 'deviceId'), + contacts: any(named: 'contacts'), + ), + ); + }); + + test('exposes AsyncError when repository fails', () async { + final repo = MockBlockPhoneRepository(); + when( + () => repo.upsertWhitelist( + userId: any(named: 'userId'), + deviceId: any(named: 'deviceId'), + contacts: any(named: 'contacts'), + ), + ).thenThrow(const ApiException(message: 'boom', isNetworkError: true)); + + final container = buildContainer(repo); + addTearDown(container.dispose); + + await container + .read(blockPhoneControllerProvider.notifier) + .removeContact( + deviceId: 'd', + userId: 'u', + current: const [_a, _b], + index: 0, + ); + + expect( + container.read(blockPhoneControllerProvider), + isA>(), + ); + }); + }); +} diff --git a/packages/sf_localizations/assets/l10n/de.json b/packages/sf_localizations/assets/l10n/de.json index c1503a35..5b4c4f88 100644 --- a/packages/sf_localizations/assets/l10n/de.json +++ b/packages/sf_localizations/assets/l10n/de.json @@ -579,6 +579,7 @@ "noBlockedNumbers": "Keine Nummern konfiguriert", "noBlockedNumbersDescription": "Fügen Sie Nummern hinzu, um einzuschränken, wer mit dem Gerät kommunizieren kann", "whitelistDescription": "Nur diese Nummern können mit dem Gerät kommunizieren", + "whitelistMinimumOneContact": "Es muss mindestens 1 erlaubte Nummer vorhanden sein", "allowedNumbersCount": "{count} erlaubte Nummern", "removeAllowedNumber": "Erlaubte Nummer entfernen", "removeAllowedNumberConfirm": "\"{name}\" aus der Liste der erlaubten Nummern entfernen? Diese Nummer kann nicht mehr mit dem Gerät kommunizieren", diff --git a/packages/sf_localizations/assets/l10n/en.json b/packages/sf_localizations/assets/l10n/en.json index 7b96abb7..6c0f9b4b 100755 --- a/packages/sf_localizations/assets/l10n/en.json +++ b/packages/sf_localizations/assets/l10n/en.json @@ -753,6 +753,7 @@ "noBlockedNumbers": "No numbers configured", "noBlockedNumbersDescription": "Add numbers to restrict who can communicate with the device", "whitelistDescription": "Only these numbers can communicate with the device", + "whitelistMinimumOneContact": "There must be at least 1 allowed number", "allowedNumbersCount": "{count} allowed numbers", "removeAllowedNumber": "Remove allowed number", "removeAllowedNumberConfirm": "Remove \"{name}\" from the allowed numbers list? This number will no longer be able to communicate with the device", diff --git a/packages/sf_localizations/assets/l10n/es.json b/packages/sf_localizations/assets/l10n/es.json index 76acfda0..72113a9c 100644 --- a/packages/sf_localizations/assets/l10n/es.json +++ b/packages/sf_localizations/assets/l10n/es.json @@ -754,6 +754,7 @@ "noBlockedNumbers": "No hay números configurados", "noBlockedNumbersDescription": "Añade números para restringir quién puede comunicarse con el dispositivo", "whitelistDescription": "Solo estos números pueden comunicarse con el dispositivo", + "whitelistMinimumOneContact": "Debe haber al menos 1 número permitido", "allowedNumbersCount": "{count} números permitidos", "removeAllowedNumber": "Eliminar número permitido", "removeAllowedNumberConfirm": "¿Eliminar \"{name}\" de la lista de números permitidos? Este número ya no podrá comunicarse con el dispositivo", diff --git a/packages/sf_localizations/assets/l10n/fr.json b/packages/sf_localizations/assets/l10n/fr.json index ebbc13e5..570ed0b2 100644 --- a/packages/sf_localizations/assets/l10n/fr.json +++ b/packages/sf_localizations/assets/l10n/fr.json @@ -579,6 +579,7 @@ "noBlockedNumbers": "Aucun numéro configuré", "noBlockedNumbersDescription": "Ajoutez des numéros pour restreindre qui peut communiquer avec l'appareil", "whitelistDescription": "Seuls ces numéros peuvent communiquer avec l'appareil", + "whitelistMinimumOneContact": "Il doit y avoir au moins 1 numéro autorisé", "allowedNumbersCount": "{count} numéros autorisés", "removeAllowedNumber": "Supprimer le numéro autorisé", "removeAllowedNumberConfirm": "Supprimer \"{name}\" de la liste des numéros autorisés ? Ce numéro ne pourra plus communiquer avec l'appareil", diff --git a/packages/sf_localizations/assets/l10n/it.json b/packages/sf_localizations/assets/l10n/it.json index 4c1f25cb..fb6b3ce3 100644 --- a/packages/sf_localizations/assets/l10n/it.json +++ b/packages/sf_localizations/assets/l10n/it.json @@ -579,6 +579,7 @@ "noBlockedNumbers": "Nessun numero configurato", "noBlockedNumbersDescription": "Aggiungi numeri per limitare chi può comunicare con il dispositivo", "whitelistDescription": "Solo questi numeri possono comunicare con il dispositivo", + "whitelistMinimumOneContact": "Deve esserci almeno 1 numero consentito", "allowedNumbersCount": "{count} numeri consentiti", "removeAllowedNumber": "Rimuovi numero consentito", "removeAllowedNumberConfirm": "Rimuovere \"{name}\" dall'elenco dei numeri consentiti? Questo numero non potrà più comunicare con il dispositivo", diff --git a/packages/sf_localizations/assets/l10n/pt.json b/packages/sf_localizations/assets/l10n/pt.json index 6fb810d5..c72c0093 100644 --- a/packages/sf_localizations/assets/l10n/pt.json +++ b/packages/sf_localizations/assets/l10n/pt.json @@ -579,6 +579,7 @@ "noBlockedNumbers": "Nenhum número configurado", "noBlockedNumbersDescription": "Adicione números para restringir quem pode comunicar com o dispositivo", "whitelistDescription": "Apenas estes números podem comunicar com o dispositivo", + "whitelistMinimumOneContact": "Deve haver pelo menos 1 número permitido", "allowedNumbersCount": "{count} números permitidos", "removeAllowedNumber": "Remover número permitido", "removeAllowedNumberConfirm": "Remover \"{name}\" da lista de números permitidos? Este número não poderá mais comunicar com o dispositivo", diff --git a/packages/sf_localizations/lib/src/generated/i18n.dart b/packages/sf_localizations/lib/src/generated/i18n.dart index 10eaacf1..9b31ab54 100755 --- a/packages/sf_localizations/lib/src/generated/i18n.dart +++ b/packages/sf_localizations/lib/src/generated/i18n.dart @@ -975,6 +975,7 @@ class I18n { static const String weekdayWedShort = 'weekdayWedShort'; static const String welcome = 'welcome'; static const String whitelistDescription = 'whitelistDescription'; + static const String whitelistMinimumOneContact = 'whitelistMinimumOneContact'; static const String wifiAvailableNetworks = 'wifiAvailableNetworks'; static const String wifiBssid = 'wifiBssid'; static const String wifiBssidHint = 'wifiBssidHint';