OnboardingPozoAppv1/flutter_app/lib/screens/mobile_login_screen.dart

654 lines
28 KiB
Dart
Raw Normal View History

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,
),
),
],
),
),
],
),
],
),
),
),
),
),
);
}
}