initial commit: push everything

This commit is contained in:
2026-07-06 20:27:53 +05:30
commit df4e044d74
329 changed files with 36620 additions and 0 deletions

View File

@@ -0,0 +1,322 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:nearle/views/helpers/constants/Colorconstants.dart';
import 'package:nearle/views/helpers/constants/Font_constant.dart';
import 'package:nearle/controllers/auth.dart';
import 'package:nearle/views/onboardscreens/Mpin.dart';
class CreateMpin extends GetResponsiveView {
CreateMpin({super.key});
@override
Widget builder() {
return const _CreateMpinBody();
}
}
class _CreateMpinBody extends StatefulWidget {
const _CreateMpinBody();
@override
State<_CreateMpinBody> createState() => _CreateMpinBodyState();
}
class _CreateMpinBodyState extends State<_CreateMpinBody> {
final AuthController _auth = Get.put(AuthController());
final List<TextEditingController> _newMpinControllers = List.generate(
4,
(_) => TextEditingController(),
);
final List<TextEditingController> _confirmMpinControllers = List.generate(
4,
(_) => TextEditingController(),
);
final List<FocusNode> _newFocusNodes = List.generate(4, (_) => FocusNode());
final List<FocusNode> _confirmFocusNodes = List.generate(
4,
(_) => FocusNode(),
);
bool isLoading = false;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
_newFocusNodes[0].requestFocus();
});
}
@override
void dispose() {
for (var c in [..._newMpinControllers, ..._confirmMpinControllers]) {
c.dispose();
}
for (var f in [..._newFocusNodes, ..._confirmFocusNodes]) {
f.dispose();
}
super.dispose();
}
void _onMpinChange(
String value,
int index,
List<TextEditingController> controllers,
List<FocusNode> nodes,
) {
if (value.isNotEmpty && index < 3) {
nodes[index + 1].requestFocus();
} else if (value.isEmpty && index > 0) {
nodes[index - 1].requestFocus();
}
final isGroupFilled = controllers.every((c) => c.text.isNotEmpty);
if (controllers == _newMpinControllers && isGroupFilled) {
_confirmFocusNodes[0].requestFocus();
}
if (controllers == _confirmMpinControllers && isGroupFilled) {
FocusScope.of(context).unfocus();
}
setState(() {});
}
String getMpin(List<TextEditingController> controllers) =>
controllers.map((c) => c.text).join();
bool get isMpinMatched =>
getMpin(_newMpinControllers) == getMpin(_confirmMpinControllers);
bool get isAllFilled => [
..._newMpinControllers,
..._confirmMpinControllers,
].every((c) => c.text.isNotEmpty);
@override
Widget build(BuildContext context) {
final screen = context.width < 600
? "mobile"
: context.width < 1100
? "tablet"
: "desktop";
final height = Get.height;
final width = Get.width;
// Adjust scale based on device
final scale = screen == "mobile"
? 1.0
: screen == "tablet"
? 1.3
: 1.6;
return Scaffold(
backgroundColor: Colors.white,
body: Stack(
children: [
SafeArea(
child: SingleChildScrollView(
padding: EdgeInsets.symmetric(horizontal: width * 0.06),
child: Column(
children: [
SizedBox(height: height * 0.04 * scale),
SizedBox(
height: height * 0.25 * scale,
width: width * 0.6,
child: Image.asset(
"assets/images/CreateMpin.png",
fit: BoxFit.contain,
),
),
SizedBox(height: height * 0.03 * scale),
Text(
"Create Your MPIN",
style: TextStyle(
fontSize: height * 0.04 * scale,
fontWeight: FontWeight.bold,
fontFamily: FontConstants.fontFamily,
color: ColorConstants.primaryColor,
),
),
SizedBox(height: height * 0.015 * scale),
Text(
"Enter a 4-digit MPIN and confirm it to secure your account.",
textAlign: TextAlign.center,
style: TextStyle(
fontSize: height * 0.02 * scale,
color: Colors.black54,
fontFamily: FontConstants.fontFamily,
),
),
SizedBox(height: height * 0.04 * scale),
_buildMpinField(
"Enter New MPIN",
_newMpinControllers,
_newFocusNodes,
scale,
),
SizedBox(height: height * 0.03 * scale),
_buildMpinField(
"Confirm MPIN",
_confirmMpinControllers,
_confirmFocusNodes,
scale,
),
],
),
),
),
Positioned(
top: height * 0.05,
left: width * 0.04,
child: InkWell(
onTap: () => Get.back(),
borderRadius: BorderRadius.circular(30),
child: Container(
padding: EdgeInsets.all(width * 0.02),
decoration: const BoxDecoration(
color: Colors.black12,
shape: BoxShape.circle,
),
child: Icon(
Icons.arrow_back,
color: Colors.black,
size: width * 0.06,
),
),
),
),
],
),
bottomNavigationBar: SafeArea(
child: Padding(
padding: EdgeInsets.all(width * 0.04),
child: SizedBox(
width: double.infinity,
height: height * 0.065 * scale,
child: ElevatedButton(
onPressed: isAllFilled && isMpinMatched && !isLoading
? () async {
setState(() => isLoading = true);
final newPin = getMpin(_newMpinControllers);
// Set PIN directly - user ID should already be available from login flow
final ok = await _auth.setPin(newPin);
setState(() => isLoading = false);
if (ok) {
// After setting PIN, go to verify PIN page to sign-in with new PIN
Get.to(() => Mpin());
} else {
Get.snackbar(
'Failed',
'Unable to set PIN. Please try again.',
);
}
}
: null,
style: ElevatedButton.styleFrom(
backgroundColor: ColorConstants.primaryColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(width * 0.03),
),
),
child: isLoading
? SizedBox(
height: height * 0.03,
width: height * 0.03,
child: const CircularProgressIndicator(
strokeWidth: 3,
color: Colors.white,
),
)
: Text(
"Continue",
style: TextStyle(
fontSize: height * 0.024 * scale,
fontWeight: FontWeight.bold,
color: Colors.white,
fontFamily: FontConstants.fontFamily,
),
),
),
),
),
),
);
}
Widget _buildMpinField(
String label,
List<TextEditingController> controllers,
List<FocusNode> focusNodes,
double scale,
) {
final height = Get.height;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(
fontSize: height * 0.02 * scale,
fontWeight: FontWeight.w600,
fontFamily: FontConstants.fontFamily,
),
),
SizedBox(height: height * 0.015 * scale),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: List.generate(4, (index) {
return SizedBox(
width: 50 * scale,
height: 55 * scale,
child: TextField(
controller: controllers[index],
focusNode: focusNodes[index],
textAlign: TextAlign.center,
obscureText: true,
maxLength: 1,
keyboardType: TextInputType.number,
style: TextStyle(
fontSize: height * 0.025 * scale,
fontWeight: FontWeight.bold,
fontFamily: FontConstants.fontFamily,
),
decoration: InputDecoration(
counterText: "",
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8 * scale),
borderSide: const BorderSide(color: Colors.grey),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8 * scale),
borderSide: const BorderSide(
color: ColorConstants.primaryColor,
width: 2,
),
),
),
onChanged: (val) =>
_onMpinChange(val, index, controllers, focusNodes),
),
);
}),
),
],
);
}
}

