first commit

This commit is contained in:
Anbarasu
2026-05-26 18:01:57 +05:30
commit 6d59c8daf6
297 changed files with 35238 additions and 0 deletions

View File

@@ -0,0 +1,152 @@
import 'package:flutter/material.dart';
import 'package:in_app_update/in_app_update.dart';
import 'package:lottie/lottie.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:nearledaily/constants/color_constants.dart';
import 'package:new_version_plus/new_version_plus.dart';
import 'package:url_launcher/url_launcher.dart';
class AppUpdateView extends StatefulWidget {
const AppUpdateView({super.key});
@override
State<AppUpdateView> createState() => _AppUpdateViewState();
}
class _AppUpdateViewState extends State<AppUpdateView> {
bool isUpdating = false;
String? errorMessage;
Future<void> _performUpdate() async {
setState(() {
isUpdating = true;
errorMessage = null;
});
try {
final newVersion = NewVersionPlus(androidId: "com.nearle.gear");
final status = await newVersion.getVersionStatus();
if (status == null) {
throw Exception("Could not check version status");
}
if (status.canUpdate) {
print("Launching Play Store for update...");
await newVersion.launchAppStore(status.appStoreLink);
// Note: App will close and open Play Store
} else {
throw Exception("No update available (should not happen)");
}
} catch (e) {
setState(() {
isUpdating = false;
errorMessage = "Failed to open Play Store: $e";
});
// Fallback: Force open Play Store link manually
try {
final Uri playStoreUrl = Uri.parse(
"https://play.google.com/store/apps/details?id=com.nearle.gear");
if (await canLaunchUrl(playStoreUrl)) {
await launchUrl(playStoreUrl);
}
} catch (_) {
setState(() {
errorMessage = "Please update app from Play Store manually";
});
}
}
}
@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 🌟 Beautiful Lottie animation for update
Lottie.asset(
'assets/lotties/update.json',
height: size.height * 0.35,
repeat: true,
fit: BoxFit.contain,
),
const SizedBox(height: 32),
// 📝 Title
Text(
"New Update Available!",
style: GoogleFonts.lato(
fontSize: 22,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
const SizedBox(height: 12),
// 💬 Description
// Text(
// "Weve made improvements and fixed some bugs to make your experience even better. Please update to continue using the app.",
// textAlign: TextAlign.center,
// style: GoogleFonts.lato(
// fontSize: 15,
// color: Colors.grey[700],
// height: 1.5,
// ),
// ),
const SizedBox(height: 40),
// 🔘 Update Button
if (isUpdating)
const CircularProgressIndicator()
else
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: ColorConstants.primaryColor,
padding: const EdgeInsets.symmetric(
horizontal: 60,
vertical: 16,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
),
onPressed: _performUpdate,
child: Text(
"Update Now",
style: GoogleFonts.lato(
color: Colors.white,
fontWeight: FontWeight.w600,
fontSize: 16,
),
),
),
// // ⚠️ Error Message
// if (errorMessage != null) ...[
// const SizedBox(height: 20),
// Text(
// errorMessage!,
// style: GoogleFonts.lato(
// color: Colors.redAccent,
// fontSize: 14,
// ),
// textAlign: TextAlign.center,
// ),
// ],
],
),
),
),
),
);
}
}

View File

