654 lines
28 KiB
Dart
654 lines
28 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter/foundation.dart' show kIsWeb;
|
|
import 'package:permission_handler/permission_handler.dart';
|
|
import 'package:contacts_service/contacts_service.dart';
|
|
import '../theme.dart';
|
|
import '../route_transitions.dart';
|
|
import 'otp_verification_screen.dart';
|
|
|
|
class MockContact {
|
|
final String? displayName;
|
|
final List<MockPhone> phones;
|
|
|
|
MockContact({this.displayName, required String phoneNumber})
|
|
: phones = [MockPhone(phoneNumber)];
|
|
}
|
|
|
|
class MockPhone {
|
|
final String? value;
|
|
MockPhone(this.value);
|
|
}
|
|
|
|
class MobileLoginScreen extends StatefulWidget {
|
|
const MobileLoginScreen({super.key});
|
|
|
|
@override
|
|
State<MobileLoginScreen> createState() => _MobileLoginScreenState();
|
|
}
|
|
|
|
class _MobileLoginScreenState extends State<MobileLoginScreen> {
|
|
final TextEditingController _controller = TextEditingController();
|
|
bool _isValid = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_controller.addListener(_validateInput);
|
|
}
|
|
|
|
void _validateInput() {
|
|
setState(() {
|
|
_isValid = _controller.text.length == 10;
|
|
});
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_controller.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
Future<void> _pickContact() async {
|
|
const permission = Permission.contacts;
|
|
var status = await permission.status;
|
|
if (!mounted) return;
|
|
|
|
if (status.isDenied) {
|
|
final bool? proceed = await showDialog<bool>(
|
|
context: context,
|
|
builder: (context) => AlertDialog(
|
|
title: const Text('Contacts Permission Required'),
|
|
content: const Text(
|
|
'PozoApp requires access to your contacts to quickly auto-fill mobile numbers during registration/billing.',
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(context, false),
|
|
child: const Text('Cancel'),
|
|
),
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(context, true),
|
|
child: const Text('Continue'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
if (!mounted) return;
|
|
if (proceed != true) return;
|
|
status = await permission.request();
|
|
if (!mounted) return;
|
|
}
|
|
|
|
if (status.isGranted || kIsWeb) {
|
|
_showContactPicker();
|
|
} else {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Contacts permission was denied. Please enter the number manually or enable it in Settings.'),
|
|
behavior: SnackBarBehavior.floating,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
Future<List<dynamic>> _loadContacts() async {
|
|
if (kIsWeb) {
|
|
await Future.delayed(const Duration(milliseconds: 600));
|
|
return [
|
|
MockContact(displayName: 'Amit Sharma', phoneNumber: '9876543210'),
|
|
MockContact(displayName: 'Rajesh Patel', phoneNumber: '9823456789'),
|
|
MockContact(displayName: 'Priya Sundaram', phoneNumber: '8765432109'),
|
|
MockContact(displayName: 'Karan Malhotra', phoneNumber: '7654321098'),
|
|
];
|
|
} else {
|
|
try {
|
|
final List<Contact> list = await ContactsService.getContacts(withThumbnails: false);
|
|
return list;
|
|
} catch (e) {
|
|
return [];
|
|
}
|
|
}
|
|
}
|
|
|
|
void _selectContactNumber(String? rawNumber) {
|
|
if (rawNumber == null) return;
|
|
var digits = rawNumber.replaceAll(RegExp(r'\D'), '');
|
|
if (digits.length == 12 && digits.startsWith('91')) {
|
|
digits = digits.substring(2);
|
|
}
|
|
if (digits.length == 11 && digits.startsWith('0')) {
|
|
digits = digits.substring(1);
|
|
}
|
|
if (digits.length > 10) {
|
|
digits = digits.substring(digits.length - 10);
|
|
}
|
|
|
|
setState(() {
|
|
_controller.text = digits;
|
|
_isValid = digits.length == 10;
|
|
});
|
|
}
|
|
|
|
Future<void> _showContactPicker() async {
|
|
List<dynamic> allContacts = [];
|
|
List<dynamic> filteredContacts = [];
|
|
bool isLoading = true;
|
|
|
|
showDialog(
|
|
context: context,
|
|
builder: (context) {
|
|
return StatefulBuilder(
|
|
builder: (context, setDialogState) {
|
|
if (isLoading) {
|
|
_loadContacts().then((loaded) {
|
|
setDialogState(() {
|
|
allContacts = loaded;
|
|
filteredContacts = loaded;
|
|
isLoading = false;
|
|
});
|
|
});
|
|
}
|
|
|
|
return AlertDialog(
|
|
title: const Text('Select Contact'),
|
|
content: SizedBox(
|
|
width: double.maxFinite,
|
|
height: 400,
|
|
child: Column(
|
|
children: [
|
|
TextField(
|
|
decoration: const InputDecoration(
|
|
hintText: 'Search contacts...',
|
|
prefixIcon: Icon(Icons.search),
|
|
),
|
|
onChanged: (query) {
|
|
setDialogState(() {
|
|
filteredContacts = allContacts.where((contact) {
|
|
final name = (contact.displayName ?? '').toLowerCase();
|
|
final phone = (contact.phones.isNotEmpty ? contact.phones.first.value ?? '' : '').toLowerCase();
|
|
return name.contains(query.toLowerCase()) || phone.contains(query.toLowerCase());
|
|
}).toList();
|
|
});
|
|
},
|
|
),
|
|
const SizedBox(height: 12),
|
|
Expanded(
|
|
child: isLoading
|
|
? const Center(child: CircularProgressIndicator())
|
|
: filteredContacts.isEmpty
|
|
? const Center(child: Text('No contacts found'))
|
|
: ListView.builder(
|
|
itemCount: filteredContacts.length,
|
|
itemBuilder: (context, index) {
|
|
final contact = filteredContacts[index];
|
|
final name = contact.displayName ?? 'Unknown';
|
|
final phone = contact.phones.isNotEmpty ? contact.phones.first.value : 'No number';
|
|
return ListTile(
|
|
title: Text(name),
|
|
subtitle: Text(phone),
|
|
leading: CircleAvatar(
|
|
backgroundColor: const Color(0xFFAF101A).withOpacity(0.1),
|
|
child: Text(
|
|
name.isNotEmpty ? name[0].toUpperCase() : '?',
|
|
style: const TextStyle(color: Color(0xFFAF101A), fontWeight: FontWeight.bold),
|
|
),
|
|
),
|
|
onTap: () {
|
|
_selectContactNumber(phone);
|
|
Navigator.pop(context);
|
|
},
|
|
);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(context),
|
|
child: const Text('Close'),
|
|
)
|
|
],
|
|
);
|
|
},
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
final gap = context.responsiveGap;
|
|
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
leading: IconButton(
|
|
icon: const Icon(Icons.arrow_back, color: Color(0xFFAF101A)),
|
|
onPressed: () => Navigator.pop(context),
|
|
),
|
|
title: Text(
|
|
'PozoApp',
|
|
style: theme.textTheme.headlineMedium?.copyWith(
|
|
color: const Color(0xFFAF101A),
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
centerTitle: false,
|
|
elevation: 0,
|
|
backgroundColor: Colors.transparent,
|
|
),
|
|
body: SafeArea(
|
|
child: Center(
|
|
child: ConstrainedBox(
|
|
constraints: const BoxConstraints(maxWidth: 600),
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const SizedBox(height: 16),
|
|
Expanded(
|
|
child: SingleChildScrollView(
|
|
physics: const BouncingScrollPhysics(),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(
|
|
'Step 2 of 5',
|
|
style: theme.textTheme.labelMedium?.copyWith(
|
|
fontWeight: FontWeight.bold,
|
|
color: theme.colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
const Text(
|
|
'Mobile Login',
|
|
style: TextStyle(
|
|
fontFamily: 'Plus Jakarta Sans',
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.bold,
|
|
color: Color(0xFFAF101A),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 8),
|
|
Row(
|
|
children: List.generate(5, (index) {
|
|
return Expanded(
|
|
child: Container(
|
|
height: 6,
|
|
margin: EdgeInsets.only(
|
|
left: index == 0 ? 0 : 3.0,
|
|
right: index == 4 ? 0 : 3.0,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: index <= 1 ? const Color(0xFFAF101A) : const Color(0xFFE2E2E2),
|
|
borderRadius: BorderRadius.circular(100),
|
|
),
|
|
),
|
|
);
|
|
}),
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
Text(
|
|
'Welcome Back',
|
|
style: theme.textTheme.headlineLarge?.copyWith(
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
'Enter your mobile number to sign in or create a new account.',
|
|
style: theme.textTheme.bodyMedium,
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFFFAFFD1).withOpacity(0.8),
|
|
borderRadius: BorderRadius.circular(100),
|
|
border: Border.all(color: const Color(0xFF016619).withOpacity(0.3)),
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
const Icon(Icons.lock, size: 14, color: Color(0xFF016619)),
|
|
const SizedBox(width: 6),
|
|
Text(
|
|
'Secure OTP Verification',
|
|
style: theme.textTheme.labelSmall?.copyWith(
|
|
color: const Color(0xFF016619),
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
Container(
|
|
padding: const EdgeInsets.all(20),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFFF3F3F3),
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(color: const Color(0xFFE4BEBA).withOpacity(0.5)),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Container(
|
|
width: 40,
|
|
height: 40,
|
|
decoration: const BoxDecoration(
|
|
color: Color(0xFF005FAF),
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: const Icon(
|
|
Icons.verified_user,
|
|
color: Colors.white,
|
|
size: 24,
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Verify instantly with',
|
|
style: theme.textTheme.bodySmall?.copyWith(
|
|
color: Colors.grey[600],
|
|
),
|
|
),
|
|
const Text(
|
|
'Truecaller',
|
|
style: TextStyle(
|
|
fontFamily: 'Plus Jakarta Sans',
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.black,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 20),
|
|
const Text(
|
|
'+91 98765 43210',
|
|
style: TextStyle(
|
|
fontFamily: 'Plus Jakarta Sans',
|
|
fontSize: 24,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.black,
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
ElevatedButton.icon(
|
|
onPressed: () {
|
|
_controller.text = '9876543210';
|
|
setState(() {
|
|
_isValid = true;
|
|
});
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('⚡ Phone verified automatically with Truecaller! Joining PozoApp...'),
|
|
backgroundColor: Color(0xFF005FAF),
|
|
behavior: SnackBarBehavior.floating,
|
|
),
|
|
);
|
|
Navigator.push(
|
|
context,
|
|
FadeSlidePageRoute(
|
|
child: const OtpVerificationScreen(
|
|
mobileNumber: '9876543210',
|
|
),
|
|
),
|
|
);
|
|
},
|
|
icon: const Icon(Icons.flash_on, size: 18, color: Colors.white),
|
|
label: const Text(
|
|
'1-Tap Verification',
|
|
style: TextStyle(
|
|
fontFamily: 'Inter',
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: const Color(0xFF005FAF),
|
|
foregroundColor: Colors.white,
|
|
minimumSize: const Size.fromHeight(56),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
elevation: 1,
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
Text(
|
|
'No OTP required. Your phone will be verified automatically.',
|
|
textAlign: TextAlign.center,
|
|
style: theme.textTheme.bodySmall?.copyWith(
|
|
color: Colors.grey[600],
|
|
height: 1.4,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
SizedBox(height: gap),
|
|
|
|
Row(
|
|
children: [
|
|
const Expanded(child: Divider(color: Color(0xFFE4BEBA))),
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
child: Text(
|
|
'Or enter mobile number manually',
|
|
style: theme.textTheme.labelMedium?.copyWith(
|
|
color: Colors.grey[600],
|
|
fontWeight: FontWeight.w500,
|
|
fontSize: 12,
|
|
),
|
|
),
|
|
),
|
|
const Expanded(child: Divider(color: Color(0xFFE4BEBA))),
|
|
],
|
|
),
|
|
|
|
SizedBox(height: gap),
|
|
|
|
Text(
|
|
'Mobile Number',
|
|
style: theme.textTheme.labelSmall?.copyWith(
|
|
color: theme.colorScheme.onSurface,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
|
|
Container(
|
|
height: 56,
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(color: theme.colorScheme.outlineVariant),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.02),
|
|
blurRadius: 4,
|
|
offset: const Offset(0, 2),
|
|
)
|
|
],
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
|
decoration: BoxDecoration(
|
|
border: Border(
|
|
right: BorderSide(color: theme.colorScheme.outlineVariant),
|
|
),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
width: 22,
|
|
height: 14,
|
|
decoration: BoxDecoration(
|
|
border: Border.all(color: Colors.grey[300]!, width: 0.5),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
Container(height: 4, color: const Color(0xFFFF9933)),
|
|
Container(
|
|
height: 4,
|
|
color: Colors.white,
|
|
child: Center(
|
|
child: Container(
|
|
width: 2,
|
|
height: 2,
|
|
decoration: const BoxDecoration(
|
|
color: Color(0xFF000080),
|
|
shape: BoxShape.circle,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
Container(height: 4, color: const Color(0xFF128807)),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
'+91',
|
|
style: theme.textTheme.bodyMedium?.copyWith(
|
|
fontWeight: FontWeight.bold,
|
|
color: theme.colorScheme.onSurface,
|
|
),
|
|
)
|
|
],
|
|
),
|
|
),
|
|
|
|
Expanded(
|
|
child: TextField(
|
|
controller: _controller,
|
|
keyboardType: TextInputType.phone,
|
|
inputFormatters: [
|
|
FilteringTextInputFormatter.digitsOnly,
|
|
LengthLimitingTextInputFormatter(10),
|
|
],
|
|
decoration: const InputDecoration(
|
|
hintText: '00000 00000',
|
|
border: InputBorder.none,
|
|
enabledBorder: InputBorder.none,
|
|
focusedBorder: InputBorder.none,
|
|
filled: false,
|
|
contentPadding: EdgeInsets.symmetric(horizontal: 16),
|
|
),
|
|
style: theme.textTheme.headlineMedium?.copyWith(
|
|
fontWeight: FontWeight.bold,
|
|
color: theme.colorScheme.onSurface,
|
|
),
|
|
),
|
|
),
|
|
IconButton(
|
|
icon: const Icon(Icons.contact_phone, color: Color(0xFFAF101A)),
|
|
onPressed: _pickContact,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
SizedBox(height: gap * 2),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
|
|
Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
ElevatedButton(
|
|
onPressed: _isValid
|
|
? () {
|
|
Navigator.push(
|
|
context,
|
|
FadeSlidePageRoute(
|
|
child: OtpVerificationScreen(
|
|
mobileNumber: _controller.text,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
: null,
|
|
style: ElevatedButton.styleFrom(
|
|
minimumSize: const Size.fromHeight(56),
|
|
backgroundColor: _isValid ? const Color(0xFFAF101A) : Colors.grey[200],
|
|
foregroundColor: _isValid ? Colors.white : Colors.grey[400],
|
|
elevation: _isValid ? 2 : 0,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
),
|
|
child: const Text('Send OTP'),
|
|
),
|
|
SizedBox(height: gap / 2),
|
|
OutlinedButton(
|
|
onPressed: () {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('💬 Connecting with Pozo Support... Please wait.'),
|
|
behavior: SnackBarBehavior.floating,
|
|
),
|
|
);
|
|
},
|
|
style: OutlinedButton.styleFrom(
|
|
minimumSize: const Size.fromHeight(56),
|
|
side: const BorderSide(color: Color(0xFFAF101A), width: 2),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
const Icon(Icons.forum, color: Color(0xFFAF101A), size: 18),
|
|
const SizedBox(width: 8),
|
|
const Text(
|
|
'Help / Talk to Support',
|
|
style: TextStyle(
|
|
color: Color(0xFFAF101A),
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 16,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|