View File

@@ -0,0 +1,343 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:nearle/views/helpers/constants/Colorconstants.dart';
import 'package:nearle/views/helpers/constants/Font_constant.dart';
import 'package:nearle/views/onboardscreens/Sign_in.dart';
import 'package:nearle/views/onboardscreens/otp_page.dart';
import 'package:nearle/widget/Bottom_page.dart';
import 'package:nearle/controllers/auth.dart';
import 'package:nearle/views/onboardscreens/signin_banner.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter/services.dart';
class Mpin extends GetResponsiveView {
Mpin({super.key});
@override
Widget builder() {
return const _MpinView();
}
}
class _MpinView extends StatefulWidget {
const _MpinView();
@override
State<_MpinView> createState() => _MpinViewState();
}
class _MpinViewState extends State<_MpinView> {
final AuthController _auth = Get.put(AuthController());
final List<TextEditingController> _mpinControllers = List.generate(
4,
(_) => TextEditingController(),
);
final List<FocusNode> _focusNodes = List.generate(4, (_) => FocusNode());
bool isVerifying = false;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
_focusNodes[0].requestFocus();
_maybeShowMasterPinReminder();
});
}
@override
void dispose() {
for (var c in _mpinControllers) {
c.dispose();
}
for (var f in _focusNodes) {
f.dispose();
}
super.dispose();
}
void _onMpinChange(String value, int index) {
if (value.isNotEmpty && index < 3) {
_focusNodes[index + 1].requestFocus();
} else if (value.isEmpty && index > 0) {
_focusNodes[index - 1].requestFocus();
}
final filled = _mpinControllers.every((c) => c.text.isNotEmpty);
if (filled) {
FocusScope.of(context).unfocus();
_submitMpin(); // auto-verify when 4 digits are filled
}
setState(() {});
}
String getMpin() => _mpinControllers.map((c) => c.text).join();
bool get isMpinFilled => _mpinControllers.every((c) => c.text.isNotEmpty);
void _clearMpinAndFocus() {
for (final c in _mpinControllers) {
c.clear();
}
if (_focusNodes.isNotEmpty) {
FocusScope.of(context).requestFocus(_focusNodes[0]);
}
setState(() {});
}
Future<void> _submitMpin() async {
if (!mounted || isVerifying || !isMpinFilled) return;
// Register retry callback so AuthController bottom-sheet "Retry" button
// can clear MPIN boxes and bring back keyboard when PIN is wrong.
_auth.onPinRetry = _clearMpinAndFocus;
setState(() => isVerifying = true);
final mpin = getMpin();
final ok = await _auth.verifyPinWithServer(mpin);
if (!mounted) return;
setState(() => isVerifying = false);
if (ok) {
// ✅ Wait a moment to ensure onduty is saved, then read it
await Future.delayed(const Duration(milliseconds: 100));
final prefs = await SharedPreferences.getInstance();
final onduty = prefs.getInt('onduty') ?? 0;
debugPrint('[MPIN] After verification - onduty=$onduty');
// Navigate based on onduty value
if (onduty == 0) {
debugPrint('[MPIN] Navigating to Introscreen (onduty=0)');
Get.offAll(() => SigninBanner());
} else {
debugPrint('[MPIN] Navigating to BottomPage (onduty=1)');
Get.offAll(() => const BottomPage());
}
} else {
// Wrong MPIN: show alert/snackbar and keep user on MPIN screen
}
}
Future<void> _maybeShowMasterPinReminder() async {
try {
final prefs = await SharedPreferences.getInstance();
final forceMasterPin =
prefs.getBool(AuthController.forceMasterPinPrefKey) ?? false;
if (forceMasterPin) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Use ${AuthController.masterPinValue} as PIN to continue.',
),
duration: const Duration(seconds: 4),
behavior: SnackBarBehavior.floating,
),
);
}
} catch (_) {}
}
@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
final height = size.height;
final width = size.width;
double scale = 1.0;
if (Get.width < 380) scale = 0.9;
if (Get.width > 800) scale = 1.2;
return Scaffold(
backgroundColor: Colors.white,
body: Stack(
children: [
SafeArea(
child: SingleChildScrollView(
padding: EdgeInsets.symmetric(horizontal: width * 0.05),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(height: height * 0.04),
SizedBox(
height: height * 0.25 * scale,
width: width * 0.6,
child: Image.asset(
"assets/images/Mpin.png",
fit: BoxFit.contain,
),
),
SizedBox(height: height * 0.04),
Text(
"Enter Your MPIN",
style: TextStyle(
fontSize: height * 0.05 * scale,
fontWeight: FontWeight.bold,
color: ColorConstants.primaryColor,
fontFamily: FontConstants.fontFamily,
),
),
SizedBox(height: height * 0.01),
Text(
"Access your account securely",
textAlign: TextAlign.center,
style: TextStyle(
fontSize: height * 0.024 * scale,
color: Colors.black54,
fontFamily: FontConstants.fontFamily,
),
),
SizedBox(height: height * 0.06),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: List.generate(4, (index) {
final isFilled = _mpinControllers[index].text.isNotEmpty;
return SizedBox(
width: 50 * scale,
height: 55 * scale,
child: RawKeyboardListener(
focusNode: FocusNode(),
onKey: (event) {
if (event is RawKeyDownEvent &&
event.logicalKey ==
LogicalKeyboardKey.backspace &&
_mpinControllers[index].text.isEmpty &&
index > 0) {
_mpinControllers[index - 1].clear();
_focusNodes[index - 1].requestFocus();
setState(() {});
}
},
child: TextField(
controller: _mpinControllers[index],
focusNode: _focusNodes[index],
textAlign: TextAlign.center,
textAlignVertical: TextAlignVertical.center,
obscureText: true,
keyboardType: TextInputType.number,
maxLength: 1,
style: TextStyle(
fontSize: height * 0.028 * scale,
fontWeight: FontWeight.bold,
color: isFilled ? Colors.white : Colors.black,
),
decoration: InputDecoration(
counterText: "",
contentPadding: EdgeInsets.zero,
filled: true,
fillColor: isFilled
? ColorConstants.primaryColor
: Colors.transparent,
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8 * scale),
borderSide: const BorderSide(
color: Colors.grey,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8 * scale),
borderSide: const BorderSide(
color: Color(0xFF662582),
width: 2,
),
),
),
onChanged: (value) => _onMpinChange(value, index),
),
),
);
}),
),
SizedBox(height: height * 0.02),
// Retry + Forget Pin row
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
TextButton(
onPressed: () {
// Clear all MPIN boxes and focus first, bringing up keyboard
for (final c in _mpinControllers) {
c.clear();
}
if (_focusNodes.isNotEmpty) {
FocusScope.of(context).requestFocus(_focusNodes[0]);
}
setState(() {});
},
child: Text(
"",
style: TextStyle(
fontSize: height * 0.022 * scale,
fontWeight: FontWeight.w600,
color: Colors.black87,
fontFamily: FontConstants.fontFamily,
),
),
),
InkWell(
onTap: () async {
await _auth.sendOtp();
Get.to(OtpPage());
},
child: Transform.translate(
offset: Offset(-18, 0),
child: Text(
"Forget Pin?",
style: TextStyle(
fontSize: height * 0.025 * scale,
fontWeight: FontWeight.bold,
color: ColorConstants.primaryColor,
fontFamily: FontConstants.fontFamily,
decoration: TextDecoration.underline,
),
),
),
),
],
),
],
),
),
),
Positioned(
top: height * 0.05,
left: width * 0.04,
child: InkWell(
onTap: () {
Get.to(SignIn());
},
borderRadius: BorderRadius.circular(30),
child: Container(
padding: EdgeInsets.all(width * 0.02),
decoration: const BoxDecoration(
color: Colors.black12,
shape: BoxShape.circle,
),
child: Icon(
Icons.arrow_back,
color: Colors.black,
size: width * 0.06,
),
),
),
),
],
),
);
}
}

