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

update otp follow

parent 1257980d
......@@ -4,4 +4,9 @@ class APIPaths {
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 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';
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 'model_maker.dart';
extension RestfullAPIClientAllApi on RestfulAPIClient {
......@@ -41,4 +42,24 @@ extension RestfullAPIClientAllApi on RestfulAPIClient {
(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';
import '../../resouce/base_color.dart';
import '../login/login_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 'onboarding_view_model.dart';
......@@ -22,7 +22,6 @@ class OnboardingScreen extends BaseScreen {
class _OnboardingScreenState extends BaseState<OnboardingScreen> with BasicState {
final OnboardingViewModel _viewModel = Get.find<OnboardingViewModel>();
final FocusNode _focusNode = FocusNode();
@override
......@@ -34,8 +33,13 @@ class _OnboardingScreenState extends BaseState<OnboardingScreen> with BasicState
hideKeyboard();
// Get.to(() => const LoginScreen());
Get.to(() => OtpScreen(
repository: SignUpOtpRepository(_viewModel.phoneNumber.value),
));
repository: VerifyOtpRepository(
_viewModel.phoneNumber.value,
response.data?.otpTtl ?? 0,
response.data?.mfaToken ?? "",
),
),
);
});
// _handleResponseCheckPhoneNumber(response.data);
// _handleResponseError(response);
......@@ -128,9 +132,7 @@ class _OnboardingScreenState extends BaseState<OnboardingScreen> with BasicState
/// 📌 Ô nhập số điện thoại
TextField(
inputFormatters: [
LengthLimitingTextInputFormatter(10)
],
inputFormatters: [LengthLimitingTextInputFormatter(10)],
// maxLength: 10,
focusNode: _focusNode,
keyboardType: TextInputType.phone,
......@@ -144,7 +146,7 @@ class _OnboardingScreenState extends BaseState<OnboardingScreen> with BasicState
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide.none,
),
prefixIcon: const Icon(Icons.phone, color: Color(0xFF9DA4AE))
prefixIcon: const Icon(Icons.phone, color: Color(0xFF9DA4AE)),
),
onChanged: _viewModel.updatePhoneNumber,
),
......@@ -158,7 +160,7 @@ class _OnboardingScreenState extends BaseState<OnboardingScreen> with BasicState
onPressed:
_viewModel.isButtonEnabled
? () {
_viewModel.checkPhoneNumber();
_viewModel.checkPhoneNumber();
}
: null,
style: ElevatedButton.styleFrom(
......@@ -178,6 +180,7 @@ 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,
......
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:get/get.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 'otp_view_model.dart';
class OtpScreen extends StatefulWidget {
class OtpScreen extends BaseScreen {
final IOtpRepository repository;
const OtpScreen({super.key, required this.repository});
@override
State<OtpScreen> createState() => _OtpScreenState();
}
class _OtpScreenState extends State<OtpScreen> {
class _OtpScreenState extends BaseState<OtpScreen> with BasicState {
@override
Widget build(BuildContext context) {
Widget createBody() {
final otpVM = Get.put(OtpViewModel(widget.repository));
return Scaffold(
appBar: AppBar(
centerTitle: true,
......@@ -31,7 +32,7 @@ class _OtpScreenState extends State<OtpScreen> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
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),
_buildWelcomeText(otpVM),
const SizedBox(height: 32),
......@@ -71,10 +72,11 @@ class _OtpScreenState extends State<OtpScreen> {
),
onChanged: (value) {
vm.otpCode.value = value;
vm.errorMessage.value = ""; // clear lỗi khi gõ
vm.errorMessage.value = "1111111"; // clear lỗi khi gõ
},
onCompleted: (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 '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
abstract class IOtpRepository {
Future<void> sendOtp();
Future<bool> verifyOtp(String otpCode);
Future<BaseResponseModel<OTPVerifyResponseModel>> verifyOtp(String otpCode);
Future<void> resendOtp();
late String phoneNumber;
}
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;
final int _maxCountdown = 150; // 2 phút 30 giây
Timer? _timer;
OtpViewModel(this.repository);
......@@ -28,7 +24,6 @@ class OtpViewModel extends GetxController {
@override
void onInit() {
super.onInit();
// Gửi OTP ngay khi vào màn hình (tuỳ logic)
sendOtp();
startCountdown();
}
......@@ -39,18 +34,15 @@ class OtpViewModel extends GetxController {
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();
......@@ -70,39 +62,31 @@ class OtpViewModel extends GetxController {
return "$m:$sStr";
}
// User nhập OTP
void onOtpChanged(String value) {
otpCode.value = value;
errorMessage.value = ""; // clear lỗi cũ
errorMessage.value = "";
}
// Submit OTP
Future<void> onSubmitOtp() async {
if (otpCode.value.length < 6) {
errorMessage.value = "Vui lòng nhập đủ 6 ký tự";
errorMessage.value = "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...");
final response = await repository.verifyOtp(otpCode.value);
if (response.isSuccess) {
errorMessage.value = "response.isSuccess";
} else {
errorMessage.value = "Mã OTP không chính xác";
errorMessage.value = response.errorMessage ?? "";
}
} 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 {
if (currentCountdown.value > 0) {
// Chưa hết thời gian => return
return;
}
if (currentCountdown.value > 0) return;
try {
await repository.resendOtp();
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