Feature_MPIN_OTP #46
@ -2,23 +2,21 @@ import 'dart:async';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||||||
|
import 'package:marco/helpers/services/auth_service.dart';
|
||||||
|
import 'package:marco/helpers/services/storage/local_storage.dart';
|
||||||
class OTPController extends GetxController {
|
class OTPController extends GetxController {
|
||||||
final formKey = GlobalKey<FormState>();
|
final formKey = GlobalKey<FormState>();
|
||||||
|
|
||||||
// Observables
|
final RxString email = ''.obs;
|
||||||
final RxString phoneNumber = ''.obs;
|
|
||||||
final RxBool isOTPSent = false.obs;
|
final RxBool isOTPSent = false.obs;
|
||||||
|
|
||||||
final RxBool isSending = false.obs;
|
final RxBool isSending = false.obs;
|
||||||
final RxBool isResending = false.obs;
|
final RxBool isResending = false.obs;
|
||||||
|
|
||||||
final RxInt timer = 0.obs;
|
final RxInt timer = 0.obs;
|
||||||
Timer? _countdownTimer;
|
Timer? _countdownTimer;
|
||||||
|
|
||||||
// Text controllers and focus nodes
|
final TextEditingController emailController = TextEditingController();
|
||||||
final TextEditingController phoneController = TextEditingController();
|
final List<TextEditingController> otpControllers =
|
||||||
final List<TextEditingController> otpControllers = List.generate(4, (_) => TextEditingController());
|
List.generate(4, (_) => TextEditingController());
|
||||||
final List<FocusNode> focusNodes = List.generate(4, (_) => FocusNode());
|
final List<FocusNode> focusNodes = List.generate(4, (_) => FocusNode());
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -30,7 +28,7 @@ class OTPController extends GetxController {
|
|||||||
@override
|
@override
|
||||||
void onClose() {
|
void onClose() {
|
||||||
_countdownTimer?.cancel();
|
_countdownTimer?.cancel();
|
||||||
phoneController.dispose();
|
emailController.dispose();
|
||||||
for (final controller in otpControllers) {
|
for (final controller in otpControllers) {
|
||||||
controller.dispose();
|
controller.dispose();
|
||||||
}
|
}
|
||||||
@ -40,19 +38,28 @@ class OTPController extends GetxController {
|
|||||||
super.onClose();
|
super.onClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<bool> _sendOTP(String phone) async {
|
Future<bool> _sendOTP(String email) async {
|
||||||
await Future.delayed(const Duration(seconds: 2));
|
final result = await AuthService.generateOtp(email);
|
||||||
debugPrint('Sending OTP to $phone');
|
if (result == null) {
|
||||||
return true;
|
debugPrint('OTP sent to $email');
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
showAppSnackbar(
|
||||||
|
title: "Error",
|
||||||
|
message: result['error'] ?? "Failed to send OTP",
|
||||||
|
type: SnackbarType.error,
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> sendOTP() async {
|
Future<void> sendOTP() async {
|
||||||
final phone = phoneController.text.trim();
|
final userEmail = emailController.text.trim();
|
||||||
|
|
||||||
if (!_validatePhone(phone)) {
|
if (!_validateEmail(userEmail)) {
|
||||||
showAppSnackbar(
|
showAppSnackbar(
|
||||||
title: "Error",
|
title: "Error",
|
||||||
message: "Please enter a valid 10-digit mobile number",
|
message: "Please enter a valid email address",
|
||||||
type: SnackbarType.error,
|
type: SnackbarType.error,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
@ -61,18 +68,12 @@ class OTPController extends GetxController {
|
|||||||
if (isSending.value) return;
|
if (isSending.value) return;
|
||||||
isSending.value = true;
|
isSending.value = true;
|
||||||
|
|
||||||
final success = await _sendOTP(phone);
|
final success = await _sendOTP(userEmail);
|
||||||
if (success) {
|
if (success) {
|
||||||
phoneNumber.value = phone;
|
email.value = userEmail;
|
||||||
isOTPSent.value = true;
|
isOTPSent.value = true;
|
||||||
_startTimer();
|
_startTimer();
|
||||||
_clearOTPFields();
|
_clearOTPFields();
|
||||||
} else {
|
|
||||||
showAppSnackbar(
|
|
||||||
title: "Error",
|
|
||||||
message: "Failed to send OTP. Please try again.",
|
|
||||||
type: SnackbarType.error,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isSending.value = false;
|
isSending.value = false;
|
||||||
@ -84,15 +85,9 @@ class OTPController extends GetxController {
|
|||||||
|
|
||||||
_clearOTPFields();
|
_clearOTPFields();
|
||||||
|
|
||||||
final success = await _sendOTP(phoneNumber.value);
|
final success = await _sendOTP(email.value);
|
||||||
if (success) {
|
if (success) {
|
||||||
_startTimer();
|
_startTimer();
|
||||||
} else {
|
|
||||||
showAppSnackbar(
|
|
||||||
title: "Error",
|
|
||||||
message: "Failed to resend OTP. Please try again.",
|
|
||||||
type: SnackbarType.error,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isResending.value = false;
|
isResending.value = false;
|
||||||
@ -112,19 +107,35 @@ class OTPController extends GetxController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void verifyOTP() {
|
Future<void> verifyOTP() async {
|
||||||
final enteredOTP = otpControllers.map((c) => c.text).join();
|
final enteredOTP = otpControllers.map((c) => c.text).join();
|
||||||
if (enteredOTP.length != otpControllers.length || enteredOTP.contains('')) {
|
|
||||||
|
final result = await AuthService.verifyOtp(
|
||||||
|
email: email.value,
|
||||||
|
otp: enteredOTP,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result == null) {
|
||||||
|
showAppSnackbar(
|
||||||
|
title: "Success",
|
||||||
|
message: "OTP verified successfully",
|
||||||
|
type: SnackbarType.success,
|
||||||
|
);
|
||||||
|
final bool isMpinEnabled = LocalStorage.getIsMpin();
|
||||||
|
print('MPIN Enabled? $isMpinEnabled');
|
||||||
|
|
||||||
|
if (isMpinEnabled) {
|
||||||
|
Get.toNamed('/auth/mpin-auth');
|
||||||
|
} else {
|
||||||
|
Get.offAllNamed('/home');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
showAppSnackbar(
|
showAppSnackbar(
|
||||||
title: "Error",
|
title: "Error",
|
||||||
message: "Please enter the complete ${otpControllers.length}-digit OTP",
|
message: result['error'] ?? "Failed to verify OTP",
|
||||||
type: SnackbarType.error,
|
type: SnackbarType.error,
|
||||||
);
|
);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Add your OTP verification logic
|
|
||||||
debugPrint('Verifying OTP: $enteredOTP');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _clearOTPFields() {
|
void _clearOTPFields() {
|
||||||
@ -146,10 +157,10 @@ class OTPController extends GetxController {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void resetForChangeNumber() {
|
void resetForChangeEmail() {
|
||||||
isOTPSent.value = false;
|
isOTPSent.value = false;
|
||||||
phoneNumber.value = '';
|
email.value = '';
|
||||||
phoneController.clear();
|
emailController.clear();
|
||||||
_clearOTPFields();
|
_clearOTPFields();
|
||||||
|
|
||||||
timer.value = 0;
|
timer.value = 0;
|
||||||
@ -161,8 +172,8 @@ class OTPController extends GetxController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _validatePhone(String phone) {
|
bool _validateEmail(String email) {
|
||||||
final regex = RegExp(r'^\d{10}$');
|
final regex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,}$');
|
||||||
return regex.hasMatch(phone);
|
return regex.hasMatch(email);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,31 +25,7 @@ class AuthService {
|
|||||||
|
|
||||||
final responseData = jsonDecode(response.body);
|
final responseData = jsonDecode(response.body);
|
||||||
if (response.statusCode == 200 && responseData['data'] != null) {
|
if (response.statusCode == 200 && responseData['data'] != null) {
|
||||||
final jwtToken = responseData['data']['token'];
|
await _handleLoginSuccess(responseData['data']);
|
||||||
final refreshToken = responseData['data']['refreshToken'];
|
|
||||||
final mpinToken = responseData['data']['mpinToken'];
|
|
||||||
|
|
||||||
// Log the tokens using the logger
|
|
||||||
logger.i("JWT Token: $jwtToken");
|
|
||||||
if (refreshToken != null) logger.i("Refresh Token: $refreshToken");
|
|
||||||
if (mpinToken != null) logger.i("MPIN Token: $mpinToken");
|
|
||||||
|
|
||||||
await LocalStorage.setJwtToken(jwtToken);
|
|
||||||
await LocalStorage.setLoggedInUser(true);
|
|
||||||
|
|
||||||
if (refreshToken != null) {
|
|
||||||
await LocalStorage.setRefreshToken(refreshToken);
|
|
||||||
}
|
|
||||||
if (mpinToken != null && mpinToken.isNotEmpty) {
|
|
||||||
await LocalStorage.setMpinToken(mpinToken);
|
|
||||||
await LocalStorage.setIsMpin(true);
|
|
||||||
} else {
|
|
||||||
await LocalStorage.setIsMpin(false);
|
|
||||||
await LocalStorage.removeMpinToken();
|
|
||||||
}
|
|
||||||
|
|
||||||
Get.put(PermissionController());
|
|
||||||
isLoggedIn = true;
|
|
||||||
return null;
|
return null;
|
||||||
} else if (response.statusCode == 401) {
|
} else if (response.statusCode == 401) {
|
||||||
return {"password": "Invalid email or password"};
|
return {"password": "Invalid email or password"};
|
||||||
@ -300,4 +276,98 @@ class AuthService {
|
|||||||
return {"error": "Network error. Please check your connection."};
|
return {"error": "Network error. Please check your connection."};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generate OTP API
|
||||||
|
static Future<Map<String, String>?> generateOtp(String email) async {
|
||||||
|
final requestBody = {"email": email};
|
||||||
|
|
||||||
|
logger.i("Sending generate OTP request: $requestBody");
|
||||||
|
|
||||||
|
try {
|
||||||
|
final response = await http.post(
|
||||||
|
Uri.parse("$_baseUrl/auth/send-otp"),
|
||||||
|
headers: _headers,
|
||||||
|
body: jsonEncode(requestBody),
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.i(
|
||||||
|
"Generate OTP API response (${response.statusCode}): ${response.body}");
|
||||||
|
|
||||||
|
final responseData = jsonDecode(response.body);
|
||||||
|
|
||||||
|
if (response.statusCode == 200 && responseData['success'] == true) {
|
||||||
|
logger.i("OTP generated successfully.");
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return {"error": responseData['message'] ?? "Failed to generate OTP."};
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.e("Exception during generate OTP: $e");
|
||||||
|
return {"error": "Network error. Please check your connection."};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify OTP API
|
||||||
|
static Future<Map<String, String>?> verifyOtp({
|
||||||
|
required String email,
|
||||||
|
required String otp,
|
||||||
|
}) async {
|
||||||
|
final requestBody = {
|
||||||
|
"email": email,
|
||||||
|
"otp": otp,
|
||||||
|
};
|
||||||
|
|
||||||
|
logger.i("Sending verify OTP request: $requestBody");
|
||||||
|
|
||||||
|
try {
|
||||||
|
final response = await http.post(
|
||||||
|
Uri.parse("$_baseUrl/auth/login-otp"),
|
||||||
|
headers: _headers,
|
||||||
|
body: jsonEncode(requestBody),
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.i(
|
||||||
|
"Verify OTP API response (${response.statusCode}): ${response.body}");
|
||||||
|
|
||||||
|
final responseData = jsonDecode(response.body);
|
||||||
|
|
||||||
|
if (response.statusCode == 200 && responseData['data'] != null) {
|
||||||
|
await _handleLoginSuccess(responseData['data']);
|
||||||
|
logger.i("OTP verified and login state initialized successfully.");
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return {"error": responseData['message'] ?? "Failed to verify OTP."};
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.e("Exception during verify OTP: $e");
|
||||||
|
return {"error": "Network error. Please check your connection."};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> _handleLoginSuccess(Map<String, dynamic> data) async {
|
||||||
|
final jwtToken = data['token'];
|
||||||
|
final refreshToken = data['refreshToken'];
|
||||||
|
final mpinToken = data['mpinToken'];
|
||||||
|
|
||||||
|
logger.i("JWT Token: $jwtToken");
|
||||||
|
if (refreshToken != null) logger.i("Refresh Token: $refreshToken");
|
||||||
|
if (mpinToken != null) logger.i("MPIN Token: $mpinToken");
|
||||||
|
|
||||||
|
await LocalStorage.setJwtToken(jwtToken);
|
||||||
|
await LocalStorage.setLoggedInUser(true);
|
||||||
|
|
||||||
|
if (refreshToken != null) {
|
||||||
|
await LocalStorage.setRefreshToken(refreshToken);
|
||||||
|
}
|
||||||
|
if (mpinToken != null && mpinToken.isNotEmpty) {
|
||||||
|
await LocalStorage.setMpinToken(mpinToken);
|
||||||
|
await LocalStorage.setIsMpin(true);
|
||||||
|
} else {
|
||||||
|
await LocalStorage.setIsMpin(false);
|
||||||
|
await LocalStorage.removeMpinToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
Get.put(PermissionController());
|
||||||
|
isLoggedIn = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ import 'package:marco/helpers/widgets/my_text.dart';
|
|||||||
import 'package:marco/helpers/widgets/my_text_style.dart';
|
import 'package:marco/helpers/widgets/my_text_style.dart';
|
||||||
import 'package:marco/controller/auth/otp_controller.dart';
|
import 'package:marco/controller/auth/otp_controller.dart';
|
||||||
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
|
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
|
||||||
|
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||||||
|
|
||||||
class OTPLoginScreen extends StatefulWidget {
|
class OTPLoginScreen extends StatefulWidget {
|
||||||
const OTPLoginScreen({super.key});
|
const OTPLoginScreen({super.key});
|
||||||
@ -29,42 +30,43 @@ class _OTPLoginScreenState extends State<OTPLoginScreen> with UIMixin {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
return Obx(() {
|
return SingleChildScrollView(
|
||||||
return SingleChildScrollView(
|
child: Column(
|
||||||
child: Column(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
children: [
|
||||||
children: [
|
MyText.bodyMedium('OTP Verification', fontWeight: 600),
|
||||||
MyText.bodyMedium('OTP Verification', fontWeight: 600),
|
MySpacing.height(24),
|
||||||
MySpacing.height(24),
|
Obx(() => !controller.isOTPSent.value
|
||||||
if (!controller.isOTPSent.value) ...[
|
? Column(
|
||||||
_buildPhoneInput(),
|
children: [
|
||||||
MySpacing.height(24),
|
_buildEmailInput(),
|
||||||
_buildSendOTPButton(theme),
|
MySpacing.height(24),
|
||||||
] else ...[
|
_buildSendOTPButton(theme),
|
||||||
_buildOTPSentInfo(),
|
],
|
||||||
MySpacing.height(12),
|
)
|
||||||
_buildOTPForm(theme),
|
: Column(
|
||||||
MySpacing.height(12),
|
children: [
|
||||||
_buildResendOTPSection(theme),
|
_buildOTPSentInfo(),
|
||||||
MySpacing.height(12),
|
MySpacing.height(12),
|
||||||
_buildVerifyOTPButton(theme),
|
_buildOTPForm(theme),
|
||||||
],
|
MySpacing.height(12),
|
||||||
],
|
_buildResendOTPSection(theme),
|
||||||
),
|
MySpacing.height(12),
|
||||||
);
|
_buildVerifyOTPButton(theme),
|
||||||
});
|
],
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildPhoneInput() {
|
Widget _buildEmailInput() {
|
||||||
return TextFormField(
|
return TextFormField(
|
||||||
controller: controller.phoneController,
|
controller: controller.emailController,
|
||||||
keyboardType: TextInputType.phone,
|
keyboardType: TextInputType.emailAddress,
|
||||||
maxLength: 10,
|
|
||||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Enter Mobile Number',
|
labelText: 'Enter Email Address',
|
||||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)),
|
border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)),
|
||||||
counterText: '',
|
|
||||||
),
|
),
|
||||||
onFieldSubmitted: (_) => controller.sendOTP(),
|
onFieldSubmitted: (_) => controller.sendOTP(),
|
||||||
);
|
);
|
||||||
@ -107,39 +109,23 @@ class _OTPLoginScreenState extends State<OTPLoginScreen> with UIMixin {
|
|||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.sms_outlined, color: Colors.redAccent, size: 24),
|
Icon(Icons.email_outlined, color: Colors.redAccent, size: 24),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
MyText.bodySmall(
|
MyText.bodySmall('OTP sent to', fontWeight: 500),
|
||||||
'OTP sent to',
|
MyText.bodySmall(controller.email.value,
|
||||||
fontWeight: 500,
|
fontWeight: 700, color: Colors.black),
|
||||||
),
|
|
||||||
MyText.bodySmall(
|
|
||||||
'+91-${controller.phoneNumber.value}',
|
|
||||||
fontWeight: 700,
|
|
||||||
color: Colors.black,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
TextButton.icon(
|
TextButton.icon(
|
||||||
onPressed: controller.resetForChangeNumber,
|
onPressed: controller.resetForChangeEmail,
|
||||||
icon: Icon(Icons.edit, size: 16, color: Colors.redAccent),
|
icon: Icon(Icons.edit, size: 16, color: Colors.redAccent),
|
||||||
label: MyText.bodySmall(
|
label: MyText.bodySmall('Change',
|
||||||
'Change',
|
fontWeight: 600, color: Colors.redAccent),
|
||||||
fontWeight: 600,
|
|
||||||
color: Colors.redAccent,
|
|
||||||
),
|
|
||||||
style: TextButton.styleFrom(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
foregroundColor: Colors.redAccent,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -170,6 +156,10 @@ class _OTPLoginScreenState extends State<OTPLoginScreen> with UIMixin {
|
|||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.number,
|
||||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||||
onChanged: (value) => controller.onOTPChanged(value, index),
|
onChanged: (value) => controller.onOTPChanged(value, index),
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.isEmpty) return '';
|
||||||
|
return null;
|
||||||
|
},
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
counterText: '',
|
counterText: '',
|
||||||
filled: true,
|
filled: true,
|
||||||
@ -204,13 +194,9 @@ class _OTPLoginScreenState extends State<OTPLoginScreen> with UIMixin {
|
|||||||
? const SizedBox(
|
? const SizedBox(
|
||||||
width: 16,
|
width: 16,
|
||||||
height: 16,
|
height: 16,
|
||||||
child: CircularProgressIndicator(strokeWidth: 2),
|
child: CircularProgressIndicator(strokeWidth: 2))
|
||||||
)
|
: MyText.bodySmall('Resend OTP',
|
||||||
: MyText.bodySmall(
|
fontWeight: 600, color: Colors.blueAccent),
|
||||||
'Resend OTP',
|
|
||||||
fontWeight: 600,
|
|
||||||
color: Colors.blueAccent,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -220,7 +206,17 @@ class _OTPLoginScreenState extends State<OTPLoginScreen> with UIMixin {
|
|||||||
return Align(
|
return Align(
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: MyButton.rounded(
|
child: MyButton.rounded(
|
||||||
onPressed: controller.verifyOTP,
|
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,
|
elevation: 2,
|
||||||
padding: MySpacing.xy(24, 16),
|
padding: MySpacing.xy(24, 16),
|
||||||
borderRadiusAll: 10,
|
borderRadiusAll: 10,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user