View File

@@ -0,0 +1,339 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:nearle/views/helpers/constants/Colorconstants.dart';
import 'package:nearle/views/helpers/constants/Font_constant.dart';
import 'package:nearle/views/onboardscreens/otp_page.dart';
import 'package:nearle/views/onboardscreens/Mpin.dart';
import 'package:nearle/controllers/auth.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:flutter/gestures.dart';
import 'package:nearle/providers/notifications/notificationservce.dart';
class LoginController extends GetxController {
var isChecked = true.obs;
final AuthController auth = Get.put(AuthController());
}
class SignIn extends StatefulWidget {
const SignIn({super.key});
@override
State<SignIn> createState() => _SignInState();
}
class _SignInState extends State<SignIn> {
final LoginController controller = Get.put(LoginController());
final TextEditingController phoneController = TextEditingController();
final FocusNode _phoneFocusNode = FocusNode();
bool isPhoneValid = false;
bool isLoading = false;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
FocusScope.of(context).requestFocus(_phoneFocusNode);
// Request notification permission the first time Sign In screen is shown
NotificationServce.initialize(context);
});
}
bool _validatePhoneNumber(String number) {
final RegExp regExp = RegExp(r'^[6-9]\d{9}$');
return regExp.hasMatch(number);
}
@override
void dispose() {
_phoneFocusNode.dispose();
phoneController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
final height = size.height;
final width = size.width;
double scale = 1.0;
if (width < 380) scale = 0.9;
if (width > 800) scale = 1.2;
return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle.dark.copyWith(
statusBarColor: Colors.white,
statusBarIconBrightness: Brightness.dark,
statusBarBrightness: Brightness.light,
systemNavigationBarColor: Colors.white,
),
child: Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
top: false,
left: true,
right: true,
bottom: true,
child: SingleChildScrollView(
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: width * 0.05,
vertical: height * 0.02,
),
child: GetBuilder<LoginController>(
builder: (_) => Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(height: height * 0.04),
// Restore hero image at the top like before
Image.asset(
'assets/images/Nearle Bike.png',
height: height * 0.28,
fit: BoxFit.contain,
),
SizedBox(height: height * 0.03),
Text(
"Sign In",
style: TextStyle(
fontSize: height * 0.05 * scale,
fontWeight: FontWeight.bold,
color: ColorConstants.primaryColor,
fontFamily: FontConstants.fontFamily,
),
),
SizedBox(height: height * 0.02),
Text(
"Enter your mobile number to get started.",
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.black54,
fontSize: FontConstants.xLarge(context),
fontFamily: FontConstants.fontFamily,
fontWeight: FontWeight.w500,
),
),
SizedBox(height: height * 0.05),
// Phone number field
SizedBox(
height: height * 0.07,
width: width * 0.9,
child: TextField(
controller: phoneController,
focusNode: _phoneFocusNode,
keyboardType: TextInputType.number,
maxLength: 10,
style: TextStyle(
fontSize: FontConstants.large(context),
fontWeight: FontWeight.w500,
color: Colors.black,
),
onChanged: (value) {
setState(() {
isPhoneValid = _validatePhoneNumber(value);
});
if (value.length == 10 && isPhoneValid) {
FocusScope.of(context).unfocus();
}
},
decoration: InputDecoration(
counterText: '',
labelText: 'Enter mobile number',
labelStyle: TextStyle(
color: Colors.grey,
fontSize: width * 0.04,
fontFamily: FontConstants.fontFamily,
),
prefixIcon: Padding(
padding: EdgeInsets.symmetric(
horizontal: width * 0.02,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Image.asset(
"assets/images/in.png",
height: height * 0.045,
width: width * 0.09,
),
SizedBox(width: width * 0.01),
Text(
"+91",
style: TextStyle(
fontSize: FontConstants.large(context),
fontFamily: FontConstants.fontFamily,
),
),
],
),
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(
color: Color(0xFF662582),
width: 1.5,
),
),
),
),
),
// Validation message
if (!isPhoneValid && phoneController.text.isNotEmpty)
Padding(
padding: EdgeInsets.only(
left: width * 0.02,
top: height * 0.005,
),
child: Text(
'Enter a valid 10-digit mobile number',
style: TextStyle(
color: Colors.red.shade700,
fontSize: width * 0.03,
),
),
),
SizedBox(height: height * 0.02),
// Terms text with clickable T&C and Privacy Policy
RichText(
textAlign: TextAlign.center,
text: TextSpan(
style: TextStyle(
fontSize: 15,
fontFamily: FontConstants.fontFamily,
fontWeight: FontWeight.w500,
color: Colors.black87,
),
children: [
const TextSpan(text: 'By continuing, you agree to '),
TextSpan(
text: 'T&C',
style: const TextStyle(
color: Colors.blue,
decoration: TextDecoration.none,
),
recognizer: TapGestureRecognizer()
..onTap = () async {
final uri = Uri.parse(
'https://nearle.in/terms',
);
final ok = await launchUrl(
uri,
mode: LaunchMode.externalApplication,
);
if (!ok) {
await launchUrl(
uri,
mode: LaunchMode.inAppWebView,
);
}
},
),
const TextSpan(text: ' and '),
TextSpan(
text: 'Privacy Policy',
style: const TextStyle(
color: Colors.blue,
decoration: TextDecoration.none,
),
recognizer: TapGestureRecognizer()
..onTap = () async {
final uri = Uri.parse(
'https://nearle.in/privacy',
);
final ok = await launchUrl(
uri,
mode: LaunchMode.externalApplication,
);
if (!ok) {
await launchUrl(
uri,
mode: LaunchMode.inAppWebView,
);
}
},
),
],
),
),
SizedBox(height: height * 0.02),
],
),
),
),
),
),
// Bottom Button
bottomNavigationBar: SafeArea(
child: Padding(
padding: EdgeInsets.all(width * 0.04),
child: SizedBox(
height: height * 0.065,
width: double.infinity,
child: ElevatedButton(
onPressed: isPhoneValid && !isLoading
? () async {
setState(() => isLoading = true);
final decision = await controller.auth.precheckPhone(
phoneController.text,
);
setState(() => isLoading = false);
if (decision == AuthNext.notRegistered) {
return;
} else if (decision == AuthNext.otp) {
await controller.auth.sendOtp(phoneController.text);
Get.to(OtpPage());
} else if (decision == AuthNext.verifyPin) {
Get.to(() => Mpin());
} else {
Get.snackbar(
'Error',
'Unable to proceed. Please try again.',
);
}
}
: null,
style: ElevatedButton.styleFrom(
backgroundColor: isPhoneValid
? const Color(0xFF662582)
: Colors.grey.shade400,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
padding: EdgeInsets.symmetric(vertical: height * 0.015),
),
child: isLoading
? SizedBox(
height: height * 0.035,
width: height * 0.035,
child: const CircularProgressIndicator(
color: Colors.white,
strokeWidth: 3,
),
)
: Text(
'Next',
style: TextStyle(
color: Colors.white,
fontSize: width * 0.06,
fontWeight: FontWeight.bold,
),
),
),
),
),
),
),
);
}
}

