initial commit: push everything
This commit is contained in:
322
lib/views/onboardscreens/Creat_mpin.dart
Normal file
322
lib/views/onboardscreens/Creat_mpin.dart
Normal 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),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
343
lib/views/onboardscreens/Mpin.dart
Normal file
343
lib/views/onboardscreens/Mpin.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
339
lib/views/onboardscreens/Sign_in.dart
Normal file
339
lib/views/onboardscreens/Sign_in.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
395
lib/views/onboardscreens/otp_page.dart
Normal file
395
lib/views/onboardscreens/otp_page.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
208
lib/views/onboardscreens/signin_banner.dart
Normal file
208
lib/views/onboardscreens/signin_banner.dart
Normal 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),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
46
lib/views/onboardscreens/splashscreen.dart
Normal file
46
lib/views/onboardscreens/splashscreen.dart
Normal 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,)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user