2026-06-10 13:29:04 +05:30
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
|
import 'package:flutter/services.dart';
|
2026-06-10 13:48:06 +05:30
|
|
|
import 'package:flutter/foundation.dart' show kIsWeb;
|
|
|
|
|
import 'package:permission_handler/permission_handler.dart';
|
2026-06-10 14:47:50 +05:30
|
|
|
import 'package:flutter_contacts/flutter_contacts.dart';
|
2026-06-10 17:43:21 +05:30
|
|
|
import 'package:url_launcher/url_launcher.dart';
|
2026-06-10 13:29:04 +05:30
|
|
|
import '../theme.dart';
|
2026-06-10 13:48:06 +05:30
|
|
|
import '../route_transitions.dart';
|
2026-06-10 17:43:21 +05:30
|
|
|
import '../feedback_helper.dart';
|
2026-06-10 13:29:04 +05:30
|
|
|
import 'otp_verification_screen.dart';
|
|
|
|
|
|
2026-06-10 13:48:06 +05:30
|
|
|
class MockContact {
|
|
|
|
|
final String? displayName;
|
|
|
|
|
final List<MockPhone> phones;
|
|
|
|
|
|
|
|
|
|
MockContact({this.displayName, required String phoneNumber})
|
|
|
|
|
: phones = [MockPhone(phoneNumber)];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class MockPhone {
|
2026-06-10 14:47:50 +05:30
|
|
|
final String number;
|
|
|
|
|
MockPhone(this.number);
|
2026-06-10 13:48:06 +05:30
|
|
|
}
|
|
|
|
|
|
2026-06-10 13:29:04 +05:30
|
|
|
class MobileLoginScreen extends StatefulWidget {
|
2026-06-10 17:43:21 +05:30
|
|
|
final String? autoFilledMobileNumber;
|
|
|
|
|
|
|
|
|
|
const MobileLoginScreen({super.key, this.autoFilledMobileNumber});
|
2026-06-10 13:29:04 +05:30
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
State<MobileLoginScreen> createState() => _MobileLoginScreenState();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class _MobileLoginScreenState extends State<MobileLoginScreen> {
|
2026-06-10 17:43:21 +05:30
|
|
|
late final TextEditingController _controller;
|
2026-06-10 13:29:04 +05:30
|
|
|
bool _isValid = false;
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void initState() {
|
|
|
|
|
super.initState();
|
2026-06-10 17:43:21 +05:30
|
|
|
_controller = TextEditingController(text: widget.autoFilledMobileNumber ?? '');
|
|
|
|
|
_isValid = _controller.text.length == 10;
|
2026-06-10 13:29:04 +05:30
|
|
|
_controller.addListener(_validateInput);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void _validateInput() {
|
|
|
|
|
setState(() {
|
|
|
|
|
_isValid = _controller.text.length == 10;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void dispose() {
|
|
|
|
|
_controller.dispose();
|
|
|
|
|
super.dispose();
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-10 13:48:06 +05:30
|
|
|
Future<void> _pickContact() async {
|
2026-06-10 17:43:21 +05:30
|
|
|
AppFeedback.click();
|
2026-06-10 13:48:06 +05:30
|
|
|
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 {
|
2026-06-10 14:05:58 +05:30
|
|
|
showTopAlert(
|
|
|
|
|
context,
|
|
|
|
|
'Contacts permission was denied. Please enter the number manually or enable it in Settings.',
|
|
|
|
|
isError: true,
|
2026-06-10 13:48:06 +05:30
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 {
|
2026-06-10 14:47:50 +05:30
|
|
|
final List<Contact> list = await FlutterContacts.getContacts(withProperties: true, withPhoto: false);
|
2026-06-10 13:48:06 +05:30
|
|
|
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();
|
2026-06-10 14:47:50 +05:30
|
|
|
final phone = (contact.phones.isNotEmpty ? contact.phones.first.number : '').toLowerCase();
|
2026-06-10 13:48:06 +05:30
|
|
|
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';
|
2026-06-10 14:47:50 +05:30
|
|
|
final phone = contact.phones.isNotEmpty ? contact.phones.first.number : 'No number';
|
2026-06-10 13:48:06 +05:30
|
|
|
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: () {
|
2026-06-10 17:43:21 +05:30
|
|
|
AppFeedback.click();
|
2026-06-10 13:48:06 +05:30
|
|
|
_selectContactNumber(phone);
|
|
|
|
|
Navigator.pop(context);
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
actions: [
|
|
|
|
|
TextButton(
|
2026-06-10 17:43:21 +05:30
|
|
|
onPressed: () {
|
|
|
|
|
AppFeedback.click();
|
|
|
|
|
Navigator.pop(context);
|
|
|
|
|
},
|
2026-06-10 13:48:06 +05:30
|
|
|
child: const Text('Close'),
|
|
|
|
|
)
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-10 13:29:04 +05:30
|
|
|
@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)),
|
2026-06-10 17:43:21 +05:30
|
|
|
onPressed: () {
|
|
|
|
|
AppFeedback.click();
|
|
|
|
|
Navigator.pop(context);
|
|
|
|
|
},
|
2026-06-10 13:29:04 +05:30
|
|
|
),
|
|
|
|
|
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: [
|
2026-06-10 13:48:06 +05:30
|
|
|
const SizedBox(height: 16),
|
2026-06-10 13:29:04 +05:30
|
|
|
Expanded(
|
|
|
|
|
child: SingleChildScrollView(
|
|
|
|
|
physics: const BouncingScrollPhysics(),
|
|
|
|
|
child: Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: [
|
2026-06-10 14:47:50 +05:30
|
|
|
SlideFadeEntrance(
|
|
|
|
|
delay: Duration.zero,
|
|
|
|
|
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(
|
2026-06-10 13:48:06 +05:30
|
|
|
fontWeight: FontWeight.bold,
|
|
|
|
|
),
|
2026-06-10 13:29:04 +05:30
|
|
|
),
|
2026-06-10 14:47:50 +05:30
|
|
|
const SizedBox(height: 16),
|
|
|
|
|
Text(
|
|
|
|
|
'Enter your mobile number to sign in or create a new account.',
|
|
|
|
|
style: theme.textTheme.bodyMedium,
|
2026-06-10 13:29:04 +05:30
|
|
|
),
|
2026-06-10 14:47:50 +05:30
|
|
|
],
|
2026-06-10 13:29:04 +05:30
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 16),
|
2026-06-10 13:48:06 +05:30
|
|
|
|
2026-06-10 14:47:50 +05:30
|
|
|
SlideFadeEntrance(
|
|
|
|
|
delay: const Duration(milliseconds: 100),
|
|
|
|
|
child: 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,
|
|
|
|
|
),
|
2026-06-10 13:29:04 +05:30
|
|
|
),
|
2026-06-10 14:47:50 +05:30
|
|
|
],
|
|
|
|
|
),
|
2026-06-10 13:29:04 +05:30
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
|
|
2026-06-10 14:47:50 +05:30
|
|
|
SlideFadeEntrance(
|
|
|
|
|
delay: const Duration(milliseconds: 200),
|
|
|
|
|
child: 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,
|
|
|
|
|
),
|
2026-06-10 13:29:04 +05:30
|
|
|
),
|
2026-06-10 14:47:50 +05:30
|
|
|
const SizedBox(width: 12),
|
|
|
|
|
Expanded(
|
|
|
|
|
child: Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: [
|
|
|
|
|
Text(
|
|
|
|
|
'Verify instantly with',
|
|
|
|
|
style: theme.textTheme.bodySmall?.copyWith(
|
|
|
|
|
color: Colors.grey[600],
|
|
|
|
|
),
|
2026-06-10 13:29:04 +05:30
|
|
|
),
|
2026-06-10 14:47:50 +05:30
|
|
|
const Text(
|
|
|
|
|
'Truecaller',
|
|
|
|
|
style: TextStyle(
|
|
|
|
|
fontFamily: 'Plus Jakarta Sans',
|
|
|
|
|
fontSize: 18,
|
|
|
|
|
fontWeight: FontWeight.bold,
|
|
|
|
|
color: Colors.black,
|
|
|
|
|
),
|
2026-06-10 13:29:04 +05:30
|
|
|
),
|
2026-06-10 14:47:50 +05:30
|
|
|
],
|
2026-06-10 13:29:04 +05:30
|
|
|
),
|
|
|
|
|
),
|
2026-06-10 14:47:50 +05:30
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 20),
|
|
|
|
|
const Text(
|
|
|
|
|
'+91 98765 43210',
|
2026-06-10 13:29:04 +05:30
|
|
|
style: TextStyle(
|
2026-06-10 14:47:50 +05:30
|
|
|
fontFamily: 'Plus Jakarta Sans',
|
|
|
|
|
fontSize: 24,
|
2026-06-10 13:29:04 +05:30
|
|
|
fontWeight: FontWeight.bold,
|
2026-06-10 14:47:50 +05:30
|
|
|
color: Colors.black,
|
2026-06-10 13:29:04 +05:30
|
|
|
),
|
|
|
|
|
),
|
2026-06-10 14:47:50 +05:30
|
|
|
const SizedBox(height: 16),
|
|
|
|
|
ElevatedButton.icon(
|
|
|
|
|
onPressed: () {
|
2026-06-10 17:43:21 +05:30
|
|
|
AppFeedback.click();
|
2026-06-10 14:47:50 +05:30
|
|
|
_controller.text = '9876543210';
|
|
|
|
|
setState(() {
|
|
|
|
|
_isValid = true;
|
|
|
|
|
});
|
|
|
|
|
showTopAlert(
|
|
|
|
|
context,
|
|
|
|
|
'Phone verified automatically with Truecaller! Joining PozoApp...',
|
|
|
|
|
icon: Icons.verified_user_outlined,
|
|
|
|
|
);
|
|
|
|
|
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,
|
2026-06-10 13:29:04 +05:30
|
|
|
),
|
|
|
|
|
),
|
2026-06-10 14:47:50 +05:30
|
|
|
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,
|
|
|
|
|
),
|
2026-06-10 13:29:04 +05:30
|
|
|
),
|
2026-06-10 14:47:50 +05:30
|
|
|
],
|
|
|
|
|
),
|
2026-06-10 13:29:04 +05:30
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
SizedBox(height: gap),
|
|
|
|
|
|
2026-06-10 14:47:50 +05:30
|
|
|
SlideFadeEntrance(
|
|
|
|
|
delay: const Duration(milliseconds: 300),
|
|
|
|
|
child: Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: [
|
|
|
|
|
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,
|
2026-06-10 13:29:04 +05:30
|
|
|
),
|
|
|
|
|
),
|
2026-06-10 14:47:50 +05:30
|
|
|
const SizedBox(height: 8),
|
2026-06-10 13:29:04 +05:30
|
|
|
Container(
|
2026-06-10 14:47:50 +05:30
|
|
|
height: 56,
|
2026-06-10 13:29:04 +05:30
|
|
|
decoration: BoxDecoration(
|
2026-06-10 14:47:50 +05:30
|
|
|
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),
|
|
|
|
|
)
|
|
|
|
|
],
|
2026-06-10 13:29:04 +05:30
|
|
|
),
|
|
|
|
|
child: Row(
|
|
|
|
|
children: [
|
|
|
|
|
Container(
|
2026-06-10 14:47:50 +05:30
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
2026-06-10 13:29:04 +05:30
|
|
|
decoration: BoxDecoration(
|
2026-06-10 14:47:50 +05:30
|
|
|
border: Border(
|
|
|
|
|
right: BorderSide(color: theme.colorScheme.outlineVariant),
|
|
|
|
|
),
|
2026-06-10 13:29:04 +05:30
|
|
|
),
|
2026-06-10 14:47:50 +05:30
|
|
|
child: Row(
|
2026-06-10 13:29:04 +05:30
|
|
|
children: [
|
|
|
|
|
Container(
|
2026-06-10 14:47:50 +05:30
|
|
|
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,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
2026-06-10 13:29:04 +05:30
|
|
|
),
|
2026-06-10 14:47:50 +05:30
|
|
|
Container(height: 4, color: const Color(0xFF128807)),
|
|
|
|
|
],
|
2026-06-10 13:29:04 +05:30
|
|
|
),
|
|
|
|
|
),
|
2026-06-10 14:47:50 +05:30
|
|
|
const SizedBox(width: 8),
|
|
|
|
|
Text(
|
|
|
|
|
'+91',
|
|
|
|
|
style: theme.textTheme.bodyMedium?.copyWith(
|
|
|
|
|
fontWeight: FontWeight.bold,
|
|
|
|
|
color: theme.colorScheme.onSurface,
|
|
|
|
|
),
|
|
|
|
|
)
|
2026-06-10 13:29:04 +05:30
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
2026-06-10 14:47:50 +05:30
|
|
|
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,
|
|
|
|
|
),
|
2026-06-10 13:29:04 +05:30
|
|
|
),
|
2026-06-10 14:47:50 +05:30
|
|
|
),
|
|
|
|
|
IconButton(
|
|
|
|
|
icon: const Icon(Icons.contact_phone, color: Color(0xFFAF101A)),
|
|
|
|
|
onPressed: _pickContact,
|
|
|
|
|
),
|
2026-06-10 13:29:04 +05:30
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
SizedBox(height: gap * 2),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
|
2026-06-10 14:47:50 +05:30
|
|
|
SlideFadeEntrance(
|
|
|
|
|
delay: const Duration(milliseconds: 400),
|
|
|
|
|
child: Column(
|
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
|
children: [
|
|
|
|
|
InteractiveScale(
|
|
|
|
|
hoverScale: _isValid ? 1.03 : 1.0,
|
|
|
|
|
tapScale: _isValid ? 0.96 : 1.0,
|
|
|
|
|
child: ElevatedButton(
|
|
|
|
|
onPressed: _isValid
|
|
|
|
|
? () {
|
2026-06-10 17:43:21 +05:30
|
|
|
AppFeedback.click();
|
2026-06-10 14:47:50 +05:30
|
|
|
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(16),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
child: const Text('Send OTP'),
|
2026-06-10 13:29:04 +05:30
|
|
|
),
|
|
|
|
|
),
|
2026-06-10 14:47:50 +05:30
|
|
|
SizedBox(height: gap / 2),
|
|
|
|
|
InteractiveScale(
|
|
|
|
|
child: OutlinedButton(
|
2026-06-10 17:43:21 +05:30
|
|
|
onPressed: () async {
|
|
|
|
|
AppFeedback.click();
|
|
|
|
|
final Uri uri = Uri.parse('tel:7324000011');
|
|
|
|
|
try {
|
|
|
|
|
if (await canLaunchUrl(uri)) {
|
|
|
|
|
await launchUrl(uri);
|
|
|
|
|
} else {
|
|
|
|
|
if (!context.mounted) return;
|
|
|
|
|
showTopAlert(
|
|
|
|
|
context,
|
|
|
|
|
'Could not launch dialer. Call 7324000011',
|
|
|
|
|
isError: true,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
if (!context.mounted) return;
|
|
|
|
|
showTopAlert(
|
|
|
|
|
context,
|
|
|
|
|
'Dialer not available in this environment. Call 7324000011',
|
|
|
|
|
isError: true,
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-06-10 14:47:50 +05:30
|
|
|
},
|
|
|
|
|
style: OutlinedButton.styleFrom(
|
|
|
|
|
minimumSize: const Size.fromHeight(56),
|
|
|
|
|
side: const BorderSide(color: Color(0xFFAF101A), width: 2),
|
|
|
|
|
shape: RoundedRectangleBorder(
|
|
|
|
|
borderRadius: BorderRadius.circular(16),
|
2026-06-10 13:29:04 +05:30
|
|
|
),
|
|
|
|
|
),
|
2026-06-10 14:47:50 +05:30
|
|
|
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,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
2026-06-10 13:29:04 +05:30
|
|
|
),
|
2026-06-10 14:47:50 +05:30
|
|
|
],
|
|
|
|
|
),
|
2026-06-10 13:29:04 +05:30
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|