Commit 1257980d authored by DatHV's avatar DatHV
Browse files

update screen otp, login

parent abd9f02e
......@@ -4,4 +4,5 @@
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
</manifest>
......@@ -4,4 +4,5 @@
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
</manifest>
......@@ -45,5 +45,9 @@
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>NSFaceIDUsageDescription</key>
<string>We need Face ID to authenticate your identity</string>
<key>NSLocalAuthenticationUseFaceID</key>
<string>true</string>
</dict>
</plist>
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:mypoint_flutter_app/splash_screen/splash_screen.dart';
import 'onboading/onboarding_screen.dart';
import 'onboading/onboarding_viewmodel.dart';
import 'package:mypoint_flutter_app/screen/onboarding/onboarding_view_model.dart';
import 'package:mypoint_flutter_app/screen/splash/splash_screen.dart';
void main() {
Get.put(OnboardingViewModel());
......
......@@ -5,8 +5,8 @@ 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 '../onboading/model/check_phone_response_model.dart';
import '../onboading/model/onboarding_info_model.dart';
import '../screen/onboarding/model/check_phone_response_model.dart';
import '../screen/onboarding/model/onboarding_info_model.dart';
import 'model_maker.dart';
extension RestfullAPIClientAllApi on RestfulAPIClient {
......
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:local_auth/local_auth.dart';
enum BiometricTypeEnum {
none,
fingerprint,
faceId,
}
class BiometricManager {
final LocalAuthentication _localAuth = LocalAuthentication();
/// Kiểm tra xem thiết bị hỗ trợ loại sinh trắc học nào: faceID, fingerprint, none
Future<BiometricTypeEnum> checkDeviceBiometric() async {
try {
final availableBiometrics = await _localAuth.getAvailableBiometrics();
if (availableBiometrics.contains(BiometricType.face)) {
return BiometricTypeEnum.faceId;
} else if (availableBiometrics.contains(BiometricType.fingerprint)) {
return BiometricTypeEnum.fingerprint;
}
return BiometricTypeEnum.none;
} catch (e) {
debugPrint("Lỗi checkDeviceBiometric: $e");
return BiometricTypeEnum.none;
}
}
/// Kiểm tra nhanh thiết bị có thể dùng sinh trắc học hay không
Future<bool> canCheckBiometrics() async {
try {
final canCheck = await _localAuth.canCheckBiometrics;
final isSupported = await _localAuth.isDeviceSupported();
return canCheck && isSupported;
} catch (e) {
debugPrint("Lỗi canCheckBiometrics/isDeviceSupported: $e");
return false;
}
}
/// Thực hiện xác thực bằng sinh trắc
/// - `localizedReason` là chuỗi yêu cầu xác thực hiển thị mặc định trên hệ thống
/// - Trả về true nếu user xác thực thành công, false nếu user huỷ hoặc thất bại
Future<bool> authenticateBiometric({String localizedReason = "Xác thực để đăng nhập"}) async {
try {
final didAuthenticate = await _localAuth.authenticate(
localizedReason: localizedReason,
options: const AuthenticationOptions(biometricOnly: true),
);
return didAuthenticate;
} catch (e) {
debugPrint("Lỗi authenticateBiometric: $e");
return false;
}
}
/// (Tuỳ chọn) Hiển thị trước một dialog hỏi "Có muốn xác thực bằng vân tay/FaceID hay không?"
/// Nếu user bấm "Đồng ý", mới gọi authenticateBiometric
Future<bool> showCustomBiometricDialog(
BuildContext context, {
String title = "Sử dụng sinh trắc học",
String content = "Bạn có muốn đăng nhập bằng vân tay/Face ID không?",
String confirmText = "Đồng ý",
String cancelText = "Huỷ",
}) async {
final result = await Get.dialog<bool>(
AlertDialog(
title: Text(title),
content: Text(content),
actions: [
TextButton(
onPressed: () => Get.back(result: false),
child: Text(cancelText),
),
TextButton(
onPressed: () => Get.back(result: true),
child: Text(confirmText),
),
],
),
);
if (result == true) {
// Chỉ khi user chọn Đồng ý thì mới gọi authenticateBiometric
return await authenticateBiometric();
}
// Người dùng huỷ dialog => false
return false;
}
}
// login_screen.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
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';
class LoginScreen extends BaseScreen {
const LoginScreen({super.key});
@override
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends BaseState<LoginScreen> with BasicState {
final TextEditingController _phoneController = TextEditingController();
@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),
),
),
),
],
),
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(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
"Đăng nhập",
style: TextStyle(color: BaseColor.second600, fontSize: 24, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
_buildWelcomeText(loginVM),
const SizedBox(height: 16),
_buildPasswordField(loginVM),
_buildErrorText(loginVM),
const SizedBox(height: 8),
_buildActionRow(loginVM),
const SizedBox(height: 8),
_buildBiometricSection(loginVM),
],
),
),
SizedBox.expand(),
Positioned(left: 0, right: 0, bottom: 16, child: _buildLoginButton(loginVM)),
],
),
),
),
);
}
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: " "),
TextSpan(
text: "${vm.phoneNumber}",
style: const TextStyle(fontWeight: FontWeight.w500, color: BaseColor.primary500),
),
],
),
);
});
}
Widget _buildPasswordField(LoginViewModel vm) {
return Obx(() {
return TextField(
controller: _phoneController,
keyboardType: TextInputType.number,
obscureText: !vm.isPasswordVisible.value,
onChanged: vm.onPasswordChanged,
decoration: InputDecoration(
hintText: "Nhập mật khẩu",
prefixIcon: const Icon(Icons.password, color: BaseColor.second500),
hintStyle: const TextStyle(color: BaseColor.second200),
fillColor: Colors.white,
filled: true,
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: Colors.red), //BaseColor.second200),
),
suffixIcon: IconButton(
icon: Icon(
vm.isPasswordVisible.value ? Icons.visibility : Icons.visibility_off,
color: BaseColor.second500,
),
onPressed: vm.togglePasswordVisibility,
),
),
);
});
}
Widget _buildErrorText(LoginViewModel vm) {
return Obx(() {
if (vm.loginState.value == LoginState.error) {
return Padding(
padding: const EdgeInsets.only(top: 4),
child: Text("Sai mật khẩu, vui lòng thử lại!", style: TextStyle(color: BaseColor.primary400)),
);
}
return const SizedBox.shrink();
});
}
Widget _buildActionRow(LoginViewModel vm) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
TextButton(
onPressed: vm.onChangePhonePressed,
child: const Text("Đổi số điện thoại", style: TextStyle(fontSize: 14, color: Color(0xFF3662FE))),
),
TextButton(
onPressed: vm.onForgotPassPressed,
child: const Text("Quên mật khẩu?", style: TextStyle(fontSize: 14, color: Color(0xFF3662FE))),
),
],
);
}
Widget _buildBiometricSection(LoginViewModel vm) {
return Obx(() {
// Nếu thiết bị không hỗ trợ => ẩn
// if (vm.biometricType.value == BiometricTypeEnum.none) {
// return const SizedBox.shrink();
// }
// Hiển thị 1 icon tuỳ loại
IconData icon = Icons.fingerprint;
String label = "Vân tay";
if (vm.biometricType.value == BiometricTypeEnum.faceId) {
icon = Icons.face;
label = "Face ID";
}
return Column(
children: [
IconButton(icon: Icon(icon, size: 36), onPressed: () => vm.onBiometricLoginPressed(Get.context!)),
Text("Đăng nhập bằng $label"),
],
);
});
}
Widget _buildLoginButton(LoginViewModel vm) {
return Obx(() {
bool enabled = false;
Color color = BaseColor.second400;
switch (vm.loginState.value) {
case LoginState.typing:
if (vm.password.value.isNotEmpty) {
color = BaseColor.primary500;
enabled = true;
} else {
enabled = false;
color = BaseColor.second400;
}
break;
case LoginState.done:
color = BaseColor.primary500;
enabled = true;
break;
case LoginState.error:
case LoginState.idle:
color = BaseColor.second400;
break;
}
return Container(
color: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: color,
minimumSize: const Size.fromHeight(48),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
onPressed: enabled ? vm.onLoginPressed : null,
child: const Text(
"Đăng nhập",
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.white),
),
),
);
});
}
}
import 'package:get/get.dart';
import 'package:flutter/material.dart';
import '../../base/restful_api_viewmodel.dart';
import '../../permission/biometric_manager.dart';
// login_state_enum.dart
enum LoginState {
idle,
typing,
done,
error,
}
class LoginViewModel extends RestfulApiViewModel {
final BiometricManager _biometricManager = BiometricManager();
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
void onInit() {
super.onInit();
_initBiometric();
}
Future<void> _initBiometric() async {
final type = await _biometricManager.checkDeviceBiometric();
biometricType.value = type;
}
// Kiểm tra thiết bị có cho phép check biometrics không
Future<bool> canUseBiometrics() async {
return _biometricManager.canCheckBiometrics();
}
void onPasswordChanged(String value) {
password.value = value;
if (value.isEmpty) {
loginState.value = LoginState.idle;
} else {
loginState.value = LoginState.typing;
}
}
void togglePasswordVisibility() {
isPasswordVisible.value = !isPasswordVisible.value;
}
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;
debugPrint("Đăng nhập thành công!");
// TODO: Chuyển màn hình
} else {
loginState.value = LoginState.error;
debugPrint("Sai mật khẩu!");
}
}
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
}
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
}
/// Xác thực đăng nhập bằng sinh trắc
Future<void> onBiometricLoginPressed(BuildContext context) async {
// Kiểm tra thiết bị hỗ trợ
final canUse = await canUseBiometrics();
if (!canUse || biometricType.value == BiometricTypeEnum.none) {
Get.snackbar("Thông báo", "Thiết bị không hỗ trợ sinh trắc học",
snackPosition: SnackPosition.BOTTOM);
return;
}
// Tuỳ chọn: hiển thị dialog xác nhận trước khi gọi authenticate
final success = await _biometricManager.showCustomBiometricDialog(
context,
title: "Xác thực sinh trắc học",
content: (biometricType.value == BiometricTypeEnum.faceId)
? "Bạn có muốn đăng nhập bằng Face ID không?"
: "Bạn có muốn đăng nhập bằng vân tay không?",
confirmText: "Đồng ý",
cancelText: "Huỷ",
);
if (success) {
loginState.value = LoginState.done;
debugPrint("Đăng nhập bằng sinh trắc thành công!");
// TODO: Chuyển màn hình
} else {
debugPrint("Xác thực thất bại hoặc người dùng huỷ.");
}
}
}
......@@ -3,12 +3,15 @@ import 'package:flutter/services.dart';
import 'package:flutter_widget_from_html/flutter_widget_from_html.dart'; // Hiển thị HTML
import 'package:get/get.dart';
import 'package:mypoint_flutter_app/base/base_response_model.dart';
import '../base/base_screen.dart';
import '../base/basic_state.dart';
import '../configs/constants.dart';
import '../resouce/base_color.dart';
import '../../base/base_screen.dart';
import '../../base/basic_state.dart';
import '../../configs/constants.dart';
import '../../resouce/base_color.dart';
import '../login/login_screen.dart';
import '../otp/otp_screen.dart';
import '../signup/signup_otp_repository.dart';
import 'model/check_phone_response_model.dart';
import 'onboarding_viewmodel.dart';
import 'onboarding_view_model.dart';
class OnboardingScreen extends BaseScreen {
const OnboardingScreen({super.key});
......@@ -27,8 +30,15 @@ class _OnboardingScreenState extends BaseState<OnboardingScreen> with BasicState
super.initState();
_viewModel.fetchOnboardingContent();
_viewModel.checkPhoneRes.listen((response) {
_handleResponseCheckPhoneNumber(response.data);
_handleResponseError(response);
WidgetsBinding.instance.addPostFrameCallback((_) {
hideKeyboard();
// Get.to(() => const LoginScreen());
Get.to(() => OtpScreen(
repository: SignUpOtpRepository(_viewModel.phoneNumber.value),
));
});
// _handleResponseCheckPhoneNumber(response.data);
// _handleResponseError(response);
});
}
......@@ -64,14 +74,11 @@ class _OnboardingScreenState extends BaseState<OnboardingScreen> with BasicState
}
}
void _hideKeyboard() {
FocusScope.of(Get.context!).unfocus();
}
@override
Widget createBody() {
final double keyboardHeight = MediaQuery.of(context).viewInsets.bottom;
return GestureDetector(
onTap: _hideKeyboard,
onTap: hideKeyboard,
child: Scaffold(
resizeToAvoidBottomInset: false,
body: Stack(
......@@ -87,21 +94,18 @@ class _OnboardingScreenState extends BaseState<OnboardingScreen> with BasicState
),
/// 📌 Nội dung chính
AnimatedPadding(
duration: const Duration(milliseconds: 120),
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
SafeArea(
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
// mainAxisAlignment: MainAxisAlignment.end,
children: [
Container(
Spacer(),
// Expanded(child: Container()),
AnimatedContainer(
duration: const Duration(milliseconds: 300),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 32),
// decoration: const BoxDecoration(
// color: Colors.redAccent,
// borderRadius: BorderRadius.only(
// topLeft: Radius.circular(30),
// topRight: Radius.circular(30),
// ),
// ),
child: SingleChildScrollView(
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
physics: BouncingScrollPhysics(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
......@@ -174,7 +178,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,
......@@ -230,9 +233,11 @@ class _OnboardingScreenState extends BaseState<OnboardingScreen> with BasicState
),
],
),
SizedBox(height: 0),
],
),
),
),
],
),
),
......
import 'package:get/get.dart';
import 'package:mypoint_flutter_app/extensions/string_extension.dart';
import 'package:mypoint_flutter_app/networking/restful_api_request.dart';
import 'package:mypoint_flutter_app/onboading/model/check_phone_response_model.dart';
import '../base/base_response_model.dart';
import '../base/restful_api_viewmodel.dart';
import '../../base/base_response_model.dart';
import '../../base/restful_api_viewmodel.dart';
import 'model/check_phone_response_model.dart';
import 'model/onboarding_info_model.dart';
class OnboardingViewModel extends RestfulApiViewModel {
......
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pin_code_fields/pin_code_fields.dart';
import '../../resouce/base_color.dart';
import 'otp_view_model.dart';
class OtpScreen extends StatefulWidget {
final IOtpRepository repository;
const OtpScreen({super.key, required this.repository});
@override
State<OtpScreen> createState() => _OtpScreenState();
}
class _OtpScreenState extends State<OtpScreen> {
@override
Widget build(BuildContext context) {
final otpVM = Get.put(OtpViewModel(widget.repository));
return Scaffold(
appBar: AppBar(
centerTitle: true,
leading: IconButton(icon: const Icon(Icons.arrow_back_ios), onPressed: () => Navigator.pop(context)),
),
body: SafeArea(
child: GestureDetector(
onTap: () => FocusScope.of(context).unfocus(),
child: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text("Nhập mã xác thực OTP", style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
const SizedBox(height: 12),
_buildWelcomeText(otpVM),
const SizedBox(height: 32),
_buildPinCodeFields(otpVM),
const SizedBox(height: 16),
_buildErrorText(otpVM),
const SizedBox(height: 16),
_buildResendOtp(otpVM),
],
),
),
),
),
);
}
/// PinCodeTextField cho 6 ô
Widget _buildPinCodeFields(OtpViewModel vm) {
double screenWidth = MediaQuery.of(context).size.width;
// return Obx(() {
return PinCodeTextField(
appContext: Get.context!,
length: 6,
obscureText: false,
cursorColor: Colors.black,
keyboardType: TextInputType.number,
autoFocus: true,
animationType: AnimationType.none,
pinTheme: PinTheme(
shape: PinCodeFieldShape.box,
borderRadius: BorderRadius.circular(6),
fieldHeight: screenWidth / 6 - 12,
fieldWidth: screenWidth / 6 - 12,
activeColor: Colors.blue,
inactiveColor: Colors.grey.shade300,
selectedColor: Colors.blueAccent,
),
onChanged: (value) {
vm.otpCode.value = value;
vm.errorMessage.value = ""; // clear lỗi khi gõ
},
onCompleted: (value) {
vm.otpCode.value = value;
},
);
// });
}
Widget _buildErrorText(OtpViewModel vm) {
// Chỉ bọc Obx ở đây vì ta đọc vm.errorMessage
return Obx(() {
final error = vm.errorMessage.value;
if (error.isEmpty) {
return const SizedBox.shrink();
}
return Text(error, style: const TextStyle(color: Colors.red));
});
}
/// "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;
final textTime = vm.countdownText;
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextButton(
onPressed: canResend ? vm.onResendOtp : null,
child: Text(
"Gửi lại OTP ${!canResend ? "($textTime)" : ""}",
style: TextStyle(color: canResend ? Colors.blue : Colors.grey),
),
),
],
);
});
}
Widget _buildWelcomeText(OtpViewModel vm) {
return RichText(
text: TextSpan(
style: TextStyle(fontSize: 14, color: BaseColor.second500),
children: [
const TextSpan(text: "Mã OTP đã được gửi về số điện thoại "),
TextSpan(
text: "0999999999", //"${vm.phoneNumber}",
style: const TextStyle(fontWeight: FontWeight.w500, color: BaseColor.primary500),
),
],
),
);
}
}
import 'dart:async';
import 'package:get/get.dart';
import 'package:get/get_state_manager/src/simple/get_controllers.dart';
// i_otp_repository.dart
abstract class IOtpRepository {
Future<void> sendOtp();
Future<bool> verifyOtp(String otpCode);
Future<void> resendOtp();
}
class OtpViewModel extends GetxController {
final IOtpRepository repository;
// Mã OTP người dùng nhập
var otpCode = "".obs;
// Lỗi (nếu OTP sai)
var errorMessage = "".obs;
// Đếm ngược thời gian resend
final int _maxCountdown = 150; // 2 phút 30 giây
var currentCountdown = 0.obs;
Timer? _timer;
OtpViewModel(this.repository);
@override
void onInit() {
super.onInit();
// Gửi OTP ngay khi vào màn hình (tuỳ logic)
sendOtp();
startCountdown();
}
@override
void onClose() {
_timer?.cancel();
super.onClose();
}
/// Gửi OTP (lần đầu)
Future<void> sendOtp() async {
try {
await repository.sendOtp();
// Reset countdown
startCountdown();
} catch (e) {
errorMessage.value = "Gửi OTP thất bại. Vui lòng thử lại.";
}
}
// Đếm ngược 2:30
void startCountdown() {
currentCountdown.value = _maxCountdown;
_timer?.cancel();
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (currentCountdown.value <= 0) {
timer.cancel();
} else {
currentCountdown.value--;
}
});
}
String get countdownText {
final m = currentCountdown.value ~/ 60;
final s = currentCountdown.value % 60;
final sStr = s < 10 ? "0$s" : "$s";
return "$m:$sStr";
}
// User nhập OTP
void onOtpChanged(String value) {
otpCode.value = value;
errorMessage.value = ""; // clear lỗi cũ
}
// Submit OTP
Future<void> onSubmitOtp() async {
if (otpCode.value.length < 6) {
errorMessage.value = "Vui lòng nhập đủ 6 ký tự";
return;
}
try {
final success = await repository.verifyOtp(otpCode.value);
if (success) {
errorMessage.value = "";
// TODO: Navigate or do something
// Example: Get.offAllNamed("/home");
print("OTP chính xác! Điều hướng tiếp...");
} else {
errorMessage.value = "Mã OTP không chính xác";
}
} catch (e) {
errorMessage.value = "Xác thực OTP thất bại. Thử lại.";
}
}
// Bấm "Gửi lại OTP"
Future<void> onResendOtp() async {
if (currentCountdown.value > 0) {
// Chưa hết thời gian => return
return;
}
try {
await repository.resendOtp();
startCountdown();
} catch (e) {
errorMessage.value = "Gửi lại OTP thất bại. Thử lại.";
}
}
}
\ No newline at end of file
// sign_up_otp_repository.dart
import 'package:flutter/material.dart';
import '../otp/otp_view_model.dart';
class SignUpOtpRepository implements IOtpRepository {
final String phoneNumber;
SignUpOtpRepository(this.phoneNumber);
@override
Future<void> sendOtp() async {
debugPrint("[SignUpOtpRepository] Gọi API gửi OTP cho luồng đăng ký");
// TODO: call API real
await Future.delayed(const Duration(seconds: 1));
}
@override
Future<bool> verifyOtp(String otpCode) async {
debugPrint("[SignUpOtpRepository] Gọi API verify OTP cho luồng đăng ký");
// TODO: call API real, giả lập OTP "123456" mới đúng
await Future.delayed(const Duration(seconds: 1));
return otpCode == "123456";
}
@override
Future<void> resendOtp() async {
debugPrint("[SignUpOtpRepository] Gọi API resend OTP đăng ký");
// TODO: call API real
await Future.delayed(const Duration(seconds: 1));
}
}
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
......@@ -7,10 +6,11 @@ import 'package:get/get_core/src/get_main.dart';
import 'package:mypoint_flutter_app/configs/api_paths.dart';
import 'package:mypoint_flutter_app/dio_http_service/api_helper.dart';
import 'package:mypoint_flutter_app/networking/api_service.dart';
import 'package:mypoint_flutter_app/splash_screen/splash_screen_view_model.dart';
import 'package:mypoint_flutter_app/screen/splash/splash_screen_view_model.dart';
import '../../model/check_update_response_model.dart';
import '../onboarding/onboarding_screen.dart';
import '../model/check_update_response_model.dart';
import '../onboading/onboarding_screen.dart';
class SplashScreen extends StatefulWidget {
const SplashScreen({super.key});
......
import 'package:get/get.dart';
import 'package:mypoint_flutter_app/base/restful_api_viewmodel.dart';
import 'package:mypoint_flutter_app/networking/restful_api_request.dart';
import '../base/base_response_model.dart';
import '../model/check_update_response_model.dart';
import '../model/update_response_object.dart';
import '../../base/base_response_model.dart';
import '../../model/update_response_object.dart';
class SplashScreenViewModel extends RestfulApiViewModel {
var infoAppUpdate = BaseResponseModel<UpdateResponseObject>().obs;
......
......@@ -45,6 +45,8 @@ dependencies:
device_info_plus: ^9.0.3
uuid: ^4.3.3
flutter_svg:
local_auth:
pin_code_fields:
dev_dependencies:
flutter_test:
......
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