Commit 8ec716d3 authored by DatHV's avatar DatHV
Browse files

update logic

parent 7d37c9c6
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:
......@@ -6,7 +6,6 @@ class APIPaths {
static const String checkPhoneNumber = "/user/api/v2.0/account/users/checkPhoneNumber";
static const String verifyOtpWithAction = "/iam/v2/authentication/otp/verifyWithAction";
static const String retryOtpWithAction = "/iam/v2/authentication/otp/retry";
static const String signup = "/user/api/v2.0/signup";
static const String otpCreateNew = "/otpCreateNew/1.0.0";
}
class Constants {
static get commonError => "Có lỗi xảy ra. Vui lòng thử lại!";
static var otpTtl = 180;
// device key
}
class ErrorCodes {
......
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:mypoint_flutter_app/screen/onboarding/onboarding_view_model.dart';
import 'package:mypoint_flutter_app/screen/onboarding/onboarding_viewmodel.dart';
import 'package:mypoint_flutter_app/screen/splash/splash_screen.dart';
void main() {
......
import 'dart:io';
import 'package:mypoint_flutter_app/configs/api_paths.dart';
import 'package:mypoint_flutter_app/base/base_response_model.dart';
import 'package:mypoint_flutter_app/configs/constants.dart';
import 'package:mypoint_flutter_app/extensions/string_extension.dart';
import 'package:mypoint_flutter_app/networking/restful_api.dart';
import '../configs/device_info.dart';
import '../model/update_response_object.dart';
import '../screen/onboarding/model/check_phone_response_model.dart';
import '../screen/onboarding/model/onboarding_info_model.dart';
import '../screen/otp/otp_verify_response_model.dart';
import '../screen/otp/model/otp_verify_response_model.dart';
import '../screen/splash/splash_screen_viewmodel.dart';
import 'model_maker.dart';
extension RestfullAPIClientAllApi on RestfulAPIClient {
......@@ -62,4 +64,26 @@ extension RestfullAPIClientAllApi on RestfulAPIClient {
(data) => OTPResendResponseModel.fromJson(data as Json),
);
}
Future<BaseResponseModel<EmptyCodable>> signup(String phone, String password) async {
var deviceKey = await DeviceInfo.getDeviceId();
final body = {"username": phone, "password": password.toSha256(), "device_key": deviceKey};
return requestNormal(
APIPaths.signup,
Method.POST,
body,
(data) => EmptyCodable.fromJson(data as Json),
);
}
Future<BaseResponseModel<EmptyCodable>> otpCreateNew(String ownerId, String password) async {
var deviceKey = await DeviceInfo.getDeviceId();
final body = {"owner_id": ownerId, "ttl": Constants.otpTtl, "resend_after_second": 30};
return requestNormal(
APIPaths.otpCreateNew,
Method.POST,
body,
(data) => EmptyCodable.fromJson(data as Json),
);
}
}
import 'package:flutter/material.dart';
import 'package:mypoint_flutter_app/resouce/base_color.dart';
enum TextStyleIconAndText {
header1,
......@@ -82,3 +83,32 @@ TextStyle getTextStyle(TextStyleIconAndText? textType) {
return textNormal;
}
}
final BorderRadius _borderRadius = BorderRadius.circular(8.0);
final BorderSide _defaultBorderSide = BorderSide(color: BaseColor.second400);
final BorderSide _focusedBorderSide = BorderSide(color: Colors.blue);
final EdgeInsets _contentPadding = EdgeInsets.symmetric(vertical: 16.0, horizontal: 16.0);
class PTTextStyles {
static final InputDecoration textFieldDecoration = InputDecoration(
border: OutlineInputBorder(
borderRadius: _borderRadius,
borderSide: _defaultBorderSide,
),
enabledBorder: OutlineInputBorder(
borderRadius: _borderRadius,
borderSide: _defaultBorderSide,
),
focusedBorder: OutlineInputBorder(
borderRadius: _borderRadius,
borderSide: _focusedBorderSide,
),
labelText: 'Enter text',
labelStyle: TextStyle(color: Colors.grey),
hintText: 'Hint text',
hintStyle: TextStyle(color: Colors.grey),
filled: true,
fillColor: Colors.white,
contentPadding: _contentPadding,
);
}
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:mypoint_flutter_app/screen/create_pass/signup_create_password_repository.dart';
import 'package:mypoint_flutter_app/screen/onboarding/onboarding_screen.dart';
import 'package:mypoint_flutter_app/widgets/back_button.dart';
import '../../resouce/base_color.dart';
import 'create_pass_viewmodel.dart';
class CreatePasswordScreen extends StatelessWidget {
final ICreatePasswordRepository repository;
const CreatePasswordScreen({super.key, required this.repository});
@override
Widget build(BuildContext context) {
final vm = Get.put(CreatePasswordViewModel(repository));
final isNewPassObscure = true.obs;
final isConfirmPassObscure = true.obs;
return Scaffold(
appBar: AppBar(
centerTitle: true,
leading: CustomBackButton(onPressed: () => {Get.off(() => OnboardingScreen())}),
),
body: SafeArea(
child: Stack(
children: [
GestureDetector(
onTap: () => FocusScope.of(context).unfocus(),
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text("Tạo mật khẩu", style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
const SizedBox(height: 32),
Obx(() {
return _buildPasswordField(
hintText: "Nhập mật khẩu mới",
obscureText: isNewPassObscure.value,
onToggleObscure: () => isNewPassObscure.value = !isNewPassObscure.value,
onChanged: vm.onNewPasswordChanged,
);
}),
const SizedBox(height: 16),
Obx(() {
return _buildPasswordField(
hintText: "Xác nhận lại mật khẩu",
obscureText: isConfirmPassObscure.value,
onToggleObscure: () => isConfirmPassObscure.value = !isConfirmPassObscure.value,
onChanged: vm.onConfirmPasswordChanged,
);
}),
const SizedBox(height: 8),
Obx(() {
final err = vm.errorMessage.value;
if (err.isEmpty) {
return const SizedBox.shrink();
} else {
return Text(err, style: const TextStyle(color: Colors.red));
}
}),
const SizedBox(height: 8),
_buildInfoGuide(icon: Icons.info_outline, text: "Mật khẩu gồm 6 chữ số"),
const SizedBox(height: 4),
_buildInfoGuide(icon: Icons.info_outline, text: "Không bao gồm dãy số trùng nhau"),
const SizedBox(height: 4),
_buildInfoGuide(icon: Icons.info_outline, text: "Không bao gồm dãy số liên tiếp"),
],
),
),
),
SizedBox.expand(),
Positioned(left: 0, right: 0, bottom: 16, child: _buildContinueButton(vm)),
],
),
),
);
}
Widget _buildContinueButton(CreatePasswordViewModel vm) {
return Obx(() {
final enabled = vm.isButtonEnabled.value;
return Container(
color: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: ElevatedButton(
onPressed: enabled ? vm.onSubmit : null,
style: ElevatedButton.styleFrom(
minimumSize: const Size.fromHeight(48),
backgroundColor: enabled ? BaseColor.primary500 : BaseColor.second400,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
child: Text(
"Tiếp tục",
style: TextStyle(color: enabled ? Colors.white : Colors.white, fontWeight: FontWeight.bold, fontSize: 16),
),
),
);
});
}
Widget _buildInfoGuide({required IconData icon, required String text}) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, color: BaseColor.second400, size: 24),
SizedBox(width: 8),
Expanded(child: Text(text, style: const TextStyle(color: BaseColor.second400, fontSize: 14))),
],
);
}
Widget _buildPasswordField({
required String hintText,
required bool obscureText,
required VoidCallback onToggleObscure,
required ValueChanged<String> onChanged,
}) {
return TextField(
obscureText: obscureText,
onChanged: onChanged,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.password, color: BaseColor.second500),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: Colors.grey, width: 1),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: Colors.grey, width: 1),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: Colors.grey, width: 1),
),
hintText: hintText,
hintStyle: const TextStyle(color: BaseColor.second200),
fillColor: Colors.white,
filled: true,
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14),
suffixIcon: IconButton(
icon: Icon(obscureText ? Icons.visibility_off : Icons.visibility, color: BaseColor.second500),
onPressed: onToggleObscure,
),
),
);
}
}
import 'package:get/get.dart';
import 'package:mypoint_flutter_app/screen/create_pass/signup_create_password_repository.dart';
class CreatePasswordViewModel extends GetxController {
final ICreatePasswordRepository repository;
var newPassword = "".obs;
var confirmPassword = "".obs;
var errorMessage = "".obs;
var isButtonEnabled = false.obs;
CreatePasswordViewModel(this.repository);
void onNewPasswordChanged(String value) {
newPassword.value = value.trim();
_validate();
}
void onConfirmPasswordChanged(String value) {
confirmPassword.value = value.trim();
_validate();
}
void _validate() {
if (newPassword.value.isEmpty || confirmPassword.value.isEmpty) {
errorMessage.value = "";
isButtonEnabled.value = false;
return;
}
if (newPassword.value != confirmPassword.value) {
errorMessage.value = "Mật khẩu không khớp. Vui lòng kiểm tra.";
isButtonEnabled.value = false;
} else {
errorMessage.value = "";
isButtonEnabled.value = true;
}
}
Future<void> onSubmit() async {
if (!isButtonEnabled.value) return;
try {
final success = await repository.signup(newPassword.value);
if (success) {
errorMessage.value = "";
// TODO: Điều hướng sang màn hình tiếp theo
// e.g. Get.offAllNamed("/home");
} else {
errorMessage.value = "Tạo mật khẩu thất bại. Thử lại sau.";
}
} catch (e) {
errorMessage.value = "Có lỗi xảy ra: $e";
}
}
}
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:mypoint_flutter_app/networking/restful_api_request.dart';
import '../../base/restful_api_viewmodel.dart';
abstract class ICreatePasswordRepository {
late String phoneNumber;
Future<bool?> createPassword(String newPassword);
Future<bool> signup(String password);
}
class SignUpCreatePasswordRepository extends RestfulApiViewModel implements ICreatePasswordRepository {
@override
late String phoneNumber;
SignUpCreatePasswordRepository(this.phoneNumber);
@override
Future<bool> signup(String password) async {
showLoading();
return client.signup(phoneNumber, password).then((value) {
hideLoading();
return value.isSuccess;
});
}
@override
Future<bool?> createPassword(String newPassword) {
// TODO: implement createPassword
throw UnimplementedError();
}
}
......@@ -5,10 +5,14 @@ import '../../base/base_screen.dart';
import '../../base/basic_state.dart';
import '../../permission/biometric_manager.dart';
import '../../resouce/base_color.dart';
import 'login_view_model.dart';
import '../../widgets/back_button.dart';
import '../../widgets/support_button.dart';
import 'login_viewmodel.dart';
class LoginScreen extends BaseScreen {
const LoginScreen({super.key});
final String phoneNumber;
final String? fullName;
const LoginScreen({super.key, required this.phoneNumber, this.fullName});
@override
State<LoginScreen> createState() => _LoginScreenState();
......@@ -16,56 +20,41 @@ class LoginScreen extends BaseScreen {
class _LoginScreenState extends BaseState<LoginScreen> with BasicState {
final TextEditingController _phoneController = TextEditingController();
final FocusNode _focusNode = FocusNode();
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
_focusNode.requestFocus();
});
}
@override
void dispose() {
_focusNode.dispose();
_phoneController.dispose();
super.dispose();
}
@override
Widget createBody() {
// Khởi tạo hoặc lấy LoginViewModel
final loginVM = Get.put(LoginViewModel());
return GestureDetector(
onTap: hideKeyboard,
child: Scaffold(
// Để nội dung nâng lên khi bàn phím xuất hiện
resizeToAvoidBottomInset: false,
appBar: AppBar(
automaticallyImplyLeading: false,
backgroundColor: Colors.white,
centerTitle: true,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios),
color: Colors.black,
onPressed: () => Navigator.pop(context),
),
actions: [
Container(
margin: const EdgeInsets.only(right: 16),
height: 36,
decoration: BoxDecoration(
border: Border.all(
color: BaseColor.second400,
width: 1,
),
borderRadius: BorderRadius.circular(18),
color: Colors.white,
),
child: TextButton.icon(
onPressed: () {
// Xử lý mở màn hình hỗ trợ hoặc gọi hotline...
},
icon: const Icon(Icons.headset_mic, size: 18, color: BaseColor.second600,),
label: const Text("Hỗ trợ"),
style: TextButton.styleFrom(
foregroundColor: BaseColor.second600,
padding: const EdgeInsets.symmetric(horizontal: 8),
),
),
),
],
leading: CustomBackButton(),
actions: [SupportButton()],
),
backgroundColor: Colors.white,
body: SafeArea(
child: Stack(
children: [
// Nội dung cuộn ở dưới
SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
child: Column(
......@@ -97,28 +86,26 @@ class _LoginScreenState extends BaseState<LoginScreen> with BasicState {
}
Widget _buildWelcomeText(LoginViewModel vm) {
return Obx(() {
return RichText(
text: TextSpan(
style: const TextStyle(fontSize: 14, color: BaseColor.second500),
children: [
const TextSpan(text: "Chào mừng "),
TextSpan(text: "${vm.userName}"),
const TextSpan(text: " "),
const TextSpan(text: "Xin chào "),
TextSpan(text: widget.fullName ?? "Quý Khách "),
TextSpan(
text: "${vm.phoneNumber}",
text: widget.phoneNumber,
style: const TextStyle(fontWeight: FontWeight.w500, color: BaseColor.primary500),
),
],
),
);
});
}
Widget _buildPasswordField(LoginViewModel vm) {
return Obx(() {
return TextField(
controller: _phoneController,
focusNode: _focusNode,
keyboardType: TextInputType.number,
obscureText: !vm.isPasswordVisible.value,
onChanged: vm.onPasswordChanged,
......@@ -131,7 +118,15 @@ class _LoginScreenState extends BaseState<LoginScreen> with BasicState {
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: Colors.red), //BaseColor.second200),
borderSide: const BorderSide(color: Colors.grey, width: 1),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: Colors.grey, width: 1),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: Colors.grey, width: 1),
),
suffixIcon: IconButton(
icon: Icon(
......
import 'package:get/get.dart';
import 'package:flutter/material.dart';
import 'package:mypoint_flutter_app/screen/onboarding/onboarding_screen.dart';
import '../../base/restful_api_viewmodel.dart';
import '../../permission/biometric_manager.dart';
......@@ -17,12 +18,6 @@ class LoginViewModel extends RestfulApiViewModel {
var loginState = LoginState.idle.obs;
var isPasswordVisible = false.obs;
var password = "".obs;
// Giả lập userName và phoneNumber
final userName = "Phạm Duy Đức".obs;
final phoneNumber = "0987654321".obs;
// Loại sinh trắc học mà thiết bị hỗ trợ
var biometricType = BiometricTypeEnum.none.obs;
@override
......@@ -36,7 +31,6 @@ class LoginViewModel extends RestfulApiViewModel {
biometricType.value = type;
}
// Kiểm tra thiết bị có cho phép check biometrics không
Future<bool> canUseBiometrics() async {
return _biometricManager.canCheckBiometrics();
}
......@@ -56,7 +50,6 @@ class LoginViewModel extends RestfulApiViewModel {
void onLoginPressed() {
if (password.value.isEmpty) return;
// Ví dụ: Mật khẩu chuẩn là "123456"
if (password.value == "123456") {
loginState.value = LoginState.done;
......@@ -69,13 +62,13 @@ class LoginViewModel extends RestfulApiViewModel {
}
void onChangePhonePressed() {
debugPrint("Người dùng chọn Đổi số điện thoại");
// TODO: Logic đổi SĐT hoặc chuyển sang màn hình khác
Get.back();
}
void onForgotPassPressed() {
debugPrint("Người dùng chọn Quên mật khẩu?");
// TODO: Logic quên mật khẩu, ví dụ chuyển sang màn hình recovery
client.getOnboardingInfo().then((value) {
info.value = value;
});
}
/// Xác thực đăng nhập bằng sinh trắc
......
......@@ -7,11 +7,13 @@ import '../../base/base_screen.dart';
import '../../base/basic_state.dart';
import '../../configs/constants.dart';
import '../../resouce/base_color.dart';
import '../create_pass/create_pass_screen.dart';
import '../create_pass/signup_create_password_repository.dart';
import '../login/login_screen.dart';
import '../otp/otp_screen.dart';
import '../otp/verify_otp_repository.dart';
import 'model/check_phone_response_model.dart';
import 'onboarding_view_model.dart';
import 'onboarding_viewmodel.dart';
class OnboardingScreen extends BaseScreen {
const OnboardingScreen({super.key});
......@@ -32,16 +34,9 @@ class _OnboardingScreenState extends BaseState<OnboardingScreen> with BasicState
WidgetsBinding.instance.addPostFrameCallback((_) {
hideKeyboard();
// Get.to(() => const LoginScreen());
Get.to(() => OtpScreen(
repository: VerifyOtpRepository(
_viewModel.phoneNumber.value,
response.data?.otpTtl ?? 0,
response.data?.mfaToken ?? "",
),
),
);
_handleResponseCheckPhoneNumber(response.data);
});
// _handleResponseCheckPhoneNumber(response.data);
//
// _handleResponseError(response);
});
}
......@@ -66,15 +61,21 @@ class _OnboardingScreenState extends BaseState<OnboardingScreen> with BasicState
// show Captcha
return;
}
if ((response.mfaToken ?? "").isNotEmpty) {
if ((response.mfaToken ?? "").isNotEmpty || (response.nextAction == "signup")) {
// show OTP
Get.to(
() => OtpScreen(
repository: VerifyOtpRepository(
_viewModel.phoneNumber.value,
response!.otpTtl ?? 0,
response!.mfaToken ?? "",
),
),
);
return;
}
if (response.nextAction == "login") {
return;
}
if (response.nextAction == "signup") {
return;
Get.to(() => LoginScreen(phoneNumber: _viewModel.phoneNumber.value));
}
}
......@@ -87,7 +88,6 @@ class _OnboardingScreenState extends BaseState<OnboardingScreen> with BasicState
resizeToAvoidBottomInset: false,
body: Stack(
children: [
/// 📌 Hiển thị background từ API (hoặc ảnh mặc định)
Obx(
() => Positioned.fill(
child:
......@@ -96,14 +96,10 @@ class _OnboardingScreenState extends BaseState<OnboardingScreen> with BasicState
: Image.asset("assets/images/bg_onboarding.png", fit: BoxFit.cover),
),
),
/// 📌 Nội dung chính
SafeArea(
child: Column(
// mainAxisAlignment: MainAxisAlignment.end,
children: [
Spacer(),
// Expanded(child: Container()),
AnimatedContainer(
duration: const Duration(milliseconds: 300),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 32),
......@@ -114,7 +110,6 @@ class _OnboardingScreenState extends BaseState<OnboardingScreen> with BasicState
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
/// 📌 Tiêu đề (Hiển thị nội dung HTML từ API hoặc mặc định)
Obx(
() => Visibility(
visible: !_focusNode.hasFocus,
......@@ -129,8 +124,6 @@ class _OnboardingScreenState extends BaseState<OnboardingScreen> with BasicState
),
),
const SizedBox(height: 16),
/// 📌 Ô nhập số điện thoại
TextField(
inputFormatters: [LengthLimitingTextInputFormatter(10)],
// maxLength: 10,
......@@ -151,8 +144,6 @@ class _OnboardingScreenState extends BaseState<OnboardingScreen> with BasicState
onChanged: _viewModel.updatePhoneNumber,
),
const SizedBox(height: 16),
/// 📌 Nút Tiếp Tục
Obx(
() => SizedBox(
width: double.infinity,
......@@ -180,8 +171,6 @@ class _OnboardingScreenState extends BaseState<OnboardingScreen> with BasicState
),
),
const SizedBox(height: 16),
/// 📌 Checkbox + Điều khoản sử dụng + Chính sách bảo mật
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
......
import 'package:json_annotation/json_annotation.dart';
part 'create_otp_response_model.g.dart';
@JsonSerializable()
class CreateOTPResponseModel {
@JsonKey(name: 'resend_after_second')
int? resendAfterSecond;
CreateOTPResponseModel({
this.resendAfterSecond,
});
factory CreateOTPResponseModel.fromJson(Map<String, dynamic> json) => _$CreateOTPResponseModelFromJson(json);
Map<String, dynamic> toJson() => _$CreateOTPResponseModelToJson(this);
}
\ No newline at end of file
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'create_otp_response_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
CreateOTPResponseModel _$CreateOTPResponseModelFromJson(
Map<String, dynamic> json,
) => CreateOTPResponseModel(
resendAfterSecond: (json['resend_after_second'] as num?)?.toInt(),
);
Map<String, dynamic> _$CreateOTPResponseModelToJson(
CreateOTPResponseModel instance,
) => <String, dynamic>{'resend_after_second': instance.resendAfterSecond};
......@@ -23,7 +23,7 @@ Map<String, dynamic> _$OTPVerifyResponseModelToJson(
OTPResendResponseModel _$OTPResendResponseModelFromJson(
Map<String, dynamic> json,
) => OTPResendResponseModel(otpTtl: json['otp_ttl'] as int?);
) => OTPResendResponseModel(otpTtl: (json['otp_ttl'] as num?)?.toInt());
Map<String, dynamic> _$OTPResendResponseModelToJson(
OTPResendResponseModel instance,
......
......@@ -4,7 +4,9 @@ import 'package:pin_code_fields/pin_code_fields.dart';
import '../../base/base_screen.dart';
import '../../base/basic_state.dart';
import '../../resouce/base_color.dart';
import 'otp_view_model.dart';
import '../../widgets/back_button.dart';
import '../../widgets/support_button.dart';
import 'otp_viewmodel.dart';
class OtpScreen extends BaseScreen {
final IOtpRepository repository;
......@@ -15,6 +17,7 @@ class OtpScreen extends BaseScreen {
}
class _OtpScreenState extends BaseState<OtpScreen> with BasicState {
final TextEditingController _pinController = TextEditingController();
@override
Widget createBody() {
......@@ -22,7 +25,10 @@ class _OtpScreenState extends BaseState<OtpScreen> with BasicState {
return Scaffold(
appBar: AppBar(
centerTitle: true,
leading: IconButton(icon: const Icon(Icons.arrow_back_ios), onPressed: () => Navigator.pop(context)),
leading: CustomBackButton(),
actions: [
SupportButton(),
],
),
body: SafeArea(
child: GestureDetector(
......@@ -49,11 +55,10 @@ class _OtpScreenState extends BaseState<OtpScreen> with BasicState {
);
}
/// PinCodeTextField cho 6 ô
Widget _buildPinCodeFields(OtpViewModel vm) {
double screenWidth = MediaQuery.of(context).size.width;
// return Obx(() {
return PinCodeTextField(
controller: _pinController,
appContext: Get.context!,
length: 6,
obscureText: false,
......@@ -72,18 +77,16 @@ class _OtpScreenState extends BaseState<OtpScreen> with BasicState {
),
onChanged: (value) {
vm.otpCode.value = value;
vm.errorMessage.value = "1111111"; // clear lỗi khi gõ
vm.errorMessage.value = "";
},
onCompleted: (value) {
vm.otpCode.value = value;
vm.onSubmitOtp;
vm.onSubmitOtp();
},
);
// });
}
Widget _buildErrorText(OtpViewModel vm) {
// Chỉ bọc Obx ở đây vì ta đọc vm.errorMessage
return Obx(() {
final error = vm.errorMessage.value;
if (error.isEmpty) {
......@@ -93,9 +96,7 @@ class _OtpScreenState extends BaseState<OtpScreen> with BasicState {
});
}
/// "Gửi lại OTP (02:30)"
Widget _buildResendOtp(OtpViewModel vm) {
// Bọc Obx vì ta đọc vm.currentCountdown
return Obx(() {
final cd = vm.currentCountdown.value;
final canResend = cd == 0;
......@@ -104,10 +105,15 @@ class _OtpScreenState extends BaseState<OtpScreen> with BasicState {
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextButton(
onPressed: canResend ? vm.onResendOtp : null,
onPressed: () => {
canResend ? vm.onResendOtp() : null,
vm.otpCode.value = "",
vm.errorMessage.value = "",
_pinController.clear(),
},
child: Text(
"Gửi lại OTP ${!canResend ? "($textTime)" : ""}",
style: TextStyle(color: canResend ? Colors.blue : Colors.grey),
style: TextStyle(color: canResend ? BaseColor.second700 : BaseColor.second500),
),
),
],
......@@ -122,7 +128,7 @@ class _OtpScreenState extends BaseState<OtpScreen> with BasicState {
children: [
const TextSpan(text: "Mã OTP đã được gửi về số điện thoại "),
TextSpan(
text: "0999999999", //"${vm.phoneNumber}",
text: widget.repository.phoneNumber,
style: const TextStyle(fontWeight: FontWeight.w500, color: BaseColor.primary500),
),
],
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment