Commit 7d37c9c6 authored by DatHV's avatar DatHV
Browse files

update otp follow

parent 1257980d
...@@ -4,4 +4,9 @@ class APIPaths { ...@@ -4,4 +4,9 @@ class APIPaths {
static const String checkUpdate = "/version-management-service/api/v1.0/check-customer-software-update"; static const String checkUpdate = "/version-management-service/api/v1.0/check-customer-software-update";
static const String getOnboardingInfo = "/resource/api/v2.0/intro-screen"; static const String getOnboardingInfo = "/resource/api/v2.0/intro-screen";
static const String checkPhoneNumber = "/user/api/v2.0/account/users/checkPhoneNumber"; 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";
} }
...@@ -7,6 +7,7 @@ import '../configs/device_info.dart'; ...@@ -7,6 +7,7 @@ import '../configs/device_info.dart';
import '../model/update_response_object.dart'; import '../model/update_response_object.dart';
import '../screen/onboarding/model/check_phone_response_model.dart'; import '../screen/onboarding/model/check_phone_response_model.dart';
import '../screen/onboarding/model/onboarding_info_model.dart'; import '../screen/onboarding/model/onboarding_info_model.dart';
import '../screen/otp/otp_verify_response_model.dart';
import 'model_maker.dart'; import 'model_maker.dart';
extension RestfullAPIClientAllApi on RestfulAPIClient { extension RestfullAPIClientAllApi on RestfulAPIClient {
...@@ -41,4 +42,24 @@ extension RestfullAPIClientAllApi on RestfulAPIClient { ...@@ -41,4 +42,24 @@ extension RestfullAPIClientAllApi on RestfulAPIClient {
(data) => CheckPhoneResponseModel.fromJson(data as Json), (data) => CheckPhoneResponseModel.fromJson(data as Json),
); );
} }
Future<BaseResponseModel<OTPVerifyResponseModel>> verifyOTP(String otp, String mfaToken) async {
final body = {"otp": otp, "mfaToken": mfaToken,};
return requestNormal(
APIPaths.verifyOtpWithAction,
Method.POST,
body,
(data) => OTPVerifyResponseModel.fromJson(data as Json),
);
}
Future<BaseResponseModel<OTPResendResponseModel>> resendOTP(String mfaToken) async {
final body = {"mfaToken": mfaToken,};
return requestNormal(
APIPaths.retryOtpWithAction,
Method.POST,
body,
(data) => OTPResendResponseModel.fromJson(data as Json),
);
}
} }
...@@ -9,7 +9,7 @@ import '../../configs/constants.dart'; ...@@ -9,7 +9,7 @@ import '../../configs/constants.dart';
import '../../resouce/base_color.dart'; import '../../resouce/base_color.dart';
import '../login/login_screen.dart'; import '../login/login_screen.dart';
import '../otp/otp_screen.dart'; import '../otp/otp_screen.dart';
import '../signup/signup_otp_repository.dart'; import '../otp/verify_otp_repository.dart';
import 'model/check_phone_response_model.dart'; import 'model/check_phone_response_model.dart';
import 'onboarding_view_model.dart'; import 'onboarding_view_model.dart';
...@@ -22,7 +22,6 @@ class OnboardingScreen extends BaseScreen { ...@@ -22,7 +22,6 @@ class OnboardingScreen extends BaseScreen {
class _OnboardingScreenState extends BaseState<OnboardingScreen> with BasicState { class _OnboardingScreenState extends BaseState<OnboardingScreen> with BasicState {
final OnboardingViewModel _viewModel = Get.find<OnboardingViewModel>(); final OnboardingViewModel _viewModel = Get.find<OnboardingViewModel>();
final FocusNode _focusNode = FocusNode(); final FocusNode _focusNode = FocusNode();
@override @override
...@@ -34,8 +33,13 @@ class _OnboardingScreenState extends BaseState<OnboardingScreen> with BasicState ...@@ -34,8 +33,13 @@ class _OnboardingScreenState extends BaseState<OnboardingScreen> with BasicState
hideKeyboard(); hideKeyboard();
// Get.to(() => const LoginScreen()); // Get.to(() => const LoginScreen());
Get.to(() => OtpScreen( Get.to(() => OtpScreen(
repository: SignUpOtpRepository(_viewModel.phoneNumber.value), repository: VerifyOtpRepository(
)); _viewModel.phoneNumber.value,
response.data?.otpTtl ?? 0,
response.data?.mfaToken ?? "",
),
),
);
}); });
// _handleResponseCheckPhoneNumber(response.data); // _handleResponseCheckPhoneNumber(response.data);
// _handleResponseError(response); // _handleResponseError(response);
...@@ -128,9 +132,7 @@ class _OnboardingScreenState extends BaseState<OnboardingScreen> with BasicState ...@@ -128,9 +132,7 @@ class _OnboardingScreenState extends BaseState<OnboardingScreen> with BasicState
/// 📌 Ô nhập số điện thoại /// 📌 Ô nhập số điện thoại
TextField( TextField(
inputFormatters: [ inputFormatters: [LengthLimitingTextInputFormatter(10)],
LengthLimitingTextInputFormatter(10)
],
// maxLength: 10, // maxLength: 10,
focusNode: _focusNode, focusNode: _focusNode,
keyboardType: TextInputType.phone, keyboardType: TextInputType.phone,
...@@ -144,7 +146,7 @@ class _OnboardingScreenState extends BaseState<OnboardingScreen> with BasicState ...@@ -144,7 +146,7 @@ class _OnboardingScreenState extends BaseState<OnboardingScreen> with BasicState
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
borderSide: BorderSide.none, borderSide: BorderSide.none,
), ),
prefixIcon: const Icon(Icons.phone, color: Color(0xFF9DA4AE)) prefixIcon: const Icon(Icons.phone, color: Color(0xFF9DA4AE)),
), ),
onChanged: _viewModel.updatePhoneNumber, onChanged: _viewModel.updatePhoneNumber,
), ),
...@@ -158,7 +160,7 @@ class _OnboardingScreenState extends BaseState<OnboardingScreen> with BasicState ...@@ -158,7 +160,7 @@ class _OnboardingScreenState extends BaseState<OnboardingScreen> with BasicState
onPressed: onPressed:
_viewModel.isButtonEnabled _viewModel.isButtonEnabled
? () { ? () {
_viewModel.checkPhoneNumber(); _viewModel.checkPhoneNumber();
} }
: null, : null,
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
...@@ -178,6 +180,7 @@ class _OnboardingScreenState extends BaseState<OnboardingScreen> with BasicState ...@@ -178,6 +180,7 @@ class _OnboardingScreenState extends BaseState<OnboardingScreen> with BasicState
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
/// 📌 Checkbox + Điều khoản sử dụng + Chính sách bảo mật /// 📌 Checkbox + Điều khoản sử dụng + Chính sách bảo mật
Row( Row(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
......
import 'package:json_annotation/json_annotation.dart';
part 'otp_claim_verify_response_model.g.dart';
@JsonSerializable()
class OTPClaimVerifyResponseModel {
String? action;
String? username;
OTPClaimVerifyResponseModel({
required this.action,
required this.username,
});
factory OTPClaimVerifyResponseModel.fromJson(Map<String, dynamic> json) => _$OTPClaimVerifyResponseModelFromJson(json);
Map<String, dynamic> toJson() => _$OTPClaimVerifyResponseModelToJson(this);
}
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'otp_claim_verify_response_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
OTPClaimVerifyResponseModel _$OTPClaimVerifyResponseModelFromJson(
Map<String, dynamic> json,
) => OTPClaimVerifyResponseModel(
action: json['action'] as String?,
username: json['username'] as String?,
);
Map<String, dynamic> _$OTPClaimVerifyResponseModelToJson(
OTPClaimVerifyResponseModel instance,
) => <String, dynamic>{
'action': instance.action,
'username': instance.username,
};
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pin_code_fields/pin_code_fields.dart'; 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 '../../resouce/base_color.dart';
import 'otp_view_model.dart'; import 'otp_view_model.dart';
class OtpScreen extends StatefulWidget { class OtpScreen extends BaseScreen {
final IOtpRepository repository; final IOtpRepository repository;
const OtpScreen({super.key, required this.repository}); const OtpScreen({super.key, required this.repository});
@override @override
State<OtpScreen> createState() => _OtpScreenState(); State<OtpScreen> createState() => _OtpScreenState();
} }
class _OtpScreenState extends State<OtpScreen> { class _OtpScreenState extends BaseState<OtpScreen> with BasicState {
@override @override
Widget build(BuildContext context) { Widget createBody() {
final otpVM = Get.put(OtpViewModel(widget.repository)); final otpVM = Get.put(OtpViewModel(widget.repository));
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
centerTitle: true, centerTitle: true,
...@@ -31,7 +32,7 @@ class _OtpScreenState extends State<OtpScreen> { ...@@ -31,7 +32,7 @@ class _OtpScreenState extends State<OtpScreen> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
const Text("Nhập mã xác thực OTP", style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)), const Text("Nhập mã xác thực OTP", style: TextStyle(fontSize: 32, fontWeight: FontWeight.bold)),
const SizedBox(height: 12), const SizedBox(height: 12),
_buildWelcomeText(otpVM), _buildWelcomeText(otpVM),
const SizedBox(height: 32), const SizedBox(height: 32),
...@@ -71,10 +72,11 @@ class _OtpScreenState extends State<OtpScreen> { ...@@ -71,10 +72,11 @@ class _OtpScreenState extends State<OtpScreen> {
), ),
onChanged: (value) { onChanged: (value) {
vm.otpCode.value = value; vm.otpCode.value = value;
vm.errorMessage.value = ""; // clear lỗi khi gõ vm.errorMessage.value = "1111111"; // clear lỗi khi gõ
}, },
onCompleted: (value) { onCompleted: (value) {
vm.otpCode.value = value; vm.otpCode.value = value;
vm.onSubmitOtp;
}, },
); );
// }); // });
......
import 'package:json_annotation/json_annotation.dart';
import 'otp_claim_verify_response_model.dart';
part 'otp_verify_response_model.g.dart';
@JsonSerializable()
class OTPVerifyResponseModel {
OTPClaimVerifyResponseModel? claim;
OTPVerifyResponseModel({
this.claim,
});
factory OTPVerifyResponseModel.fromJson(Map<String, dynamic> json) => _$OTPVerifyResponseModelFromJson(json);
Map<String, dynamic> toJson() => _$OTPVerifyResponseModelToJson(this);
}
@JsonSerializable()
class OTPResendResponseModel {
@JsonKey(name: "otp_ttl")
int? otpTtl;
OTPResendResponseModel({
this.otpTtl,
});
factory OTPResendResponseModel.fromJson(Map<String, dynamic> json) => _$OTPResendResponseModelFromJson(json);
Map<String, dynamic> toJson() => _$OTPResendResponseModelToJson(this);
}
\ No newline at end of file
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'otp_verify_response_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
OTPVerifyResponseModel _$OTPVerifyResponseModelFromJson(
Map<String, dynamic> json,
) => OTPVerifyResponseModel(
claim:
json['claim'] == null
? null
: OTPClaimVerifyResponseModel.fromJson(
json['claim'] as Map<String, dynamic>,
),
);
Map<String, dynamic> _$OTPVerifyResponseModelToJson(
OTPVerifyResponseModel instance,
) => <String, dynamic>{'claim': instance.claim};
OTPResendResponseModel _$OTPResendResponseModelFromJson(
Map<String, dynamic> json,
) => OTPResendResponseModel(otpTtl: json['otp_ttl'] as int?);
Map<String, dynamic> _$OTPResendResponseModelToJson(
OTPResendResponseModel instance,
) => <String, dynamic>{'otp_ttl': instance.otpTtl};
import 'dart:async'; import 'dart:async';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:get/get_state_manager/src/simple/get_controllers.dart'; import 'package:mypoint_flutter_app/base/base_response_model.dart';
import 'otp_verify_response_model.dart';
// i_otp_repository.dart // i_otp_repository.dart
abstract class IOtpRepository { abstract class IOtpRepository {
Future<void> sendOtp(); Future<void> sendOtp();
Future<bool> verifyOtp(String otpCode); Future<BaseResponseModel<OTPVerifyResponseModel>> verifyOtp(String otpCode);
Future<void> resendOtp(); Future<void> resendOtp();
late String phoneNumber;
} }
class OtpViewModel extends GetxController { class OtpViewModel extends GetxController {
final IOtpRepository repository; final IOtpRepository repository;
// Mã OTP người dùng nhập
var otpCode = "".obs; var otpCode = "".obs;
// Lỗi (nếu OTP sai)
var errorMessage = "".obs; var errorMessage = "".obs;
// Đếm ngược thời gian resend
final int _maxCountdown = 150; // 2 phút 30 giây
var currentCountdown = 0.obs; var currentCountdown = 0.obs;
final int _maxCountdown = 150; // 2 phút 30 giây
Timer? _timer; Timer? _timer;
OtpViewModel(this.repository); OtpViewModel(this.repository);
...@@ -28,7 +24,6 @@ class OtpViewModel extends GetxController { ...@@ -28,7 +24,6 @@ class OtpViewModel extends GetxController {
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
// Gửi OTP ngay khi vào màn hình (tuỳ logic)
sendOtp(); sendOtp();
startCountdown(); startCountdown();
} }
...@@ -39,18 +34,15 @@ class OtpViewModel extends GetxController { ...@@ -39,18 +34,15 @@ class OtpViewModel extends GetxController {
super.onClose(); super.onClose();
} }
/// Gửi OTP (lần đầu)
Future<void> sendOtp() async { Future<void> sendOtp() async {
try { try {
await repository.sendOtp(); await repository.sendOtp();
// Reset countdown
startCountdown(); startCountdown();
} catch (e) { } catch (e) {
errorMessage.value = "Gửi OTP thất bại. Vui lòng thử lại."; errorMessage.value = "Gửi OTP thất bại. Vui lòng thử lại.";
} }
} }
// Đếm ngược 2:30
void startCountdown() { void startCountdown() {
currentCountdown.value = _maxCountdown; currentCountdown.value = _maxCountdown;
_timer?.cancel(); _timer?.cancel();
...@@ -70,39 +62,31 @@ class OtpViewModel extends GetxController { ...@@ -70,39 +62,31 @@ class OtpViewModel extends GetxController {
return "$m:$sStr"; return "$m:$sStr";
} }
// User nhập OTP
void onOtpChanged(String value) { void onOtpChanged(String value) {
otpCode.value = value; otpCode.value = value;
errorMessage.value = ""; // clear lỗi cũ errorMessage.value = "";
} }
// Submit OTP
Future<void> onSubmitOtp() async { Future<void> onSubmitOtp() async {
if (otpCode.value.length < 6) { if (otpCode.value.length < 6) {
errorMessage.value = "Vui lòng nhập đủ 6 ký tự"; errorMessage.value = "Nhập đủ 6 ký tự";
return; return;
} }
try { try {
final success = await repository.verifyOtp(otpCode.value); final response = await repository.verifyOtp(otpCode.value);
if (success) { if (response.isSuccess) {
errorMessage.value = ""; errorMessage.value = "response.isSuccess";
// TODO: Navigate or do something
// Example: Get.offAllNamed("/home");
print("OTP chính xác! Điều hướng tiếp...");
} else { } else {
errorMessage.value = "Mã OTP không chính xác"; errorMessage.value = response.errorMessage ?? "";
} }
} catch (e) { } catch (e) {
errorMessage.value = "Xác thực OTP thất bại. Thử lại."; // Bắt lỗi do repository throw
errorMessage.value = "Xác thực thất bại: $e";
} }
} }
// Bấm "Gửi lại OTP"
Future<void> onResendOtp() async { Future<void> onResendOtp() async {
if (currentCountdown.value > 0) { if (currentCountdown.value > 0) return;
// Chưa hết thời gian => return
return;
}
try { try {
await repository.resendOtp(); await repository.resendOtp();
startCountdown(); startCountdown();
......
// sign_up_otp_repository.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:mypoint_flutter_app/networking/restful_api_request.dart';
import '../../base/base_response_model.dart';
import '../../base/restful_api_viewmodel.dart';
import 'otp_verify_response_model.dart';
import 'otp_view_model.dart';
class VerifyOtpRepository extends RestfulApiViewModel implements IOtpRepository {
int otpTtl;
final String mfaToken;
var otpVerifyResponse = BaseResponseModel<OTPVerifyResponseModel>().obs;
VerifyOtpRepository(this.phoneNumber, this.otpTtl, this.mfaToken);
@override
String phoneNumber;
@override
Future<void> sendOtp() async {}
@override
Future<BaseResponseModel<OTPVerifyResponseModel>> verifyOtp(String otpCode) async {
showLoading();
return client.verifyOTP(otpCode, mfaToken).then((value) {
hideLoading();
return value;
});
}
@override
Future<void> resendOtp() async {
showLoading();
return client.resendOTP(mfaToken).then((value) {
otpTtl = value.data?.otpTtl ?? 0;
hideLoading();
});
}
}
// 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));
}
}
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