View File

@@ -0,0 +1,395 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:nearle/views/helpers/constants/Colorconstants.dart';
import 'package:nearle/views/helpers/constants/Font_constant.dart';
import 'package:nearle/views/onboardscreens/Sign_in.dart';
import 'package:nearle/controllers/auth.dart';
import 'package:nearle/views/onboardscreens/Creat_mpin.dart';
import 'package:sms_autofill/sms_autofill.dart';
import 'package:shared_preferences/shared_preferences.dart';
class OtpPage extends GetResponsiveView {
OtpPage({super.key});
@override
Widget? phone() => _OtpPageLayout();
@override
Widget? tablet() => _OtpPageLayout(scale: 1.2);
@override
Widget? desktop() => _OtpPageLayout(scale: 1.3);
}
class _OtpPageLayout extends StatefulWidget {
final double scale;
const _OtpPageLayout({this.scale = 1.0});
@override
State<_OtpPageLayout> createState() => _OtpPageLayoutState();
}
class _OtpPageLayoutState extends State<_OtpPageLayout> with CodeAutoFill {
final AuthController _auth = Get.put(AuthController());
final List<TextEditingController> _otpControllers = List.generate(
6,
(_) => TextEditingController(),
);
final List<FocusNode> _focusNodes = List.generate(6, (_) => FocusNode());
bool isVerifying = false;
int _secondsRemaining = 60;
Timer? _timer;
String _appSignature = '';
// Consent fallback removed due to plugin AGP incompatibility
int _smsDefaultProvider = 0; // 0 = normal, 1 = passkey provider
int? _smsPassKey; // when provider is passkey based
@override
void initState() {
super.initState();
_startTimer();
_listenForOtp();
_loadOtpProviderPrefs();
// Ensure cursor starts in first box
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted && _focusNodes.isNotEmpty) {
_focusNodes[0].requestFocus();
}
});
}
void _startTimer() {
_secondsRemaining = 60;
_timer?.cancel();
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (_secondsRemaining > 0) {
setState(() => _secondsRemaining--);
} else {
timer.cancel();
}
});
}
Future<void> _resendOtp() async {
setState(() => _secondsRemaining = 60);
_timer?.cancel();
_startTimer();
await _auth.sendOtp();
}
Future<void> _listenForOtp() async {
try {
await SmsAutoFill().unregisterListener();
listenForCode();
// Fetch and cache app hash for SMS retriever compatibility
try {
final sig = await SmsAutoFill().getAppSignature;
if (sig.isNotEmpty) {
_appSignature = sig;
// Helpful for integrating with SMS provider templates
debugPrint('[OTP] App signature hash: $_appSignature');
}
} catch (_) {}
} catch (_) {}
// Consent fallback temporarily disabled; use SMS Retriever with app hash
}
Future<void> _loadOtpProviderPrefs() async {
try {
final prefs = await SharedPreferences.getInstance();
_smsDefaultProvider = prefs.getInt('smsDefaultProvider') ?? 0;
_smsPassKey = prefs.getInt('smsPassKey');
if (_smsDefaultProvider == 1 && _smsPassKey != null) {
final passKeyStr = _smsPassKey!.toString().padLeft(6, '0');
// Prefill only if fields are empty
if (mounted && !_otpControllers.any((c) => c.text.isNotEmpty)) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
for (int i = 0; i < 6 && i < passKeyStr.length; i++) {
_otpControllers[i].text = passKeyStr[i];
}
setState(() {});
});
}
}
} catch (_) {}
}
@override
void codeUpdated() {
final received = code ?? '';
if (received.isNotEmpty) {
final digits = received.replaceAll(RegExp(r'\D'), '');
if (digits.length >= 6) {
final otp = digits.substring(0, 6);
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
for (int i = 0; i < 6; i++) {
_otpControllers[i].text = otp[i];
}
setState(() {});
// hide keyboard on autofill and verify with delay before navigation
FocusScope.of(context).unfocus();
_autoVerify(delayBeforeNav: true);
});
}
}
}
Future<void> _autoVerify({bool delayBeforeNav = false}) async {
if (!mounted) return;
if (!_otpControllers.every((c) => c.text.isNotEmpty)) return;
if (isVerifying) return;
setState(() => isVerifying = true);
final entered = getOtp();
bool ok = false;
// Accept provider passkey as valid OTP when enabled
if (_smsDefaultProvider == 1 &&
_smsPassKey != null &&
entered == _smsPassKey!.toString().padLeft(6, '0')) {
ok = true;
} else {
ok = await _auth.verifyOtp(entered);
}
setState(() => isVerifying = false);
if (ok) {
if (delayBeforeNav) {
await Future.delayed(const Duration(seconds: 3));
}
Get.to(() => CreateMpin());
}
}
@override
void dispose() {
for (final controller in _otpControllers) {
controller.dispose();
}
for (final node in _focusNodes) {
node.dispose();
}
_timer?.cancel();
try {
cancel();
} catch (_) {}
super.dispose();
}
void _onOtpChange(String value, int index) {
if (value.isNotEmpty && index < 5) {
_focusNodes[index + 1].requestFocus();
} else if (value.isNotEmpty && index == 5) {
FocusScope.of(context).unfocus();
}
if (value.isEmpty && index > 0) {
// backspace: jump focus back
_focusNodes[index - 1].requestFocus();
_otpControllers[index - 1].selection = TextSelection(
baseOffset: 0,
extentOffset: _otpControllers[index - 1].text.length,
);
}
setState(() {});
}
String getOtp() => _otpControllers.map((e) => e.text).join();
bool get isOtpFilled => _otpControllers.every((c) => c.text.isNotEmpty);
@override
Widget build(BuildContext context) {
final scale = widget.scale;
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: Stack(
children: [
Padding(
padding: EdgeInsets.symmetric(
horizontal: 20 * scale,
vertical: 20 * scale,
),
child: SingleChildScrollView(
child: Column(
children: [
SizedBox(height: 60 * scale),
Image.asset(
'assets/images/verify.png',
fit: BoxFit.contain,
height: 200 * scale,
),
SizedBox(height: 24 * scale),
Text(
"Verify OTP",
style: TextStyle(
fontSize: 28 * scale,
fontWeight: FontWeight.bold,
fontFamily: FontConstants.fontFamily,
color: ColorConstants.primaryColor,
),
),
SizedBox(height: 24 * scale),
Text(
"Enter the 6-digit code sent to your number",
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.black54,
fontSize: 19 * scale,
fontFamily: FontConstants.fontFamily,
),
),
SizedBox(height: 40 * scale),
// OTP Fields (6 boxes)
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: List.generate(6, (index) {
final isFilled = _otpControllers[index].text.isNotEmpty;
return SizedBox(
width: 45 * scale,
height: 50 * scale,
child: TextField(
controller: _otpControllers[index],
focusNode: _focusNodes[index],
autofocus: index == 0,
keyboardType: TextInputType.number,
textAlign: TextAlign.center,
maxLength: 1,
style: TextStyle(
fontSize: 17 * scale,
fontWeight: FontWeight.bold,
color: isFilled ? Colors.white : Colors.black,
),
decoration: InputDecoration(
counterText: '',
filled: true,
fillColor: isFilled
? ColorConstants.primaryColor
: Colors.transparent,
enabledBorder: OutlineInputBorder(
borderSide: const BorderSide(
color: Colors.grey,
),
borderRadius: BorderRadius.circular(8 * scale),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: ColorConstants.primaryColor,
width: 2,
),
borderRadius: BorderRadius.circular(8 * scale),
),
),
onChanged: (value) => _onOtpChange(value, index),
),
);
}),
),
SizedBox(height: 16 * scale),
// Resend OTP
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextButton(
onPressed: _secondsRemaining == 0
? () async {
await _resendOtp();
}
: null,
child: Transform.translate(
offset: Offset(-10, 0),
child: Text(
_secondsRemaining == 0
? "Resend OTP"
: "Resend in 00:${_secondsRemaining.toString().padLeft(2, '0')}",
style: TextStyle(
fontSize: 18 * scale,
color: _secondsRemaining == 0
? ColorConstants.primaryColor
: Colors.black,
fontFamily: FontConstants.fontFamily,
),
),
),
),
],
),
],
),
),
),
// Back button
Positioned(
top: 40 * scale,
left: 16 * scale,
child: InkWell(
onTap: () => Get.to(() => SignIn()),
borderRadius: BorderRadius.circular(30 * scale),
child: Container(
padding: EdgeInsets.all(8 * scale),
decoration: BoxDecoration(
color: Colors.black12,
shape: BoxShape.circle,
),
child: Icon(
Icons.arrow_back,
color: Colors.black,
size: 28 * scale,
),
),
),
),
],
),
),
bottomNavigationBar: SafeArea(
child: Padding(
padding: EdgeInsets.all(16 * scale),
child: SizedBox(
height: 55 * scale,
width: double.infinity,
child: ElevatedButton(
onPressed: isOtpFilled && !isVerifying
? () async {
// Reuse common verification flow (same as auto-verify)
await _autoVerify();
}
: null,
style: ElevatedButton.styleFrom(
backgroundColor: isOtpFilled
? ColorConstants.primaryColor
: Colors.grey.shade400,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10 * scale),
),
),
child: isVerifying
? SizedBox(
height: 30 * scale,
width: 30 * scale,
child: const CircularProgressIndicator(
color: Colors.white,
strokeWidth: 3,
),
)
: Text(
"Verify",
style: TextStyle(
fontSize: 21 * scale,
fontWeight: FontWeight.bold,
color: Colors.white,
fontFamily: FontConstants.fontFamily,
),
),
),
),
),
),
);
}
}

