feat(contact): add contact picker functionality for selecting Indian phone numbers

This commit is contained in:
Vaibhav Surve 2025-07-14 10:12:40 +05:30
parent 574e7df447
commit 395444e8fc
2 changed files with 126 additions and 21 deletions

View File

@ -0,0 +1,90 @@
import 'package:flutter/material.dart';
import 'package:flutter_contacts/flutter_contacts.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/helpers/services/app_logger.dart';
class ContactPickerHelper {
static Future<String?> pickIndianPhoneNumber(BuildContext context) async {
final status = await Permission.contacts.request();
if (!status.isGranted) {
if (status.isPermanentlyDenied) {
await openAppSettings();
}
showAppSnackbar(
title: "Permission Required",
message:
"Please allow Contacts permission from settings to pick a contact.",
type: SnackbarType.warning,
);
return null;
}
try {
final picked = await FlutterContacts.openExternalPick();
if (picked == null) return null;
final contact =
await FlutterContacts.getContact(picked.id, withProperties: true);
if (contact == null || contact.phones.isEmpty) {
showAppSnackbar(
title: "No Phone Number",
message: "Selected contact has no phone number.",
type: SnackbarType.warning,
);
return null;
}
final indiaPhones = contact.phones.where((p) {
final normalized = p.number.replaceAll(RegExp(r'[^0-9+]'), '');
return normalized.startsWith('+91') || RegExp(r'^\d{10}$').hasMatch(normalized);
}).toList();
if (indiaPhones.isEmpty) {
showAppSnackbar(
title: "No Indian Number",
message: "Selected contact has no Indian (+91) phone number.",
type: SnackbarType.warning,
);
return null;
}
if (indiaPhones.length == 1) {
return _normalizeNumber(indiaPhones.first.number);
}
return await showDialog<String>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text("Choose a number"),
content: Column(
mainAxisSize: MainAxisSize.min,
children: indiaPhones
.map((p) => ListTile(
title: Text(p.number),
onTap: () => Navigator.of(ctx).pop(_normalizeNumber(p.number)),
))
.toList(),
),
),
);
} catch (e, st) {
logSafe("Error picking contact", level: LogLevel.error, error: e, stackTrace: st);
showAppSnackbar(
title: "Error",
message: "Failed to fetch contact.",
type: SnackbarType.error,
);
return null;
}
}
static String _normalizeNumber(String raw) {
final normalized = raw.replaceAll(RegExp(r'[^0-9]'), '');
return normalized.length > 10
? normalized.substring(normalized.length - 10)
: normalized;
}
}

View File

@ -7,6 +7,7 @@ import 'package:marco/helpers/widgets/my_spacing.dart';
import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/helpers/widgets/my_text_style.dart'; import 'package:marco/helpers/widgets/my_text_style.dart';
import 'package:marco/model/directory/contact_model.dart'; import 'package:marco/model/directory/contact_model.dart';
import 'package:marco/helpers/utils/contact_picker_helper.dart';
class AddContactBottomSheet extends StatefulWidget { class AddContactBottomSheet extends StatefulWidget {
final ContactModel? existingContact; final ContactModel? existingContact;
@ -184,8 +185,23 @@ class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
inputFormatters: inputType == TextInputType.phone inputFormatters: inputType == TextInputType.phone
? [FilteringTextInputFormatter.digitsOnly] ? [FilteringTextInputFormatter.digitsOnly]
: [], : [],
decoration: _inputDecoration("Enter $inputLabel") decoration: _inputDecoration("Enter $inputLabel").copyWith(
.copyWith(counterText: ""), counterText: "",
suffixIcon: inputType == TextInputType.phone
? IconButton(
icon: const Icon(Icons.contact_phone,
color: Colors.blue),
onPressed: () async {
final selectedPhone =
await ContactPickerHelper.pickIndianPhoneNumber(
context);
if (selectedPhone != null) {
controller.text = selectedPhone;
}
},
)
: null,
),
validator: (value) { validator: (value) {
if (value == null || value.trim().isEmpty) if (value == null || value.trim().isEmpty)
return "$inputLabel is required"; return "$inputLabel is required";
@ -195,7 +211,6 @@ class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
return "Enter valid phone number"; return "Enter valid phone number";
} }
} }
if (inputType == TextInputType.emailAddress && if (inputType == TextInputType.emailAddress &&
!RegExp(r'^[\w.-]+@([\w-]+\.)+[\w-]{2,4}$') !RegExp(r'^[\w.-]+@([\w-]+\.)+[\w-]{2,4}$')
.hasMatch(trimmed)) { .hasMatch(trimmed)) {
@ -243,24 +258,24 @@ class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
Widget _buildPhoneList() => Column( Widget _buildPhoneList() => Column(
children: List.generate(phoneControllers.length, (index) { children: List.generate(phoneControllers.length, (index) {
return Padding( return Padding(
padding: const EdgeInsets.only(bottom: 12), padding: const EdgeInsets.only(bottom: 12),
child: _buildLabeledRow( child: _buildLabeledRow(
"Phone Label", "Phone Label",
phoneLabels[index], phoneLabels[index],
["Work", "Mobile", "Other"], ["Work", "Mobile", "Other"],
"Phone", "Phone",
phoneControllers[index], phoneControllers[index],
TextInputType.phone, TextInputType.phone,
onRemove: phoneControllers.length > 1 onRemove: phoneControllers.length > 1
? () { ? () {
phoneControllers.removeAt(index); phoneControllers.removeAt(index);
phoneLabels.removeAt(index); phoneLabels.removeAt(index);
} }
: null, : null,
), ),
); );
}), }),
); );
Widget _popupSelector({ Widget _popupSelector({