marco.pms.mobileapp/lib/view/auth/otp_login_form.dart

233 lines
7.2 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:marco/helpers/widgets/my_button.dart';
import 'package:marco/helpers/widgets/my_spacing.dart';
import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/helpers/widgets/my_text_style.dart';
import 'package:marco/controller/auth/otp_controller.dart';
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
class OTPLoginScreen extends StatefulWidget {
const OTPLoginScreen({super.key});
@override
State<OTPLoginScreen> createState() => _OTPLoginScreenState();
}
class _OTPLoginScreenState extends State<OTPLoginScreen> with UIMixin {
late final OTPController controller;
final GlobalKey<FormState> _otpFormKey = GlobalKey<FormState>();
@override
void initState() {
controller = Get.put(OTPController());
super.initState();
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MyText.bodyMedium('OTP Verification', fontWeight: 600),
MySpacing.height(24),
Obx(() => !controller.isOTPSent.value
? Column(
children: [
_buildEmailInput(),
MySpacing.height(24),
_buildSendOTPButton(theme),
],
)
: Column(
children: [
_buildOTPSentInfo(),
MySpacing.height(12),
_buildOTPForm(theme),
MySpacing.height(12),
_buildResendOTPSection(theme),
MySpacing.height(12),
_buildVerifyOTPButton(theme),
],
)),
],
),
);
}
Widget _buildEmailInput() {
return TextFormField(
controller: controller.emailController,
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
labelText: 'Enter Email Address',
border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)),
),
onFieldSubmitted: (_) => controller.sendOTP(),
);
}
Widget _buildSendOTPButton(ThemeData theme) {
final isDisabled = controller.isSending.value || controller.timer.value > 0;
return Align(
alignment: Alignment.center,
child: MyButton.rounded(
onPressed: isDisabled ? null : controller.sendOTP,
elevation: 2,
padding: MySpacing.xy(24, 16),
borderRadiusAll: 10,
backgroundColor: isDisabled ? Colors.grey : contentTheme.primary,
child: controller.isSending.value
? SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: theme.colorScheme.onPrimary,
),
)
: MyText.labelMedium(
'Send OTP',
fontWeight: 600,
color: theme.colorScheme.onPrimary,
),
),
);
}
Widget _buildOTPSentInfo() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
Icon(Icons.email_outlined, color: Colors.redAccent, size: 24),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MyText.bodySmall('OTP sent to', fontWeight: 500),
MyText.bodySmall(controller.email.value,
fontWeight: 700, color: Colors.black),
],
),
),
TextButton.icon(
onPressed: controller.resetForChangeEmail,
icon: Icon(Icons.edit, size: 16, color: Colors.redAccent),
label: MyText.bodySmall('Change',
fontWeight: 600, color: Colors.redAccent),
),
],
),
);
}
Widget _buildOTPForm(ThemeData theme) {
return Form(
key: _otpFormKey,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(
4, (index) => _buildOTPBox(index: index, theme: theme)),
),
);
}
Widget _buildOTPBox({required int index, required ThemeData theme}) {
return Container(
margin: MySpacing.x(6),
width: 50,
child: TextFormField(
controller: controller.otpControllers[index],
focusNode: controller.focusNodes[index],
maxLength: 1,
textAlign: TextAlign.center,
style: MyTextStyle.titleLarge(fontWeight: 700),
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
onChanged: (value) => controller.onOTPChanged(value, index),
validator: (value) {
if (value == null || value.isEmpty) return '';
return null;
},
decoration: InputDecoration(
counterText: '',
filled: true,
fillColor: Colors.grey.shade100,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade400, width: 1),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: contentTheme.brandRed, width: 2),
),
),
),
);
}
Widget _buildResendOTPSection(ThemeData theme) {
if (controller.timer.value > 0) {
return MyText.bodySmall(
'Resend OTP in ${controller.timer.value}s',
color: theme.colorScheme.onBackground.withOpacity(0.6),
textAlign: TextAlign.center,
);
} else {
return Align(
alignment: Alignment.center,
child: TextButton(
onPressed:
controller.isResending.value ? null : controller.onResendOTP,
child: controller.isResending.value
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2))
: MyText.bodySmall('Resend OTP',
fontWeight: 600, color: Colors.blueAccent),
),
);
}
}
Widget _buildVerifyOTPButton(ThemeData theme) {
return Align(
alignment: Alignment.center,
child: MyButton.rounded(
onPressed: () {
if (_otpFormKey.currentState!.validate()) {
controller.verifyOTP();
} else {
showAppSnackbar(
title: "Error",
message: "Please enter all 4 digits of the OTP",
type: SnackbarType.error,
);
}
},
elevation: 2,
padding: MySpacing.xy(24, 16),
borderRadiusAll: 10,
backgroundColor: contentTheme.brandRed,
child: MyText.labelMedium(
'Verify OTP',
fontWeight: 600,
color: theme.colorScheme.onPrimary,
),
),
);
}
}