refactor(settings): unify SOS and block_phone sheets with shared ContactFormSheet
This commit is contained in:
@@ -0,0 +1,276 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:sf_localizations/sf_localizations.dart';
|
||||||
|
import 'package:utils/utils.dart';
|
||||||
|
|
||||||
|
/// Presentational sheet shared by SOS contacts and block-phone whitelist
|
||||||
|
/// flows. Holds no state and no VM knowledge — the caller wires its own
|
||||||
|
/// feature VM and passes the pieces down.
|
||||||
|
class ContactFormSheet extends StatelessWidget {
|
||||||
|
final String title;
|
||||||
|
final Color primaryColor;
|
||||||
|
final TextEditingController nameController;
|
||||||
|
final TextEditingController phoneController;
|
||||||
|
final String isoCode;
|
||||||
|
final bool canSave;
|
||||||
|
final bool isSubmitting;
|
||||||
|
final String? phoneError;
|
||||||
|
final ValueChanged<String> onCountryChanged;
|
||||||
|
final Future<void> Function() onPickContact;
|
||||||
|
final VoidCallback onSubmit;
|
||||||
|
|
||||||
|
const ContactFormSheet({
|
||||||
|
super.key,
|
||||||
|
required this.title,
|
||||||
|
required this.primaryColor,
|
||||||
|
required this.nameController,
|
||||||
|
required this.phoneController,
|
||||||
|
required this.isoCode,
|
||||||
|
required this.canSave,
|
||||||
|
required this.isSubmitting,
|
||||||
|
required this.phoneError,
|
||||||
|
required this.onCountryChanged,
|
||||||
|
required this.onPickContact,
|
||||||
|
required this.onSubmit,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final bottomInset = MediaQuery.of(context).viewInsets.bottom;
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.only(bottom: bottomInset),
|
||||||
|
child: Container(
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||||
|
),
|
||||||
|
child: SafeArea(
|
||||||
|
top: false,
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.symmetric(
|
||||||
|
horizontal: SizeUtils.getByScreen(small: 22, big: 24),
|
||||||
|
vertical: SizeUtils.getByScreen(small: 16, big: 18),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const _DragHandle(),
|
||||||
|
SizedBox(height: SizeUtils.getByScreen(small: 12, big: 14)),
|
||||||
|
_Header(
|
||||||
|
title: title,
|
||||||
|
primaryColor: primaryColor,
|
||||||
|
canSave: canSave,
|
||||||
|
isSubmitting: isSubmitting,
|
||||||
|
onSubmit: onSubmit,
|
||||||
|
),
|
||||||
|
SizedBox(height: SizeUtils.getByScreen(small: 16, big: 18)),
|
||||||
|
_FieldLabel(label: context.translate(I18n.name)),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
TextField(
|
||||||
|
controller: nameController,
|
||||||
|
decoration: _inputDecoration(
|
||||||
|
hintText: context.translate(I18n.contactName),
|
||||||
|
primaryColor: primaryColor,
|
||||||
|
),
|
||||||
|
textCapitalization: TextCapitalization.words,
|
||||||
|
),
|
||||||
|
SizedBox(height: SizeUtils.getByScreen(small: 16, big: 18)),
|
||||||
|
_FieldLabel(label: context.translate(I18n.phone)),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_PhoneRow(
|
||||||
|
isoCode: isoCode,
|
||||||
|
controller: phoneController,
|
||||||
|
primaryColor: primaryColor,
|
||||||
|
onCountryChanged: onCountryChanged,
|
||||||
|
onPickContact: onPickContact,
|
||||||
|
),
|
||||||
|
if (phoneError != null) ...[
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
context.translate(phoneError!),
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.red.shade600,
|
||||||
|
fontSize: SizeUtils.getByScreen(small: 13, big: 13),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
SizedBox(height: SizeUtils.getByScreen(small: 16, big: 18)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DragHandle extends StatelessWidget {
|
||||||
|
const _DragHandle();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Center(
|
||||||
|
child: Container(
|
||||||
|
width: 40,
|
||||||
|
height: 4,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.shade300,
|
||||||
|
borderRadius: BorderRadius.circular(2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FieldLabel extends StatelessWidget {
|
||||||
|
final String label;
|
||||||
|
const _FieldLabel({required this.label});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: SizeUtils.getByScreen(small: 15, big: 16),
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _Header extends StatelessWidget {
|
||||||
|
final String title;
|
||||||
|
final Color primaryColor;
|
||||||
|
final bool canSave;
|
||||||
|
final bool isSubmitting;
|
||||||
|
final VoidCallback onSubmit;
|
||||||
|
|
||||||
|
const _Header({
|
||||||
|
required this.title,
|
||||||
|
required this.primaryColor,
|
||||||
|
required this.canSave,
|
||||||
|
required this.isSubmitting,
|
||||||
|
required this.onSubmit,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: SizeUtils.getByScreen(small: 20, big: 21),
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: primaryColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: (canSave && !isSubmitting) ? onSubmit : null,
|
||||||
|
child: isSubmitting
|
||||||
|
? SizedBox(
|
||||||
|
width: SizeUtils.getByScreen(small: 20, big: 22),
|
||||||
|
height: SizeUtils.getByScreen(small: 20, big: 22),
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
color: primaryColor,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Text(
|
||||||
|
context.translate(I18n.save),
|
||||||
|
style: TextStyle(
|
||||||
|
color: canSave ? primaryColor : Colors.grey,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
fontSize: SizeUtils.getByScreen(small: 16, big: 17),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PhoneRow extends StatelessWidget {
|
||||||
|
final String isoCode;
|
||||||
|
final TextEditingController controller;
|
||||||
|
final Color primaryColor;
|
||||||
|
final ValueChanged<String> onCountryChanged;
|
||||||
|
final Future<void> Function() onPickContact;
|
||||||
|
|
||||||
|
const _PhoneRow({
|
||||||
|
required this.isoCode,
|
||||||
|
required this.controller,
|
||||||
|
required this.primaryColor,
|
||||||
|
required this.onCountryChanged,
|
||||||
|
required this.onPickContact,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
CountryPrefixPicker(
|
||||||
|
headerText: context.translate(I18n.selectYourCountry),
|
||||||
|
initialSelection: isoCode,
|
||||||
|
onChanged: (country) {
|
||||||
|
final code = country.code;
|
||||||
|
if (code != null) onCountryChanged(code);
|
||||||
|
},
|
||||||
|
width: 80,
|
||||||
|
),
|
||||||
|
SizedBox(width: SizeUtils.getByScreen(small: 10, big: 8)),
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
controller: controller,
|
||||||
|
decoration: _inputDecoration(
|
||||||
|
hintText: context.translate(I18n.phoneNumber),
|
||||||
|
primaryColor: primaryColor,
|
||||||
|
),
|
||||||
|
keyboardType: TextInputType.phone,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: SizeUtils.getByScreen(small: 10, big: 8)),
|
||||||
|
DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
color: primaryColor,
|
||||||
|
),
|
||||||
|
child: IconButton(
|
||||||
|
onPressed: onPickContact,
|
||||||
|
icon: Icon(
|
||||||
|
SFIcons.contactsCircle,
|
||||||
|
color: Colors.white,
|
||||||
|
size: SizeUtils.getByScreen(small: 28, big: 26),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
InputDecoration _inputDecoration({
|
||||||
|
required String hintText,
|
||||||
|
required Color primaryColor,
|
||||||
|
}) {
|
||||||
|
return InputDecoration(
|
||||||
|
hintText: hintText,
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||||
|
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||||
|
),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||||
|
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||||
|
borderSide: BorderSide(color: primaryColor, width: 2),
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -32,8 +32,10 @@ class BlockPhoneViewModel extends Notifier<BlockPhoneViewState> {
|
|||||||
if (device == null) return;
|
if (device == null) return;
|
||||||
|
|
||||||
final contacts = await _repository.getWhitelist(deviceId: device.id);
|
final contacts = await _repository.getWhitelist(deviceId: device.id);
|
||||||
|
if (!ref.mounted) return;
|
||||||
state = state.copyWith(contacts: contacts, isLoading: false);
|
state = state.copyWith(contacts: contacts, isLoading: false);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
if (!ref.mounted) return;
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
errorMessage: formatErrorMessage(e),
|
errorMessage: formatErrorMessage(e),
|
||||||
@@ -55,6 +57,7 @@ class BlockPhoneViewModel extends Notifier<BlockPhoneViewState> {
|
|||||||
deviceId: device.id,
|
deviceId: device.id,
|
||||||
contacts: updatedContacts,
|
contacts: updatedContacts,
|
||||||
);
|
);
|
||||||
|
if (!ref.mounted) return;
|
||||||
|
|
||||||
unawaited(
|
unawaited(
|
||||||
_tracking.legacySettingsBlockPhoneContactAdded(
|
_tracking.legacySettingsBlockPhoneContactAdded(
|
||||||
@@ -68,6 +71,7 @@ class BlockPhoneViewModel extends Notifier<BlockPhoneViewState> {
|
|||||||
successMessage: I18n.numberAdded,
|
successMessage: I18n.numberAdded,
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
if (!ref.mounted) return;
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
isSaving: false,
|
isSaving: false,
|
||||||
errorMessage: formatErrorMessage(e),
|
errorMessage: formatErrorMessage(e),
|
||||||
@@ -89,6 +93,7 @@ class BlockPhoneViewModel extends Notifier<BlockPhoneViewState> {
|
|||||||
deviceId: device.id,
|
deviceId: device.id,
|
||||||
contacts: updatedContacts,
|
contacts: updatedContacts,
|
||||||
);
|
);
|
||||||
|
if (!ref.mounted) return;
|
||||||
|
|
||||||
unawaited(
|
unawaited(
|
||||||
_tracking.legacySettingsBlockPhoneContactRemoved(
|
_tracking.legacySettingsBlockPhoneContactRemoved(
|
||||||
@@ -102,6 +107,7 @@ class BlockPhoneViewModel extends Notifier<BlockPhoneViewState> {
|
|||||||
successMessage: I18n.numberRemoved,
|
successMessage: I18n.numberRemoved,
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
if (!ref.mounted) return;
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
isSaving: false,
|
isSaving: false,
|
||||||
errorMessage: formatErrorMessage(e),
|
errorMessage: formatErrorMessage(e),
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
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<NewBlockPhoneContactViewState> {
|
||||||
|
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<void> openSystemSettings() =>
|
||||||
|
ref.read(deviceContactPickerProvider).openSystemSettings();
|
||||||
|
|
||||||
|
Future<void> 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<bool> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,283 @@
|
|||||||
|
// 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>(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<NewBlockPhoneContactViewState> get copyWith => _$NewBlockPhoneContactViewStateCopyWithImpl<NewBlockPhoneContactViewState>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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
|
||||||
@@ -1,13 +1,10 @@
|
|||||||
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_contacts/flutter_contacts.dart';
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:sf_localizations/sf_localizations.dart';
|
import 'package:sf_localizations/sf_localizations.dart';
|
||||||
import 'package:utils/utils.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/new_block_phone_contact_view_model.dart';
|
||||||
import '../state/block_phone_view_model.dart';
|
|
||||||
|
|
||||||
void showAddContactSheet(BuildContext context) {
|
void showAddContactSheet(BuildContext context) {
|
||||||
showModalBottomSheet<void>(
|
showModalBottomSheet<void>(
|
||||||
@@ -18,220 +15,43 @@ void showAddContactSheet(BuildContext context) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AddContactSheet extends ConsumerStatefulWidget {
|
class _AddContactSheet extends ConsumerWidget {
|
||||||
const _AddContactSheet();
|
const _AddContactSheet();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ConsumerState<_AddContactSheet> createState() => _AddContactSheetState();
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
}
|
|
||||||
|
|
||||||
class _AddContactSheetState extends ConsumerState<_AddContactSheet> {
|
|
||||||
final _nameController = TextEditingController();
|
|
||||||
final _phoneController = TextEditingController();
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_nameController.dispose();
|
|
||||||
_phoneController.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool get _canSave =>
|
|
||||||
_nameController.text.trim().isNotEmpty &&
|
|
||||||
_phoneController.text.trim().isNotEmpty;
|
|
||||||
|
|
||||||
Future<void> _pickContact() async {
|
|
||||||
final contact = await FlutterContacts.openExternalPick();
|
|
||||||
if (contact == null || !mounted) return;
|
|
||||||
|
|
||||||
final fullContact = await FlutterContacts.getContact(
|
|
||||||
contact.id,
|
|
||||||
withProperties: true,
|
|
||||||
);
|
|
||||||
if (fullContact == null || fullContact.phones.isEmpty) return;
|
|
||||||
|
|
||||||
_phoneController.text = fullContact.phones.first.number;
|
|
||||||
|
|
||||||
if (_nameController.text.trim().isEmpty) {
|
|
||||||
_nameController.text = fullContact.displayName;
|
|
||||||
}
|
|
||||||
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
|
|
||||||
void _submit() {
|
|
||||||
if (!_canSave) return;
|
|
||||||
|
|
||||||
final vm = ref.read(blockPhoneViewModelProvider.notifier);
|
|
||||||
vm.addContact(
|
|
||||||
ContactListContactEntity(
|
|
||||||
name: _nameController.text.trim(),
|
|
||||||
phone: _phoneController.text.trim(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
Navigator.pop(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final theme = ref.watch(themePortProvider);
|
final theme = ref.watch(themePortProvider);
|
||||||
final primaryColor = theme.getColorFor(ThemeCode.legacyPrimary);
|
final vm = ref.read(newBlockPhoneContactViewModelProvider.notifier);
|
||||||
final isSaving = ref.watch(
|
final state = ref.watch(newBlockPhoneContactViewModelProvider);
|
||||||
blockPhoneViewModelProvider.select((s) => s.isSaving),
|
|
||||||
);
|
|
||||||
final bottomInset = MediaQuery.of(context).viewInsets.bottom;
|
|
||||||
|
|
||||||
return Padding(
|
ref.listen(
|
||||||
padding: EdgeInsets.only(bottom: bottomInset),
|
newBlockPhoneContactViewModelProvider.select((s) => s.permissionBlocked),
|
||||||
child: Container(
|
(_, blocked) {
|
||||||
decoration: BoxDecoration(
|
if (blocked == true) {
|
||||||
color: Colors.white,
|
showContactsPermissionDialog(
|
||||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
context,
|
||||||
),
|
onOpenSettings: vm.openSystemSettings,
|
||||||
child: SafeArea(
|
);
|
||||||
top: false,
|
vm.clearPermissionBlocked();
|
||||||
child: Padding(
|
}
|
||||||
padding: EdgeInsets.symmetric(
|
},
|
||||||
horizontal: SizeUtils.getByScreen(small: 22, big: 24),
|
|
||||||
vertical: SizeUtils.getByScreen(small: 16, big: 18),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Center(
|
|
||||||
child: Container(
|
|
||||||
width: 40,
|
|
||||||
height: 4,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.grey.shade300,
|
|
||||||
borderRadius: BorderRadius.circular(2),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(height: SizeUtils.getByScreen(small: 12, big: 14)),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
context.translate(I18n.addAllowedNumber),
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: SizeUtils.getByScreen(small: 20, big: 21),
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: primaryColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
onPressed: _canSave && !isSaving ? _submit : null,
|
|
||||||
child: isSaving
|
|
||||||
? SizedBox(
|
|
||||||
width: SizeUtils.getByScreen(small: 20, big: 22),
|
|
||||||
height: SizeUtils.getByScreen(small: 20, big: 22),
|
|
||||||
child: CircularProgressIndicator(
|
|
||||||
strokeWidth: 2,
|
|
||||||
color: primaryColor,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: Text(
|
|
||||||
context.translate(I18n.save),
|
|
||||||
style: TextStyle(
|
|
||||||
color: _canSave ? primaryColor : Colors.grey,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
fontSize: SizeUtils.getByScreen(
|
|
||||||
small: 16,
|
|
||||||
big: 17,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
SizedBox(height: SizeUtils.getByScreen(small: 16, big: 18)),
|
|
||||||
Text(
|
|
||||||
context.translate(I18n.name),
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: SizeUtils.getByScreen(small: 15, big: 16),
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(height: 8),
|
|
||||||
TextField(
|
|
||||||
controller: _nameController,
|
|
||||||
onChanged: (_) => setState(() {}),
|
|
||||||
decoration: _inputDecoration(
|
|
||||||
hintText: context.translate(I18n.contactName),
|
|
||||||
primaryColor: primaryColor,
|
|
||||||
),
|
|
||||||
textCapitalization: TextCapitalization.words,
|
|
||||||
),
|
|
||||||
SizedBox(height: SizeUtils.getByScreen(small: 16, big: 18)),
|
|
||||||
Text(
|
|
||||||
context.translate(I18n.phone),
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: SizeUtils.getByScreen(small: 15, big: 16),
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(height: 8),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: TextField(
|
|
||||||
controller: _phoneController,
|
|
||||||
onChanged: (_) => setState(() {}),
|
|
||||||
readOnly: true,
|
|
||||||
decoration: _inputDecoration(
|
|
||||||
hintText: context.translate(I18n.phoneNumber),
|
|
||||||
primaryColor: primaryColor,
|
|
||||||
),
|
|
||||||
keyboardType: TextInputType.phone,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(width: SizeUtils.getByScreen(small: 10, big: 8)),
|
|
||||||
DecoratedBox(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
color: primaryColor,
|
|
||||||
),
|
|
||||||
child: IconButton(
|
|
||||||
onPressed: _pickContact,
|
|
||||||
icon: Icon(
|
|
||||||
SFIcons.contactsCircle,
|
|
||||||
color: Colors.white,
|
|
||||||
size: SizeUtils.getByScreen(small: 28, big: 26),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
SizedBox(height: SizeUtils.getByScreen(small: 16, big: 18)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
InputDecoration _inputDecoration({
|
return ContactFormSheet(
|
||||||
required String hintText,
|
title: context.translate(I18n.addAllowedNumber),
|
||||||
required Color primaryColor,
|
primaryColor: theme.getColorFor(ThemeCode.legacyPrimary),
|
||||||
}) {
|
nameController: vm.nameController,
|
||||||
return InputDecoration(
|
phoneController: vm.phoneController,
|
||||||
hintText: hintText,
|
isoCode: state.isoCode,
|
||||||
border: OutlineInputBorder(
|
canSave: state.canSave,
|
||||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
isSubmitting: state.isSubmitting,
|
||||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
phoneError: state.phoneError,
|
||||||
),
|
onCountryChanged: vm.updateCountry,
|
||||||
enabledBorder: OutlineInputBorder(
|
onPickContact: vm.pickContactFromDevice,
|
||||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
onSubmit: () async {
|
||||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
final success = await vm.submit();
|
||||||
),
|
if (success && context.mounted) Navigator.pop(context);
|
||||||
focusedBorder: OutlineInputBorder(
|
},
|
||||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
|
||||||
borderSide: BorderSide(color: primaryColor, width: 2),
|
|
||||||
),
|
|
||||||
contentPadding: EdgeInsets.symmetric(horizontal: 14, vertical: 14),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
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/sos_contacts/presentation/state/new_sos_contact_view_state.dart';
|
||||||
|
import 'package:settings/src/features/sos_contacts/presentation/state/sos_contacts_view_model.dart';
|
||||||
|
|
||||||
|
final newSosContactViewModelProvider =
|
||||||
|
NotifierProvider.autoDispose<NewSosContactViewModel, NewSosContactViewState>(
|
||||||
|
NewSosContactViewModel.new,
|
||||||
|
);
|
||||||
|
|
||||||
|
class NewSosContactViewModel extends Notifier<NewSosContactViewState> {
|
||||||
|
late final TextEditingController nameController;
|
||||||
|
late final TextEditingController phoneController;
|
||||||
|
|
||||||
|
@override
|
||||||
|
NewSosContactViewState 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 NewSosContactViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
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<void> openSystemSettings() =>
|
||||||
|
ref.read(deviceContactPickerProvider).openSystemSettings();
|
||||||
|
|
||||||
|
Future<void> 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<bool> 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(sosContactsViewModelProvider.notifier)
|
||||||
|
.addContact(
|
||||||
|
ContactListContactEntity(
|
||||||
|
name: nameController.text.trim(),
|
||||||
|
phone: parsed.e164,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!ref.mounted) return false;
|
||||||
|
state = state.copyWith(isSubmitting: false);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
|
part 'new_sos_contact_view_state.freezed.dart';
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
abstract class NewSosContactViewState with _$NewSosContactViewState {
|
||||||
|
const factory NewSosContactViewState({
|
||||||
|
@Default('ES') String isoCode,
|
||||||
|
@Default(false) bool canSave,
|
||||||
|
@Default(false) bool isSubmitting,
|
||||||
|
@Default(false) bool permissionBlocked,
|
||||||
|
String? phoneError,
|
||||||
|
}) = _NewSosContactViewState;
|
||||||
|
}
|
||||||
@@ -0,0 +1,283 @@
|
|||||||
|
// 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_sos_contact_view_state.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// FreezedGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
// dart format off
|
||||||
|
T _$identity<T>(T value) => value;
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$NewSosContactViewState {
|
||||||
|
|
||||||
|
String get isoCode; bool get canSave; bool get isSubmitting; bool get permissionBlocked; String? get phoneError;
|
||||||
|
/// Create a copy of NewSosContactViewState
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$NewSosContactViewStateCopyWith<NewSosContactViewState> get copyWith => _$NewSosContactViewStateCopyWithImpl<NewSosContactViewState>(this as NewSosContactViewState, _$identity);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is NewSosContactViewState&&(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 'NewSosContactViewState(isoCode: $isoCode, canSave: $canSave, isSubmitting: $isSubmitting, permissionBlocked: $permissionBlocked, phoneError: $phoneError)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class $NewSosContactViewStateCopyWith<$Res> {
|
||||||
|
factory $NewSosContactViewStateCopyWith(NewSosContactViewState value, $Res Function(NewSosContactViewState) _then) = _$NewSosContactViewStateCopyWithImpl;
|
||||||
|
@useResult
|
||||||
|
$Res call({
|
||||||
|
String isoCode, bool canSave, bool isSubmitting, bool permissionBlocked, String? phoneError
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class _$NewSosContactViewStateCopyWithImpl<$Res>
|
||||||
|
implements $NewSosContactViewStateCopyWith<$Res> {
|
||||||
|
_$NewSosContactViewStateCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final NewSosContactViewState _self;
|
||||||
|
final $Res Function(NewSosContactViewState) _then;
|
||||||
|
|
||||||
|
/// Create a copy of NewSosContactViewState
|
||||||
|
/// 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 [NewSosContactViewState].
|
||||||
|
extension NewSosContactViewStatePatterns on NewSosContactViewState {
|
||||||
|
/// 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( _NewSosContactViewState value)? $default,{required TResult orElse(),}){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _NewSosContactViewState() 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( _NewSosContactViewState value) $default,){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _NewSosContactViewState():
|
||||||
|
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( _NewSosContactViewState value)? $default,){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _NewSosContactViewState() 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 canSave, bool isSubmitting, bool permissionBlocked, String? phoneError)? $default,{required TResult orElse(),}) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _NewSosContactViewState() 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 extends Object?>(TResult Function( String isoCode, bool canSave, bool isSubmitting, bool permissionBlocked, String? phoneError) $default,) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _NewSosContactViewState():
|
||||||
|
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 extends Object?>(TResult? Function( String isoCode, bool canSave, bool isSubmitting, bool permissionBlocked, String? phoneError)? $default,) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _NewSosContactViewState() when $default != null:
|
||||||
|
return $default(_that.isoCode,_that.canSave,_that.isSubmitting,_that.permissionBlocked,_that.phoneError);case _:
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
|
||||||
|
|
||||||
|
class _NewSosContactViewState implements NewSosContactViewState {
|
||||||
|
const _NewSosContactViewState({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 NewSosContactViewState
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$NewSosContactViewStateCopyWith<_NewSosContactViewState> get copyWith => __$NewSosContactViewStateCopyWithImpl<_NewSosContactViewState>(this, _$identity);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is _NewSosContactViewState&&(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 'NewSosContactViewState(isoCode: $isoCode, canSave: $canSave, isSubmitting: $isSubmitting, permissionBlocked: $permissionBlocked, phoneError: $phoneError)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class _$NewSosContactViewStateCopyWith<$Res> implements $NewSosContactViewStateCopyWith<$Res> {
|
||||||
|
factory _$NewSosContactViewStateCopyWith(_NewSosContactViewState value, $Res Function(_NewSosContactViewState) _then) = __$NewSosContactViewStateCopyWithImpl;
|
||||||
|
@override @useResult
|
||||||
|
$Res call({
|
||||||
|
String isoCode, bool canSave, bool isSubmitting, bool permissionBlocked, String? phoneError
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class __$NewSosContactViewStateCopyWithImpl<$Res>
|
||||||
|
implements _$NewSosContactViewStateCopyWith<$Res> {
|
||||||
|
__$NewSosContactViewStateCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final _NewSosContactViewState _self;
|
||||||
|
final $Res Function(_NewSosContactViewState) _then;
|
||||||
|
|
||||||
|
/// Create a copy of NewSosContactViewState
|
||||||
|
/// 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(_NewSosContactViewState(
|
||||||
|
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
|
||||||
@@ -34,8 +34,10 @@ class SosContactsViewModel extends Notifier<SosContactsViewState> {
|
|||||||
final contacts = await _repository.getEmergencyContacts(
|
final contacts = await _repository.getEmergencyContacts(
|
||||||
deviceId: device.id,
|
deviceId: device.id,
|
||||||
);
|
);
|
||||||
|
if (!ref.mounted) return;
|
||||||
state = state.copyWith(contacts: contacts, isLoading: false);
|
state = state.copyWith(contacts: contacts, isLoading: false);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
if (!ref.mounted) return;
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
errorMessage: formatErrorMessage(e),
|
errorMessage: formatErrorMessage(e),
|
||||||
@@ -57,6 +59,7 @@ class SosContactsViewModel extends Notifier<SosContactsViewState> {
|
|||||||
deviceId: device.id,
|
deviceId: device.id,
|
||||||
contacts: updatedContacts,
|
contacts: updatedContacts,
|
||||||
);
|
);
|
||||||
|
if (!ref.mounted) return;
|
||||||
|
|
||||||
unawaited(
|
unawaited(
|
||||||
_tracking.legacySettingsSosContactAdded(
|
_tracking.legacySettingsSosContactAdded(
|
||||||
@@ -70,6 +73,7 @@ class SosContactsViewModel extends Notifier<SosContactsViewState> {
|
|||||||
successMessage: I18n.sosNumberAdded,
|
successMessage: I18n.sosNumberAdded,
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
if (!ref.mounted) return;
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
isSaving: false,
|
isSaving: false,
|
||||||
errorMessage: formatErrorMessage(e),
|
errorMessage: formatErrorMessage(e),
|
||||||
@@ -91,6 +95,7 @@ class SosContactsViewModel extends Notifier<SosContactsViewState> {
|
|||||||
deviceId: device.id,
|
deviceId: device.id,
|
||||||
contacts: updatedContacts,
|
contacts: updatedContacts,
|
||||||
);
|
);
|
||||||
|
if (!ref.mounted) return;
|
||||||
|
|
||||||
unawaited(
|
unawaited(
|
||||||
_tracking.legacySettingsSosContactRemoved(
|
_tracking.legacySettingsSosContactRemoved(
|
||||||
@@ -104,6 +109,7 @@ class SosContactsViewModel extends Notifier<SosContactsViewState> {
|
|||||||
successMessage: I18n.sosNumberRemoved,
|
successMessage: I18n.sosNumberRemoved,
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
if (!ref.mounted) return;
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
isSaving: false,
|
isSaving: false,
|
||||||
errorMessage: formatErrorMessage(e),
|
errorMessage: formatErrorMessage(e),
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
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_contacts/flutter_contacts.dart';
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:sf_localizations/sf_localizations.dart';
|
import 'package:sf_localizations/sf_localizations.dart';
|
||||||
import 'package:utils/utils.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/sos_contacts/presentation/state/new_sos_contact_view_model.dart';
|
||||||
import '../state/sos_contacts_view_model.dart';
|
|
||||||
|
|
||||||
void showAddSosContactSheet(BuildContext context) {
|
void showAddSosContactSheet(BuildContext context) {
|
||||||
showModalBottomSheet<void>(
|
showModalBottomSheet<void>(
|
||||||
@@ -18,221 +15,43 @@ void showAddSosContactSheet(BuildContext context) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AddSosContactSheet extends ConsumerStatefulWidget {
|
class _AddSosContactSheet extends ConsumerWidget {
|
||||||
const _AddSosContactSheet();
|
const _AddSosContactSheet();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ConsumerState<_AddSosContactSheet> createState() =>
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
_AddSosContactSheetState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AddSosContactSheetState extends ConsumerState<_AddSosContactSheet> {
|
|
||||||
final _nameController = TextEditingController();
|
|
||||||
final _phoneController = TextEditingController();
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_nameController.dispose();
|
|
||||||
_phoneController.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool get _canSave =>
|
|
||||||
_nameController.text.trim().isNotEmpty &&
|
|
||||||
_phoneController.text.trim().isNotEmpty;
|
|
||||||
|
|
||||||
Future<void> _pickContact() async {
|
|
||||||
final contact = await FlutterContacts.openExternalPick();
|
|
||||||
if (contact == null || !mounted) return;
|
|
||||||
|
|
||||||
final fullContact = await FlutterContacts.getContact(
|
|
||||||
contact.id,
|
|
||||||
withProperties: true,
|
|
||||||
);
|
|
||||||
if (fullContact == null || fullContact.phones.isEmpty) return;
|
|
||||||
|
|
||||||
_phoneController.text = fullContact.phones.first.number;
|
|
||||||
|
|
||||||
if (_nameController.text.trim().isEmpty) {
|
|
||||||
_nameController.text = fullContact.displayName;
|
|
||||||
}
|
|
||||||
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
|
|
||||||
void _submit() {
|
|
||||||
if (!_canSave) return;
|
|
||||||
|
|
||||||
final vm = ref.read(sosContactsViewModelProvider.notifier);
|
|
||||||
vm.addContact(
|
|
||||||
ContactListContactEntity(
|
|
||||||
name: _nameController.text.trim(),
|
|
||||||
phone: _phoneController.text.trim(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
Navigator.pop(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final theme = ref.watch(themePortProvider);
|
final theme = ref.watch(themePortProvider);
|
||||||
final primaryColor = theme.getColorFor(ThemeCode.legacyPrimary);
|
final vm = ref.read(newSosContactViewModelProvider.notifier);
|
||||||
final isSaving = ref.watch(
|
final state = ref.watch(newSosContactViewModelProvider);
|
||||||
sosContactsViewModelProvider.select((s) => s.isSaving),
|
|
||||||
);
|
|
||||||
final bottomInset = MediaQuery.of(context).viewInsets.bottom;
|
|
||||||
|
|
||||||
return Padding(
|
ref.listen(
|
||||||
padding: EdgeInsets.only(bottom: bottomInset),
|
newSosContactViewModelProvider.select((s) => s.permissionBlocked),
|
||||||
child: Container(
|
(_, blocked) {
|
||||||
decoration: BoxDecoration(
|
if (blocked == true) {
|
||||||
color: Colors.white,
|
showContactsPermissionDialog(
|
||||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
context,
|
||||||
),
|
onOpenSettings: vm.openSystemSettings,
|
||||||
child: SafeArea(
|
);
|
||||||
top: false,
|
vm.clearPermissionBlocked();
|
||||||
child: Padding(
|
}
|
||||||
padding: EdgeInsets.symmetric(
|
},
|
||||||
horizontal: SizeUtils.getByScreen(small: 22, big: 24),
|
|
||||||
vertical: SizeUtils.getByScreen(small: 16, big: 18),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Center(
|
|
||||||
child: Container(
|
|
||||||
width: 40,
|
|
||||||
height: 4,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.grey.shade300,
|
|
||||||
borderRadius: BorderRadius.circular(2),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(height: SizeUtils.getByScreen(small: 12, big: 14)),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
context.translate(I18n.addSosContact),
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: SizeUtils.getByScreen(small: 20, big: 21),
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: primaryColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
onPressed: _canSave && !isSaving ? _submit : null,
|
|
||||||
child: isSaving
|
|
||||||
? SizedBox(
|
|
||||||
width: SizeUtils.getByScreen(small: 20, big: 22),
|
|
||||||
height: SizeUtils.getByScreen(small: 20, big: 22),
|
|
||||||
child: CircularProgressIndicator(
|
|
||||||
strokeWidth: 2,
|
|
||||||
color: primaryColor,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: Text(
|
|
||||||
context.translate(I18n.save),
|
|
||||||
style: TextStyle(
|
|
||||||
color: _canSave ? primaryColor : Colors.grey,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
fontSize: SizeUtils.getByScreen(
|
|
||||||
small: 16,
|
|
||||||
big: 17,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
SizedBox(height: SizeUtils.getByScreen(small: 16, big: 18)),
|
|
||||||
Text(
|
|
||||||
context.translate(I18n.name),
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: SizeUtils.getByScreen(small: 15, big: 16),
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(height: 8),
|
|
||||||
TextField(
|
|
||||||
controller: _nameController,
|
|
||||||
onChanged: (_) => setState(() {}),
|
|
||||||
decoration: _inputDecoration(
|
|
||||||
hintText: context.translate(I18n.contactName),
|
|
||||||
primaryColor: primaryColor,
|
|
||||||
),
|
|
||||||
textCapitalization: TextCapitalization.words,
|
|
||||||
),
|
|
||||||
SizedBox(height: SizeUtils.getByScreen(small: 16, big: 18)),
|
|
||||||
Text(
|
|
||||||
context.translate(I18n.phone),
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: SizeUtils.getByScreen(small: 15, big: 16),
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(height: 8),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: TextField(
|
|
||||||
controller: _phoneController,
|
|
||||||
onChanged: (_) => setState(() {}),
|
|
||||||
readOnly: true,
|
|
||||||
decoration: _inputDecoration(
|
|
||||||
hintText: context.translate(I18n.phoneNumber),
|
|
||||||
primaryColor: primaryColor,
|
|
||||||
),
|
|
||||||
keyboardType: TextInputType.phone,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(width: SizeUtils.getByScreen(small: 10, big: 8)),
|
|
||||||
DecoratedBox(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
color: primaryColor,
|
|
||||||
),
|
|
||||||
child: IconButton(
|
|
||||||
onPressed: _pickContact,
|
|
||||||
icon: Icon(
|
|
||||||
SFIcons.contactsCircle,
|
|
||||||
color: Colors.white,
|
|
||||||
size: SizeUtils.getByScreen(small: 28, big: 26),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
SizedBox(height: SizeUtils.getByScreen(small: 16, big: 18)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
InputDecoration _inputDecoration({
|
return ContactFormSheet(
|
||||||
required String hintText,
|
title: context.translate(I18n.addSosContact),
|
||||||
required Color primaryColor,
|
primaryColor: theme.getColorFor(ThemeCode.legacyPrimary),
|
||||||
}) {
|
nameController: vm.nameController,
|
||||||
return InputDecoration(
|
phoneController: vm.phoneController,
|
||||||
hintText: hintText,
|
isoCode: state.isoCode,
|
||||||
border: OutlineInputBorder(
|
canSave: state.canSave,
|
||||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
isSubmitting: state.isSubmitting,
|
||||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
phoneError: state.phoneError,
|
||||||
),
|
onCountryChanged: vm.updateCountry,
|
||||||
enabledBorder: OutlineInputBorder(
|
onPickContact: vm.pickContactFromDevice,
|
||||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
onSubmit: () async {
|
||||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
final success = await vm.submit();
|
||||||
),
|
if (success && context.mounted) Navigator.pop(context);
|
||||||
focusedBorder: OutlineInputBorder(
|
},
|
||||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
|
||||||
borderSide: BorderSide(color: primaryColor, width: 2),
|
|
||||||
),
|
|
||||||
contentPadding: EdgeInsets.symmetric(horizontal: 14, vertical: 14),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user