first commit
This commit is contained in:
152
lib/view/authentication/app_update_view.dart
Normal file
152
lib/view/authentication/app_update_view.dart
Normal 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(
|
||||
// "We’ve 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,
|
||||
// ),
|
||||
// ],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
591
lib/view/authentication/costomer_create_view.dart
Normal file
591
lib/view/authentication/costomer_create_view.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
443
lib/view/authentication/login_view.dart
Normal file
443
lib/view/authentication/login_view.dart
Normal 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),
|
||||
);
|
||||
}
|
||||
}
|
||||
69
lib/view/authentication/map_view.dart
Normal file
69
lib/view/authentication/map_view.dart
Normal 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;
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
331
lib/view/authentication/verification_view.dart
Normal file
331
lib/view/authentication/verification_view.dart
Normal 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(
|
||||
"Didn’t 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),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user