@@ -0,0 +1,591 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:geocoding/geocoding.dart';
import 'package:geolocator/geolocator.dart';
import 'package:get/get.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:google_places_flutter/google_places_flutter.dart';
import 'package:google_places_flutter/model/prediction.dart';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
import '../../constants/color_constants.dart';
import '../../constants/font_constants.dart';
import '../../controllers/tenant_controller /tenant_list.dart';
import '../../widgets/text_widget.dart';
import '../home_view.dart';
class CustomerCreateView extends StatefulWidget {
final String mobileNumber;
const CustomerCreateView({super.key,required this.mobileNumber});
@override
State<CustomerCreateView> createState() => _CustomerCreateViewState();
}
class _CustomerCreateViewState extends State<CustomerCreateView> {
Map<String, dynamic>? selectedLocationData;
bool isFetching = false;
final TenantController tenantController = Get.put(TenantController());
final TextEditingController nameController = TextEditingController();
final TextEditingController landmarkController = TextEditingController();
Future<void> createCustomer(Map<String, dynamic> locationData) async {
try {
SharedPreferences prefs = await SharedPreferences.getInstance();
String? fcmToken = prefs.getString('fcmToken') ?? '';
String deviceId = prefs.getString('currentDeviceId') ?? '';
String deviceType = Platform.isAndroid ? "android" : "ios";
final url = Uri.parse('https://fiesta.nearle.app/live/api/v1/mob/customers/create');
final Map<String, dynamic> body = {
"configid": 2,
"firstname": nameController.text.trim(),
"applocationid": 1,
"profileimage": "",
"dialcode": "+91",
"contactno": widget.mobileNumber,
"devicetype": deviceType,
"deviceid": deviceId,
"customertoken": fcmToken,
"address": locationData["address"] ?? "",
"suburb": locationData["suburb"] ?? "",
"city": locationData["city"] ?? "",
"state": locationData["state"] ?? "",
"postcode": locationData["postcode"] ?? "",
"landmark": landmarkController.text.isEmpty ? "near" : landmarkController.text.trim(),
"doorno": locationData["doorno"] ?? "",
"latitude": locationData["latitude"] ?? "",
"longitude": locationData["longitude"] ?? "",
"tenantid": 630,
"email": "",
"primaryaddress": 1,
"gender": "Male",
"dob": "2025-06-30"
};
Fluttertoast.showToast(
msg: "Creating customer...",
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.TOP,
backgroundColor: Colors.black.withOpacity(0.8),
textColor: Colors.white,
fontSize: 15,
);
final response = await http.post(
url,
headers: {"Content-Type": "application/json"},
body: jsonEncode(body),
);
final data = jsonDecode(response.body);
final bool status = data['status'] ?? false;
final String message = data['message'] ?? 'Unknown response';
if (status) {
final details = data['details'];
if (details != null) {
// ✅ Save important details to SharedPreferences
final customerIdStr = details['customerid']?.toString() ?? '0';
await prefs.setInt('customerId', int.tryParse(customerIdStr) ?? 0);
await prefs.setString('customerFirstname', details['firstname'] ?? '');
await prefs.setString('customertoken', details['customertoken'] ?? '');
await prefs.setInt('deliverylocationid', details['deliverylocationid'] ?? 0);
await prefs.setInt('contactno', int.tryParse(details['contactno'] ?? '0') ?? 0);
await prefs.setString('customerAddress', details['address'] ?? '');
await prefs.setString('customerSuburb', details['suburb'] ?? '');
await prefs.setString('customerCity', details['city'] ?? '');
await prefs.setString('customerState', details['state'] ?? '');
await prefs.setString('customerLandmark', details['landmark'] ?? '');
await prefs.setString('customerDoorNo', details['doorno'] ?? '');
debugPrint("✅ Customer info saved to SharedPreferences.");
}
tenantController.loadTenants();
print(data);
// Get.put(TenantController());
Get.offAll(() => BottomNavigation());
// ✅ Use message from API
Fluttertoast.showToast(
msg: "Customer created successfully!",
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.TOP,
backgroundColor: Colors.green.withOpacity(0.8),
textColor: Colors.white,
fontSize: 15,
);
} else {
// ❌ Handle failure message from API
debugPrint("❌ API returned failure: $message");
Fluttertoast.showToast(
msg: "Customer already available",
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.TOP,
backgroundColor: Colors.black.withOpacity(0.8),
textColor: Colors.white,
fontSize: 15,
);
}
} catch (e, stacktrace) {
debugPrint(" Something went wrong");
debugPrint("Stacktrace: $stacktrace");
Fluttertoast.showToast(
msg: "Something went wrong",
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.TOP,
backgroundColor: Colors.black.withOpacity(0.8),
textColor: Colors.white,
fontSize: 15,
);
}
}
@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
final width = size.width;
final height = size.height;
double scaleFont(double size) {
if (width > 800) return size * 1.5;
if (width > 600) return size * 1.3;
return size;
}
return SafeArea(
top: false,
child: Scaffold(
backgroundColor: Colors.grey.shade100,
appBar: AppBar(
backgroundColor: Colors.white,
elevation: 0,
centerTitle: true,
leadingWidth: 300,
leading: Row(
children: [
IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.black87),
onPressed: () => Navigator.pop(context),
),
ReusableTextWidget(
text: "Create Account",
color: ColorConstants.blackColor,
fontWeight: FontWeight.w700,
fontSize: scaleFont(17),
fontFamily: FontConstants.fontFamily,
)
],
),
),
body: SingleChildScrollView(
padding: EdgeInsets.symmetric(horizontal: width * 0.06, vertical: height * 0.02),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.only(bottom: height * 0.02),
child: ReusableTextWidget(
text: "Welcome 👋\nPlease enter your details below",
color: Colors.black87,
fontWeight: FontWeight.w600,
fontSize: scaleFont(13),
fontFamily: FontConstants.fontFamily,
),
),
_buildLabel("Full Name", scaleFont),
_buildTextField("Enter your name", Icons.person, width, controller: nameController),
SizedBox(height: height * 0.03),
_buildLabel("Location", scaleFont),
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.black54, width: 0.40),
),
child: ListTile(
leading: Icon(Icons.location_on, color: ColorConstants.primaryColor),
title: ReusableTextWidget(
text: selectedLocationData == null
? "Use my current location"
: selectedLocationData!["address"],
color: selectedLocationData == null
? ColorConstants.primaryColor
: Colors.black,
fontWeight: FontWeight.w600,
fontSize: 12,
fontFamily: FontConstants.fontFamily,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
subtitle: selectedLocationData == null
? ReusableTextWidget(
text: "Fetching current location...",
color: Colors.grey,
fontWeight: FontWeight.w400,
fontSize: 9,
fontFamily: FontConstants.fontFamily,
)
: null,
trailing: isFetching
? const SizedBox(
width: 22,
height: 22,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.arrow_forward_ios_rounded,
color: Colors.grey, size: 18),
onTap: () async {
setState(() => isFetching = true);
final result = await Get.to(() => const MapPickerPage1());
if (result != null) {
setState(() {
selectedLocationData = result;
});
}
setState(() => isFetching = false);
},
),
),
SizedBox(height: height * 0.03),
_buildLabel("Door No / Landmark", scaleFont),
_buildTextField("Enter door no / landmark", Icons.home_filled, width,
controller: landmarkController),
SizedBox(height: height * 0.05),
],
),
),
bottomNavigationBar: Container(
padding: EdgeInsets.symmetric(horizontal: width * 0.06, vertical: height * 0.02),
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.grey.shade300,
blurRadius: 10,
offset: const Offset(0, -2),
)
],
),
child: SizedBox(
height: height * 0.065,
width: double.infinity,
child: ElevatedButton(
onPressed: selectedLocationData == null
? null
: () {
if (nameController.text.isEmpty) {
Get.snackbar("Error", "Please enter your name");
return;
}
createCustomer(selectedLocationData!);
},
style: ElevatedButton.styleFrom(
backgroundColor: ColorConstants.primaryColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 2,
),
child: Text(
"Submit",
style: TextStyle(
fontFamily: FontConstants.fontFamily,
fontSize: scaleFont(17),
fontWeight: FontWeight.w700,
color: Colors.white,
),
),
),
),
),
),
);
}
Widget _buildLabel(String text, double Function(double) scaleFont) {
return Padding(
padding: const EdgeInsets.only(bottom: 6),
child: ReusableTextWidget(
text: text,
color: Colors.black87,
fontWeight: FontWeight.w700,
fontSize: scaleFont(15),
fontFamily: FontConstants.fontFamily,
),
);
}
Widget _buildTextField(String hint, IconData icon, double width,
{TextEditingController? controller}) {
return TextFormField(
controller: controller,
style: const TextStyle(fontSize: 14),
decoration: InputDecoration(
hintText: hint,
hintStyle: TextStyle(
fontFamily: FontConstants.fontFamily,
fontWeight: FontWeight.w400,
color: Colors.grey,
),
prefixIcon: Icon(icon, color: Colors.grey[700]),
filled: true,
fillColor: Colors.white,
contentPadding: const EdgeInsets.symmetric(vertical: 14, horizontal: 12),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide.none,
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: Colors.black54, width: 0.40),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: ColorConstants.primaryColor, width: 1.3),
),
),
);
}
}
class MapPickerPage1 extends StatefulWidget {
const MapPickerPage1({super.key});
@override
State<MapPickerPage1> createState() => _MapPickerPage1State();
}
class _MapPickerPage1State extends State<MapPickerPage1> {
GoogleMapController? mapController;
LatLng? selectedLatLng;
final TextEditingController searchController = TextEditingController();
@override
void initState() {
super.initState();
_getCurrentLocation();
}
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
// App came back from background, retry location
_getCurrentLocation();
}
}
Future<void> _getCurrentLocation() async {
try {
bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) {
// Get.snackbar("Location Disabled", "Please enable GPS to continue");
await Geolocator.openLocationSettings();
_getCurrentLocation();
serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) return _getCurrentLocation();
}
LocationPermission permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) return;
}
if (permission == LocationPermission.deniedForever) {
Get.snackbar("Permission Denied Forever",
"Please enable location in app settings.");
await Geolocator.openAppSettings();
return;
}
Position position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high,
);
setState(() {
selectedLatLng = LatLng(position.latitude, position.longitude);
});
mapController?.animateCamera(CameraUpdate.newLatLngZoom(selectedLatLng!, 16));
} catch (e) {
Get.snackbar("Error", "Failed to get location: $e");
}
}
Future<void> _goToSearchedPlace(double lat, double lng) async {
setState(() {
selectedLatLng = LatLng(lat, lng);
});
mapController?.animateCamera(
CameraUpdate.newCameraPosition(
CameraPosition(target: selectedLatLng!, zoom: 16),
),
);
}
@override
Widget build(BuildContext context) {
return SafeArea(
top: false,
child: Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
backgroundColor: Colors.white,
title: const Text("Pick Location"),
),
body: Stack(
children: [
selectedLatLng == null
? const Center(child: CircularProgressIndicator())
: GoogleMap(
initialCameraPosition:
CameraPosition(target: selectedLatLng!, zoom: 16),
onMapCreated: (controller) => mapController = controller,
onTap: (latLng) {
setState(() => selectedLatLng = latLng);
},
markers: selectedLatLng != null
? {
Marker(
markerId: const MarkerId("selected"),
position: selectedLatLng!,
draggable: true,
onDragEnd: (newPos) =>
setState(() => selectedLatLng = newPos),
),
}
: {},
),
Positioned(
top: 10,
left: 15,
right: 15,
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 5,
),
],
),
child: GooglePlaceAutoCompleteTextField(
textEditingController: searchController,
googleAPIKey: "AIzaSyBhkGfnq27sN0wV5y_S-M2KojpFTk_by-Q",
inputDecoration: const InputDecoration(
hintText: "Search location...",
border: InputBorder.none,
contentPadding: EdgeInsets.all(12),
),
debounceTime: 400,
countries: ["in"],
isLatLngRequired: true,
getPlaceDetailWithLatLng: (Prediction prediction) {
double lat = double.parse(prediction.lat!);
double lng = double.parse(prediction.lng!);
_goToSearchedPlace(lat, lng);
},
itemClick: (Prediction prediction) {
searchController.text = prediction.description!;
FocusScope.of(context).unfocus();
},
),
),
),
Positioned(
bottom: 30,
left: 20,
right: 20,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: ColorConstants.primaryColor,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
),
onPressed: selectedLatLng == null
? null
: () async {
try {
List<Placemark> placemarks =
await placemarkFromCoordinates(
selectedLatLng!.latitude,
selectedLatLng!.longitude,
);
if (placemarks.isNotEmpty) {
final place = placemarks.first;
String address =
"${place.name}, ${place.locality}, ${place.administrativeArea}, ${place.postalCode}, ${place.country}";
Map<String, dynamic> selectedLocation = {
"address": address,
"suburb": place.subLocality ?? "",
"city": place.locality ?? "",
"state": place.administrativeArea ?? "",
"postcode": place.postalCode ?? "",
"doorno": place.name ?? "",
"landmark": "near",
"latitude": selectedLatLng!.latitude.toString(),
"longitude":
selectedLatLng!.longitude.toString(),
};
Navigator.of(Get.context!).pop(selectedLocation);
}
} catch (e) {
Get.snackbar("Error", "Failed to get location: $e");
}
},
child: const Text(
"Confirm Location",
style: TextStyle(color: Colors.white, fontSize: 16),
),
),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,443 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:webview_flutter/webview_flutter.dart';
import '../../controllers/authentication/auth_controller.dart';
import '../authentication/verification_view.dart';
class Login_view extends StatelessWidget {
Login_view({super.key});
final TextEditingController phoneController = TextEditingController();
// Fix: RxString mirrors the field so Obx rebuilds on every keystroke/clear
final RxString phoneValue = ''.obs;
final RxBool isAgreed = false.obs;
@override
Widget build(BuildContext context) {
Size screenSize = MediaQuery.of(context).size;
return Scaffold(
backgroundColor: Colors.white,
body: Stack(
children: [
// Top curved purple background
Positioned(
top: 0,
left: 0,
right: 0,
child: CustomPaint(
size: Size(screenSize.width, screenSize.height * 0.52),
painter: _TopCurvePainter(),
),
),
// Decorative circles
Positioned(
top: screenSize.height * 0.04,
right: -30,
child: Container(
width: 120,
height: 120,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.white.withOpacity(0.06),
),
),
),
Positioned(
top: screenSize.height * 0.10,
left: -20,
child: Container(
width: 80,
height: 80,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.white.withOpacity(0.06),
),
),
),
SafeArea(
child: SingleChildScrollView(
physics: const ClampingScrollPhysics(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header area
Padding(
padding: EdgeInsets.symmetric(
horizontal: screenSize.width * 0.06,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: screenSize.height * 0.03),
// Logo / brand chip
SizedBox(height: screenSize.height * 0.02),
const Text(
"Groceries & More,\nDelivered in Minutes!",
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w800,
fontSize: 26,
height: 1.25,
letterSpacing: -0.5,
),
),
SizedBox(height: screenSize.height * 0.008),
const Text(
"Sign in to enjoy lightning-fast delivery!",
style: TextStyle(
color: Colors.white70,
fontSize: 14,
height: 1.4,
),
),
],
),
),
// Image — right-aligned, overlapping curve
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Image.asset(
"assets/images/loginImage.png",
height: screenSize.height * 0.30,
fit: BoxFit.contain,
),
],
),
// White card form area
Container(
margin: EdgeInsets.symmetric(
horizontal: screenSize.width * 0.05),
padding: EdgeInsets.symmetric(
horizontal: screenSize.width * 0.05,
vertical: screenSize.height * 0.03,
),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(24),
boxShadow: [
BoxShadow(
color: const Color(0xFF662582).withOpacity(0.08),
blurRadius: 30,
offset: const Offset(0, 8),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"Login or Signup",
style: TextStyle(
color: Color(0xFF1A1A2E),
fontWeight: FontWeight.w800,
fontSize: 20,
letterSpacing: -0.3,
),
),
const SizedBox(height: 4),
const Text(
"Enter your mobile number to continue",
style: TextStyle(
color: Color(0xFF9CA3AF),
fontSize: 13,
),
),
SizedBox(height: screenSize.height * 0.025),
// Phone input
TextFormField(
controller: phoneController,
maxLength: 10,
onChanged: (value) {
phoneValue.value = value; // Fix: keep Rx in sync
if (value.length == 10) {
FocusScope.of(context).unfocus();
}
},
decoration: InputDecoration(
labelText: "Mobile Number",
hintText: "Enter 10-digit number",
counterText: "",
labelStyle: const TextStyle(
color: Color(0xFF662582), fontSize: 13),
hintStyle:
const TextStyle(color: Color(0xFFD1D5DB)),
prefixIcon: Container(
width: screenSize.width * 0.2,
padding:
const EdgeInsets.symmetric(horizontal: 12),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
"+91",
style: TextStyle(
color: Color(0xFF662582),
fontWeight: FontWeight.w600,
fontSize: 15,
),
),
const SizedBox(width: 8),
Container(
width: 1,
height: 20,
color: const Color(0xFFE5E7EB),
),
],
),
),
// filled: true,
// fillColor: const Color(0xFFF9F5FF),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(
color: Color(0xFFE9D5FF), width: 1.2),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(
color: Color(0xFFE9D5FF), width: 1.2),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(
color: Color(0xFF662582), width: 1.8),
),
),
style: const TextStyle(
color: Color(0xFF1A1A2E),
fontWeight: FontWeight.w500,
fontSize: 16,
letterSpacing: 1,
),
cursorColor: const Color(0xFF662582),
keyboardType: TextInputType.phone,
),
SizedBox(height: screenSize.height * 0.02),
// Agree checkbox row
Obx(() => GestureDetector(
onTap: () => isAgreed.value = !isAgreed.value,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
AnimatedContainer(
duration:
const Duration(milliseconds: 200),
width: 20,
height: 20,
decoration: BoxDecoration(
color: isAgreed.value
? const Color(0xFF662582)
: Colors.white,
borderRadius: BorderRadius.circular(5),
border: Border.all(
color: isAgreed.value
? const Color(0xFF662582)
: const Color(0xFFD1D5DB),
width: 1.5,
),
),
child: isAgreed.value
? const Icon(Icons.check,
size: 13, color: Colors.white)
: null,
),
const SizedBox(width: 10),
Expanded(
child: RichText(
text: TextSpan(
style: const TextStyle(
color: Color(0xFF6B7280),
fontSize: 12.5,
),
children: [
const TextSpan(
text: "I agree to the "),
TextSpan(
text: "Terms & Privacy Policy",
style: const TextStyle(
color: Color(0xFF662582),
fontWeight: FontWeight.w600,
),
recognizer:
TapGestureRecognizer()
..onTap = () {
Get.to(() => WebViewScreen(
url:
"https://nearle.in/privacy",
title:
"Terms & Privacy Policy",
));
},
),
],
),
),
),
],
),
)),
SizedBox(height: screenSize.height * 0.025),
// Continue button
SizedBox(
width: double.infinity,
height: 52,
child: Obx(() {
final authController =
Get.find<AuthController>();
final phone = phoneValue.value.trim(); // Fix: reactive read
bool isValidMobile(String phone) {
return RegExp(r'^[6-9]\d{9}$').hasMatch(phone);
}
final bool isPhoneValid = isValidMobile(phone);
final bool canProceed = isPhoneValid &&
isAgreed.value &&
!authController.isLoading.value;
return ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF662582),
foregroundColor: Colors.white,
disabledBackgroundColor:
const Color(0xFFD8B4FE),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
elevation: 0,
),
onPressed: canProceed
? () =>
authController.signIn(context, phone)
: null,
child: authController.isLoading.value
? const SizedBox(
width: 22,
height: 22,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor:
AlwaysStoppedAnimation<Color>(
Colors.white),
),
)
: const Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Text(
"Continue",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
letterSpacing: 0.3,
),
),
SizedBox(width: 8),
Icon(Icons.arrow_forward_rounded,
size: 18),
],
),
);
}),
),
],
),
),
SizedBox(height: screenSize.height * 0.04),
],
),
),
),
],
),
);
}
}
// ── Custom painter for top curved purple background ──────────────────────────
class _TopCurvePainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..shader = const LinearGradient(
colors: [Color(0xFF8B2FC9), Color(0xFF662582)],
begin: Alignment.topRight,
end: Alignment.bottomLeft,
).createShader(Rect.fromLTWH(0, 0, size.width, size.height));
final path = Path();
path.lineTo(0, size.height * 0.85);
path.quadraticBezierTo(
size.width * 0.25,
size.height * 1.0,
size.width * 0.5,
size.height * 0.92,
);
path.quadraticBezierTo(
size.width * 0.75,
size.height * 0.84,
size.width,
size.height * 0.94,
);
path.lineTo(size.width, 0);
path.close();
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(_TopCurvePainter oldDelegate) => false;
}
// ── WebView screen (unchanged) ────────────────────────────────────────────────
class WebViewScreen extends StatefulWidget {
final String url;
final String title;
const WebViewScreen({
super.key,
required this.url,
required this.title,
});
@override
State<WebViewScreen> createState() => _WebViewScreenState();
}
class _WebViewScreenState extends State<WebViewScreen> {
late final WebViewController controller;
@override
void initState() {
super.initState();
controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..loadRequest(Uri.parse(widget.url));
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
backgroundColor: Colors.white,
foregroundColor: Colors.black,
),
body: WebViewWidget(controller: controller),
);
}
}