View File

@@ -0,0 +1,208 @@
import 'package:flutter/material.dart';
import 'package:slider_button_lite/feature/presentation/slider_button/slider.dart';
import 'package:slider_button_lite/feature/presentation/slider_button/slider_button_prop.dart';
import 'package:get/get.dart';
import 'package:nearle/controllers/riderlog.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:nearle/widget/Bottom_page.dart';
class SigninBanner extends StatefulWidget {
const SigninBanner({super.key});
@override
State<SigninBanner> createState() => _SigninBannerState();
}
class _SigninBannerState extends State<SigninBanner> {
String _shiftText = 'Your shift: -';
@override
void initState() {
super.initState();
// ✅ CRITICAL: Load shift info in background, don't block UI
_loadShiftInfo();
}
Future<void> _loadShiftInfo() async {
try {
final prefs = await SharedPreferences.getInstance();
if (mounted) {
setState(() {
final s = (prefs.getString('starttime') ?? '').trim();
final e = (prefs.getString('endtime') ?? '').trim();
_shiftText = (s.isEmpty || e.isEmpty)
? 'Your shift: -'
: 'Your shift: $s $e';
});
}
} catch (e) {
// Keep default text on error
}
}
@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
final width = size.width;
final height = size.height;
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.white,
elevation: 0,
scrolledUnderElevation: 0,
surfaceTintColor: Colors.transparent,
automaticallyImplyLeading: false,
),
backgroundColor: Colors.white,
body: SafeArea(
child: Center(
child: SingleChildScrollView(
padding: EdgeInsets.symmetric(horizontal: width * 0.01),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(height: height * 0.02),
Text(
"Welcome Back!",
style: TextStyle(
fontSize: width * 0.07,
fontWeight: FontWeight.bold,
color: const Color(0xFF6A1B9A),
),
textAlign: TextAlign.center,
),
SizedBox(height: height * 0.01),
Text(
"Start your ride and make today amazing!",
style: TextStyle(
fontSize: width * 0.04,
color: Colors.grey[600],
),
textAlign: TextAlign.center,
),
SizedBox(height: height * 0.04),
CircleAvatar(
radius: width * 0.14,
backgroundColor: const Color(0xFFEDE7F6),
child: Icon(
Icons.person,
color: const Color(0xFF6A1B9A),
size: width * 0.12,
),
),
SizedBox(height: height * 0.015),
// ✅ CRITICAL: Show shift text immediately (no FutureBuilder blocking)
Text(
_shiftText,
style: TextStyle(
color: Colors.grey[700],
fontSize: width * 0.04,
fontWeight: FontWeight.w500,
),
),
SizedBox(height: height * 0.04),
Image.asset('assets/images/signin_banner.png'),
SizedBox(height: height * 0.05),
LayoutBuilder(
builder: (context, constraints) {
final sliderWidth = constraints.maxWidth;
return Padding(
padding: const EdgeInsets.all(8.0),
child: SliderButton(
properties: SliderButtonProperties(
height: height * 0.07,
width: sliderWidth,
buttonSize: height * 0.065,
disable: false,
isLoading: false,
backgroundColor: const Color(0xFF6A1B9A),
disableButtonColor: const Color(0xFFCCCCDD),
dismissThresholds: 0.9,
action: () async {
try {
final rlc = Get.find<RiderLogController>();
// Ensure any previous break is ended when coming online
debugPrint(
'[SIGNIN_BANNER] Ending break before going online',
);
final breakEnded = await rlc
.endBreakAuto()
.timeout(
const Duration(seconds: 12),
onTimeout: () => false,
);
debugPrint(
'[SIGNIN_BANNER] endBreakAuto -> $breakEnded',
);
// Set rider ON duty (onduty = 1)
final ok = await rlc.setOnDuty(true);
if (ok && context.mounted) {
Get.offAll(() => const BottomPage());
}
// Show snackbar if still on this screen (unlikely after navigation)
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
ok
? "You're now on duty"
: "Failed to update status",
),
backgroundColor: ok
? Colors.deepPurple
: Colors.red,
),
);
}
} catch (e) {
debugPrint(
'[SIGNIN_BANNER] Error updating status: $e',
);
}
return false;
},
label: Text(
'Slide to Start',
style: TextStyle(
fontSize: width * 0.05,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
alignLabel: Alignment.center,
icon: ClipOval(
child: Material(
color: Colors.white,
child: SizedBox(
width: height * 0.065,
height: height * 0.065,
child: const Icon(
Icons.arrow_forward_ios_outlined,
color: Color(0xFF6A1B9A),
),
),
),
),
),
),
);
},
),
SizedBox(height: height * 0.04),
// Add extra bottom padding for devices with navigation bars
SizedBox(height: MediaQuery.of(context).padding.bottom),
],
),
),
),
),
);
}
}

View File

@@ -0,0 +1,46 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'dart:async';
import 'package:nearle/views/helpers/constants/Colorconstants.dart';
import 'package:nearle/views/introscreens/introscreen.dart';
class Splashscreen extends StatefulWidget {
const Splashscreen({super.key});
@override
State<Splashscreen> createState() => _SplashscreenState();
}
class _SplashscreenState extends State<Splashscreen> {
@override
void initState() {
super.initState();
Timer(const Duration(seconds: 3), () {
Get.to(() => Introscreen());
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: ColorConstants.secondaryColor,
elevation: 0,
),
backgroundColor: ColorConstants.secondaryColor,
body: SafeArea(
child: Column(
children: [
SizedBox(height: 230,),
Center(child: Image.asset("assets/images/splashimg.png",fit: BoxFit.contain,height:180,width: 180,)),
],
),
),
);
}
}