View File

@@ -0,0 +1,69 @@
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:geolocator/geolocator.dart';
class MapView extends StatefulWidget {
const MapView({super.key});
@override
State<MapView> createState() => _MapViewState();
}
class _MapViewState extends State<MapView> {
GoogleMapController? _mapController;
LatLng? _currentLatLng;
@override
void initState() {
super.initState();
_getCurrentLocation();
}
Future<void> _getCurrentLocation() async {
bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) {
return Future.error('Location services are disabled.');
}
LocationPermission permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) {
return Future.error('Location permission denied');
}
}
if (permission == LocationPermission.deniedForever) {
return Future.error('Location permission permanently denied');
}
Position position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high);
setState(() {
_currentLatLng = LatLng(position.latitude, position.longitude);
});
_mapController?.animateCamera(CameraUpdate.newCameraPosition(
CameraPosition(target: _currentLatLng!, zoom: 15),
));
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Select Location")),
body: _currentLatLng == null
? const Center(child: CircularProgressIndicator())
: GoogleMap(
initialCameraPosition:
CameraPosition(target: _currentLatLng!, zoom: 15),
myLocationEnabled: true,
myLocationButtonEnabled: true,
onMapCreated: (controller) {
_mapController = controller;
},
),
);
}
}

View File

@@ -0,0 +1,331 @@
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:get/get.dart';
import 'package:otp_timer_button/otp_timer_button.dart';
import 'package:sms_autofill/sms_autofill.dart';
import '../../controllers/authentication/auth_controller.dart';
class VerificationUiPage extends StatefulWidget {
final String phoneNumber;
final bool isNewUser; // true if new user, false if existing
const VerificationUiPage({
super.key,
required this.phoneNumber,
required this.isNewUser,
});
@override
State<VerificationUiPage> createState() => _VerificationUiPageState();
}
class _VerificationUiPageState extends State<VerificationUiPage>
with CodeAutoFill {
String? otpCode;
final AuthController authController = Get.find<AuthController>(); // ✅ Reuses existing instance with isNewUser state
// final AuthController authController = Get.put(AuthController()); // ✅ Controller instance
final OtpTimerButtonController otpTimerController = OtpTimerButtonController();
@override
void initState() {
super.initState();
listenForCode();
}
@override
void codeUpdated() {
setState(() {
otpCode = code;
});
// Auto-verify when OTP is received
if (otpCode != null && otpCode!.length == 6) {
authController.validateOtp(otpCode!, context);
}
}
@override
void dispose() {
cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
Size screenSize = MediaQuery.of(context).size;
return Scaffold(
backgroundColor: const Color(0xFF662582),
body: Stack(
alignment: Alignment.bottomCenter,
children: [
/// Top Section
Container(
width: double.infinity,
padding: EdgeInsets.only(
top: screenSize.height * 0.07,
left: screenSize.width * 0.06,
right: screenSize.width * 0.06,
),
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [Color(0xFF662582), Color(0xFF8546A6)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"Groceries, Essentials & More Delivered in Minutes!",
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 26,
height: 1.3,
),
),
SizedBox(height: screenSize.height * 0.02),
const Text(
"Sign in to enjoy lightning-fast delivery!",
style: TextStyle(
color: Colors.white70,
fontSize: 16,
),
),
Align(
alignment: Alignment.centerRight,
child: Image.asset(
"assets/images/loginImage.png",
height: screenSize.height * 0.35,
fit: BoxFit.contain,
),
),
],
),
),
/// Bottom OTP Section
SingleChildScrollView(
child: Container(
width: screenSize.width,
padding: EdgeInsets.all(screenSize.width * 0.07),
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
boxShadow: [
BoxShadow(
color: Colors.black12,
blurRadius: 10,
spreadRadius: 2,
offset: Offset(0, -3),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
/// Title
const Text(
"Verify with OTP",
style: TextStyle(
color: Colors.black87,
fontWeight: FontWeight.w700,
fontSize: 22,
),
),
SizedBox(height: screenSize.height * 0.01),
Text(
"6 digit OTP has been sent to your number",
style: TextStyle(
color: Colors.grey[600],
fontSize: 16,
),
),
SizedBox(height: screenSize.height * 0.02),
/// Number + Change
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
widget.phoneNumber,
style: const TextStyle(
color: Colors.black87,
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
Row(
children: [
const Text(
"Not Yours?",
style: TextStyle(
color: Colors.black54,
fontWeight: FontWeight.w500,
fontSize: 15,
),
),
const SizedBox(width: 6),
InkWell(
onTap: () => Navigator.of(context).pop(),
child: const Text(
"Change",
style: TextStyle(
color: Color(0xFF662582),
fontWeight: FontWeight.bold,
fontSize: 15,
),
),
),
],
),
],
),
SizedBox(height: screenSize.height * 0.04),
/// OTP Input
Center(
child: PinFieldAutoFill(
codeLength: 6,
decoration: BoxLooseDecoration(
strokeColorBuilder:
FixedColorBuilder(Colors.grey.shade400),
bgColorBuilder:
FixedColorBuilder(Colors.grey.shade100),
gapSpace: 12,
radius: const Radius.circular(10),
textStyle: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
onCodeChanged: (code) {
otpCode = code;
if (code != null && code.length == 6) {
authController.validateOtp(otpCode!, context, widget.isNewUser);
}
},
),
),
SizedBox(height: screenSize.height * 0.04),
/// Resend OTP
Center(
child: Column(
children: [
Text(
"Didnt receive an OTP?",
style: TextStyle(
color: Colors.grey[600],
fontSize: 15,
),
),
OtpTimerButton(
controller: otpTimerController,
onPressed: () async {
await authController.receiveSmsOtp(); // ✅ Resend OTP
Fluttertoast.showToast(
msg: "A new OTP has been sent to your number",
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.TOP,
backgroundColor: Colors.green.withOpacity(0.8),
textColor: Colors.white,
fontSize: 15,
);
},
text: const Text(
"Resend Again",
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
color: Color(0xFF662582),
),
),
duration: 60,
buttonType: ButtonType.text_button,
),
],
),
),
SizedBox(height: screenSize.height * 0.04),
/// Verify Button
SizedBox(
width: double.infinity,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF662582),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
padding:
const EdgeInsets.symmetric(vertical: 14),
),
onPressed: () {
if (otpCode != null && otpCode!.length == 6) {
authController.validateOtp(otpCode!, context);
} else {
Fluttertoast.showToast(
msg: "Enter a valid OTP",
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.TOP,
backgroundColor: Colors.green.withOpacity(0.8),
textColor: Colors.white,
fontSize: 15,
);
}
},
child: const Text(
"Verify OTP",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
),
SizedBox(height: screenSize.height * 0.03),
/// Terms
RichText(
textAlign: TextAlign.center,
text: TextSpan(
style: TextStyle(
color: Colors.grey[600],
fontSize: 12,
),
children: const [
TextSpan(text: "By continuing, you agree to the "),
TextSpan(
text: "Terms & Privacy Policy",
style: TextStyle(
color: Colors.blue,
fontWeight: FontWeight.bold,
),
),
],
),
),
SizedBox(height: screenSize.height * 0.02),
],
),
),
),
],
),
);
}
}