Skip to content
GitLab
Menu
Projects
Groups
Snippets
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Menu
Open sidebar
Hoàng Văn Đạt
mypoint_flutter_app
Commits
8ec716d3
Commit
8ec716d3
authored
Mar 05, 2025
by
DatHV
Browse files
update logic
parent
7d37c9c6
Changes
26
Hide whitespace changes
Inline
Side-by-side
devtools_options.yaml
0 → 100644
View file @
8ec716d3
description
:
This file stores settings for Dart & Flutter DevTools.
documentation
:
https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions
:
lib/configs/api_paths.dart
View file @
8ec716d3
...
@@ -6,7 +6,6 @@ class APIPaths {
...
@@ -6,7 +6,6 @@ class APIPaths {
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
verifyOtpWithAction
=
"/iam/v2/authentication/otp/verifyWithAction"
;
static
const
String
retryOtpWithAction
=
"/iam/v2/authentication/otp/retry"
;
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"
;
}
}
lib/configs/constants.dart
View file @
8ec716d3
class
Constants
{
class
Constants
{
static
get
commonError
=>
"Có lỗi xảy ra. Vui lòng thử lại!"
;
static
get
commonError
=>
"Có lỗi xảy ra. Vui lòng thử lại!"
;
static
var
otpTtl
=
180
;
// device key
// device key
}
}
class
ErrorCodes
{
class
ErrorCodes
{
...
...
lib/main.dart
View file @
8ec716d3
import
'package:flutter/material.dart'
;
import
'package:flutter/material.dart'
;
import
'package:get/get.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'
;
import
'package:mypoint_flutter_app/screen/splash/splash_screen.dart'
;
void
main
(
)
{
void
main
(
)
{
...
...
lib/networking/restful_api_request.dart
View file @
8ec716d3
import
'dart:io'
;
import
'dart:io'
;
import
'package:mypoint_flutter_app/configs/api_paths.dart'
;
import
'package:mypoint_flutter_app/configs/api_paths.dart'
;
import
'package:mypoint_flutter_app/base/base_response_model.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/extensions/string_extension.dart'
;
import
'package:mypoint_flutter_app/networking/restful_api.dart'
;
import
'package:mypoint_flutter_app/networking/restful_api.dart'
;
import
'../configs/device_info.dart'
;
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
'../screen/otp/model/otp_verify_response_model.dart'
;
import
'../screen/splash/splash_screen_viewmodel.dart'
;
import
'model_maker.dart'
;
import
'model_maker.dart'
;
extension
RestfullAPIClientAllApi
on
RestfulAPIClient
{
extension
RestfullAPIClientAllApi
on
RestfulAPIClient
{
...
@@ -62,4 +64,26 @@ extension RestfullAPIClientAllApi on RestfulAPIClient {
...
@@ -62,4 +64,26 @@ extension RestfullAPIClientAllApi on RestfulAPIClient {
(
data
)
=>
OTPResendResponseModel
.
fromJson
(
data
as
Json
),
(
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
),
);
}
}
}
lib/resouce/text_style.dart
View file @
8ec716d3
import
'package:flutter/material.dart'
;
import
'package:flutter/material.dart'
;
import
'package:mypoint_flutter_app/resouce/base_color.dart'
;
enum
TextStyleIconAndText
{
enum
TextStyleIconAndText
{
header1
,
header1
,
...
@@ -81,4 +82,33 @@ TextStyle getTextStyle(TextStyleIconAndText? textType) {
...
@@ -81,4 +82,33 @@ TextStyle getTextStyle(TextStyleIconAndText? textType) {
default
:
default
:
return
textNormal
;
return
textNormal
;
}
}
}
}
\ No newline at end of file
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
,
);
}
lib/screen/create_pass/create_pass_screen.dart
0 → 100644
View file @
8ec716d3
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
,
),
),
);
}
}
lib/screen/create_pass/create_pass_viewmodel.dart
0 → 100644
View file @
8ec716d3
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
"
;
}
}
}
lib/screen/create_pass/signup_create_password_repository.dart
0 → 100644
View file @
8ec716d3
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
();
}
}
lib/screen/login/login_screen.dart
View file @
8ec716d3
...
@@ -5,10 +5,14 @@ import '../../base/base_screen.dart';
...
@@ -5,10 +5,14 @@ import '../../base/base_screen.dart';
import
'../../base/basic_state.dart'
;
import
'../../base/basic_state.dart'
;
import
'../../permission/biometric_manager.dart'
;
import
'../../permission/biometric_manager.dart'
;
import
'../../resouce/base_color.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
{
class
LoginScreen
extends
BaseScreen
{
const
LoginScreen
({
super
.
key
});
final
String
phoneNumber
;
final
String
?
fullName
;
const
LoginScreen
({
super
.
key
,
required
this
.
phoneNumber
,
this
.
fullName
});
@override
@override
State
<
LoginScreen
>
createState
()
=>
_LoginScreenState
();
State
<
LoginScreen
>
createState
()
=>
_LoginScreenState
();
...
@@ -16,56 +20,41 @@ class LoginScreen extends BaseScreen {
...
@@ -16,56 +20,41 @@ class LoginScreen extends BaseScreen {
class
_LoginScreenState
extends
BaseState
<
LoginScreen
>
with
BasicState
{
class
_LoginScreenState
extends
BaseState
<
LoginScreen
>
with
BasicState
{
final
TextEditingController
_phoneController
=
TextEditingController
();
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
@override
Widget
createBody
()
{
Widget
createBody
()
{
// Khởi tạo hoặc lấy LoginViewModel
final
loginVM
=
Get
.
put
(
LoginViewModel
());
final
loginVM
=
Get
.
put
(
LoginViewModel
());
return
GestureDetector
(
return
GestureDetector
(
onTap:
hideKeyboard
,
onTap:
hideKeyboard
,
child:
Scaffold
(
child:
Scaffold
(
// Để nội dung nâng lên khi bàn phím xuất hiện
resizeToAvoidBottomInset:
false
,
resizeToAvoidBottomInset:
false
,
appBar:
AppBar
(
appBar:
AppBar
(
automaticallyImplyLeading:
false
,
automaticallyImplyLeading:
false
,
backgroundColor:
Colors
.
white
,
backgroundColor:
Colors
.
white
,
centerTitle:
true
,
centerTitle:
true
,
leading:
IconButton
(
leading:
CustomBackButton
(),
icon:
const
Icon
(
Icons
.
arrow_back_ios
),
actions:
[
SupportButton
()],
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
,
backgroundColor:
Colors
.
white
,
body:
SafeArea
(
body:
SafeArea
(
child:
Stack
(
child:
Stack
(
children:
[
children:
[
// Nội dung cuộn ở dưới
SingleChildScrollView
(
SingleChildScrollView
(
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
16
,
vertical:
16
),
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
16
,
vertical:
16
),
child:
Column
(
child:
Column
(
...
@@ -97,28 +86,26 @@ class _LoginScreenState extends BaseState<LoginScreen> with BasicState {
...
@@ -97,28 +86,26 @@ class _LoginScreenState extends BaseState<LoginScreen> with BasicState {
}
}
Widget
_buildWelcomeText
(
LoginViewModel
vm
)
{
Widget
_buildWelcomeText
(
LoginViewModel
vm
)
{
return
Obx
(()
{
return
RichText
(
return
RichText
(
text:
TextSpan
(
text:
TextSpan
(
style:
const
TextStyle
(
fontSize:
14
,
color:
BaseColor
.
second500
),
style:
const
TextStyle
(
fontSize:
14
,
color:
BaseColor
.
second500
),
children:
[
children:
[
const
TextSpan
(
text:
"Xin chào "
),
const
TextSpan
(
text:
"Chào mừng "
),
TextSpan
(
text:
widget
.
fullName
??
"Quý Khách "
),
TextSpan
(
text:
"
${vm.userName}
"
),
TextSpan
(
const
TextSpan
(
text:
" "
),
text:
widget
.
phoneNumber
,
TextSpan
(
style:
const
TextStyle
(
fontWeight:
FontWeight
.
w500
,
color:
BaseColor
.
primary500
),
text:
"
${vm.phoneNumber}
"
,
),
style:
const
TextStyle
(
fontWeight:
FontWeight
.
w500
,
color:
BaseColor
.
primary500
),
],
),
),
],
);
),
);
});
}
}
Widget
_buildPasswordField
(
LoginViewModel
vm
)
{
Widget
_buildPasswordField
(
LoginViewModel
vm
)
{
return
Obx
(()
{
return
Obx
(()
{
return
TextField
(
return
TextField
(
controller:
_phoneController
,
controller:
_phoneController
,
focusNode:
_focusNode
,
keyboardType:
TextInputType
.
number
,
keyboardType:
TextInputType
.
number
,
obscureText:
!
vm
.
isPasswordVisible
.
value
,
obscureText:
!
vm
.
isPasswordVisible
.
value
,
onChanged:
vm
.
onPasswordChanged
,
onChanged:
vm
.
onPasswordChanged
,
...
@@ -131,7 +118,15 @@ class _LoginScreenState extends BaseState<LoginScreen> with BasicState {
...
@@ -131,7 +118,15 @@ class _LoginScreenState extends BaseState<LoginScreen> with BasicState {
contentPadding:
const
EdgeInsets
.
symmetric
(
horizontal:
12
,
vertical:
14
),
contentPadding:
const
EdgeInsets
.
symmetric
(
horizontal:
12
,
vertical:
14
),
border:
OutlineInputBorder
(
border:
OutlineInputBorder
(
borderRadius:
BorderRadius
.
circular
(
8
),
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
(
suffixIcon:
IconButton
(
icon:
Icon
(
icon:
Icon
(
...
...
lib/screen/login/login_view
_
model.dart
→
lib/screen/login/login_viewmodel.dart
View file @
8ec716d3
import
'package:get/get.dart'
;
import
'package:get/get.dart'
;
import
'package:flutter/material.dart'
;
import
'package:flutter/material.dart'
;
import
'package:mypoint_flutter_app/screen/onboarding/onboarding_screen.dart'
;
import
'../../base/restful_api_viewmodel.dart'
;
import
'../../base/restful_api_viewmodel.dart'
;
import
'../../permission/biometric_manager.dart'
;
import
'../../permission/biometric_manager.dart'
;
...
@@ -17,12 +18,6 @@ class LoginViewModel extends RestfulApiViewModel {
...
@@ -17,12 +18,6 @@ class LoginViewModel extends RestfulApiViewModel {
var
loginState
=
LoginState
.
idle
.
obs
;
var
loginState
=
LoginState
.
idle
.
obs
;
var
isPasswordVisible
=
false
.
obs
;
var
isPasswordVisible
=
false
.
obs
;
var
password
=
""
.
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
;
var
biometricType
=
BiometricTypeEnum
.
none
.
obs
;
@override
@override
...
@@ -36,7 +31,6 @@ class LoginViewModel extends RestfulApiViewModel {
...
@@ -36,7 +31,6 @@ class LoginViewModel extends RestfulApiViewModel {
biometricType
.
value
=
type
;
biometricType
.
value
=
type
;
}
}
// Kiểm tra thiết bị có cho phép check biometrics không
Future
<
bool
>
canUseBiometrics
()
async
{
Future
<
bool
>
canUseBiometrics
()
async
{
return
_biometricManager
.
canCheckBiometrics
();
return
_biometricManager
.
canCheckBiometrics
();
}
}
...
@@ -56,7 +50,6 @@ class LoginViewModel extends RestfulApiViewModel {
...
@@ -56,7 +50,6 @@ class LoginViewModel extends RestfulApiViewModel {
void
onLoginPressed
()
{
void
onLoginPressed
()
{
if
(
password
.
value
.
isEmpty
)
return
;
if
(
password
.
value
.
isEmpty
)
return
;
// Ví dụ: Mật khẩu chuẩn là "123456"
// Ví dụ: Mật khẩu chuẩn là "123456"
if
(
password
.
value
==
"123456"
)
{
if
(
password
.
value
==
"123456"
)
{
loginState
.
value
=
LoginState
.
done
;
loginState
.
value
=
LoginState
.
done
;
...
@@ -69,13 +62,13 @@ class LoginViewModel extends RestfulApiViewModel {
...
@@ -69,13 +62,13 @@ class LoginViewModel extends RestfulApiViewModel {
}
}
void
onChangePhonePressed
()
{
void
onChangePhonePressed
()
{
debugPrint
(
"Người dùng chọn Đổi số điện thoại"
);
Get
.
back
();
// TODO: Logic đổi SĐT hoặc chuyển sang màn hình khác
}
}
void
onForgotPassPressed
()
{
void
onForgotPassPressed
()
{
debugPrint
(
"Người dùng chọn Quên mật khẩu?"
);
client
.
getOnboardingInfo
().
then
((
value
)
{
// TODO: Logic quên mật khẩu, ví dụ chuyển sang màn hình recovery
info
.
value
=
value
;
});
}
}
/// Xác thực đăng nhập bằng sinh trắc
/// Xác thực đăng nhập bằng sinh trắc
...
...
lib/screen/onboarding/onboarding_screen.dart
View file @
8ec716d3
...
@@ -7,11 +7,13 @@ import '../../base/base_screen.dart';
...
@@ -7,11 +7,13 @@ import '../../base/base_screen.dart';
import
'../../base/basic_state.dart'
;
import
'../../base/basic_state.dart'
;
import
'../../configs/constants.dart'
;
import
'../../configs/constants.dart'
;
import
'../../resouce/base_color.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
'../login/login_screen.dart'
;
import
'../otp/otp_screen.dart'
;
import
'../otp/otp_screen.dart'
;
import
'../otp/verify_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_viewmodel.dart'
;
class
OnboardingScreen
extends
BaseScreen
{
class
OnboardingScreen
extends
BaseScreen
{
const
OnboardingScreen
({
super
.
key
});
const
OnboardingScreen
({
super
.
key
});
...
@@ -32,16 +34,9 @@ class _OnboardingScreenState extends BaseState<OnboardingScreen> with BasicState
...
@@ -32,16 +34,9 @@ class _OnboardingScreenState extends BaseState<OnboardingScreen> with BasicState
WidgetsBinding
.
instance
.
addPostFrameCallback
((
_
)
{
WidgetsBinding
.
instance
.
addPostFrameCallback
((
_
)
{
hideKeyboard
();
hideKeyboard
();
// Get.to(() => const LoginScreen());
// Get.to(() => const LoginScreen());
Get
.
to
(()
=>
OtpScreen
(
_handleResponseCheckPhoneNumber
(
response
.
data
);
repository:
VerifyOtpRepository
(
_viewModel
.
phoneNumber
.
value
,
response
.
data
?.
otpTtl
??
0
,
response
.
data
?.
mfaToken
??
""
,
),
),
);
});
});
//
_handleResponseCheckPhoneNumber(response.data);
//
// _handleResponseError(response);
// _handleResponseError(response);
});
});
}
}
...
@@ -66,15 +61,21 @@ class _OnboardingScreenState extends BaseState<OnboardingScreen> with BasicState
...
@@ -66,15 +61,21 @@ class _OnboardingScreenState extends BaseState<OnboardingScreen> with BasicState
// show Captcha
// show Captcha
return
;
return
;
}
}
if
((
response
.
mfaToken
??
""
).
isNotEmpty
)
{
if
((
response
.
mfaToken
??
""
).
isNotEmpty
||
(
response
.
nextAction
==
"signup"
)
)
{
// show OTP
// show OTP
Get
.
to
(
()
=>
OtpScreen
(
repository:
VerifyOtpRepository
(
_viewModel
.
phoneNumber
.
value
,
response
!.
otpTtl
??
0
,
response
!.
mfaToken
??
""
,
),
),
);
return
;
return
;
}
}
if
(
response
.
nextAction
==
"login"
)
{
if
(
response
.
nextAction
==
"login"
)
{
return
;
Get
.
to
(()
=>
LoginScreen
(
phoneNumber:
_viewModel
.
phoneNumber
.
value
));
}
if
(
response
.
nextAction
==
"signup"
)
{
return
;
}
}
}
}
...
@@ -87,7 +88,6 @@ class _OnboardingScreenState extends BaseState<OnboardingScreen> with BasicState
...
@@ -87,7 +88,6 @@ class _OnboardingScreenState extends BaseState<OnboardingScreen> with BasicState
resizeToAvoidBottomInset:
false
,
resizeToAvoidBottomInset:
false
,
body:
Stack
(
body:
Stack
(
children:
[
children:
[
/// 📌 Hiển thị background từ API (hoặc ảnh mặc định)
Obx
(
Obx
(
()
=>
Positioned
.
fill
(
()
=>
Positioned
.
fill
(
child:
child:
...
@@ -96,14 +96,10 @@ class _OnboardingScreenState extends BaseState<OnboardingScreen> with BasicState
...
@@ -96,14 +96,10 @@ class _OnboardingScreenState extends BaseState<OnboardingScreen> with BasicState
:
Image
.
asset
(
"assets/images/bg_onboarding.png"
,
fit:
BoxFit
.
cover
),
:
Image
.
asset
(
"assets/images/bg_onboarding.png"
,
fit:
BoxFit
.
cover
),
),
),
),
),
/// 📌 Nội dung chính
SafeArea
(
SafeArea
(
child:
Column
(
child:
Column
(
// mainAxisAlignment: MainAxisAlignment.end,
children:
[
children:
[
Spacer
(),
Spacer
(),
// Expanded(child: Container()),
AnimatedContainer
(
AnimatedContainer
(
duration:
const
Duration
(
milliseconds:
300
),
duration:
const
Duration
(
milliseconds:
300
),
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
16
,
vertical:
32
),
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
16
,
vertical:
32
),
...
@@ -114,7 +110,6 @@ class _OnboardingScreenState extends BaseState<OnboardingScreen> with BasicState
...
@@ -114,7 +110,6 @@ class _OnboardingScreenState extends BaseState<OnboardingScreen> with BasicState
crossAxisAlignment:
CrossAxisAlignment
.
start
,
crossAxisAlignment:
CrossAxisAlignment
.
start
,
mainAxisSize:
MainAxisSize
.
min
,
mainAxisSize:
MainAxisSize
.
min
,
children:
[
children:
[
/// 📌 Tiêu đề (Hiển thị nội dung HTML từ API hoặc mặc định)
Obx
(
Obx
(
()
=>
Visibility
(
()
=>
Visibility
(
visible:
!
_focusNode
.
hasFocus
,
visible:
!
_focusNode
.
hasFocus
,
...
@@ -129,8 +124,6 @@ class _OnboardingScreenState extends BaseState<OnboardingScreen> with BasicState
...
@@ -129,8 +124,6 @@ class _OnboardingScreenState extends BaseState<OnboardingScreen> with BasicState
),
),
),
),
const
SizedBox
(
height:
16
),
const
SizedBox
(
height:
16
),
/// 📌 Ô nhập số điện thoại
TextField
(
TextField
(
inputFormatters:
[
LengthLimitingTextInputFormatter
(
10
)],
inputFormatters:
[
LengthLimitingTextInputFormatter
(
10
)],
// maxLength: 10,
// maxLength: 10,
...
@@ -151,8 +144,6 @@ class _OnboardingScreenState extends BaseState<OnboardingScreen> with BasicState
...
@@ -151,8 +144,6 @@ class _OnboardingScreenState extends BaseState<OnboardingScreen> with BasicState
onChanged:
_viewModel
.
updatePhoneNumber
,
onChanged:
_viewModel
.
updatePhoneNumber
,
),
),
const
SizedBox
(
height:
16
),
const
SizedBox
(
height:
16
),
/// 📌 Nút Tiếp Tục
Obx
(
Obx
(
()
=>
SizedBox
(
()
=>
SizedBox
(
width:
double
.
infinity
,
width:
double
.
infinity
,
...
@@ -180,8 +171,6 @@ class _OnboardingScreenState extends BaseState<OnboardingScreen> with BasicState
...
@@ -180,8 +171,6 @@ 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
Row
(
Row
(
crossAxisAlignment:
CrossAxisAlignment
.
center
,
crossAxisAlignment:
CrossAxisAlignment
.
center
,
children:
[
children:
[
...
...
lib/screen/onboarding/onboarding_view
_
model.dart
→
lib/screen/onboarding/onboarding_viewmodel.dart
View file @
8ec716d3
File moved
lib/screen/otp/model/create_otp_response_model.dart
0 → 100644
View file @
8ec716d3
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
lib/screen/otp/model/create_otp_response_model.g.dart
0 → 100644
View file @
8ec716d3
// 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
};
lib/screen/otp/otp_claim_verify_response_model.dart
→
lib/screen/otp/
model/
otp_claim_verify_response_model.dart
View file @
8ec716d3
File moved
lib/screen/otp/otp_claim_verify_response_model.g.dart
→
lib/screen/otp/
model/
otp_claim_verify_response_model.g.dart
View file @
8ec716d3
File moved
lib/screen/otp/otp_verify_response_model.dart
→
lib/screen/otp/
model/
otp_verify_response_model.dart
View file @
8ec716d3
File moved
lib/screen/otp/otp_verify_response_model.g.dart
→
lib/screen/otp/
model/
otp_verify_response_model.g.dart
View file @
8ec716d3
...
@@ -23,7 +23,7 @@ Map<String, dynamic> _$OTPVerifyResponseModelToJson(
...
@@ -23,7 +23,7 @@ Map<String, dynamic> _$OTPVerifyResponseModelToJson(
OTPResendResponseModel
_$OTPResendResponseModelFromJson
(
OTPResendResponseModel
_$OTPResendResponseModelFromJson
(
Map
<
String
,
dynamic
>
json
,
Map
<
String
,
dynamic
>
json
,
)
=>
OTPResendResponseModel
(
otpTtl:
json
[
'otp_ttl'
]
as
int
?
);
)
=>
OTPResendResponseModel
(
otpTtl:
(
json
[
'otp_ttl'
]
as
num
?)?.
toInt
()
);
Map
<
String
,
dynamic
>
_$OTPResendResponseModelToJson
(
Map
<
String
,
dynamic
>
_$OTPResendResponseModelToJson
(
OTPResendResponseModel
instance
,
OTPResendResponseModel
instance
,
...
...
lib/screen/otp/otp_screen.dart
View file @
8ec716d3
...
@@ -4,7 +4,9 @@ import 'package:pin_code_fields/pin_code_fields.dart';
...
@@ -4,7 +4,9 @@ import 'package:pin_code_fields/pin_code_fields.dart';
import
'../../base/base_screen.dart'
;
import
'../../base/base_screen.dart'
;
import
'../../base/basic_state.dart'
;
import
'../../base/basic_state.dart'
;
import
'../../resouce/base_color.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
{
class
OtpScreen
extends
BaseScreen
{
final
IOtpRepository
repository
;
final
IOtpRepository
repository
;
...
@@ -15,6 +17,7 @@ class OtpScreen extends BaseScreen {
...
@@ -15,6 +17,7 @@ class OtpScreen extends BaseScreen {
}
}
class
_OtpScreenState
extends
BaseState
<
OtpScreen
>
with
BasicState
{
class
_OtpScreenState
extends
BaseState
<
OtpScreen
>
with
BasicState
{
final
TextEditingController
_pinController
=
TextEditingController
();
@override
@override
Widget
createBody
()
{
Widget
createBody
()
{
...
@@ -22,7 +25,10 @@ class _OtpScreenState extends BaseState<OtpScreen> with BasicState {
...
@@ -22,7 +25,10 @@ class _OtpScreenState extends BaseState<OtpScreen> with BasicState {
return
Scaffold
(
return
Scaffold
(
appBar:
AppBar
(
appBar:
AppBar
(
centerTitle:
true
,
centerTitle:
true
,
leading:
IconButton
(
icon:
const
Icon
(
Icons
.
arrow_back_ios
),
onPressed:
()
=>
Navigator
.
pop
(
context
)),
leading:
CustomBackButton
(),
actions:
[
SupportButton
(),
],
),
),
body:
SafeArea
(
body:
SafeArea
(
child:
GestureDetector
(
child:
GestureDetector
(
...
@@ -49,41 +55,38 @@ class _OtpScreenState extends BaseState<OtpScreen> with BasicState {
...
@@ -49,41 +55,38 @@ class _OtpScreenState extends BaseState<OtpScreen> with BasicState {
);
);
}
}
/// PinCodeTextField cho 6 ô
Widget
_buildPinCodeFields
(
OtpViewModel
vm
)
{
Widget
_buildPinCodeFields
(
OtpViewModel
vm
)
{
double
screenWidth
=
MediaQuery
.
of
(
context
).
size
.
width
;
double
screenWidth
=
MediaQuery
.
of
(
context
).
size
.
width
;
// return Obx(() {
return
PinCodeTextField
(
return
PinCodeTextField
(
controller:
_pinController
,
appContext:
Get
.
context
!,
appContext:
Get
.
context
!,
length:
6
,
length:
6
,
obscureText:
false
,
obscureText:
false
,
cursorColor:
Colors
.
black
,
cursorColor:
Colors
.
black
,
keyboardType:
TextInputType
.
number
,
keyboardType:
TextInputType
.
number
,
autoFocus:
true
,
autoFocus:
true
,
animationType:
AnimationType
.
none
,
animationType:
AnimationType
.
none
,
pinTheme:
PinTheme
(
pinTheme:
PinTheme
(
shape:
PinCodeFieldShape
.
box
,
shape:
PinCodeFieldShape
.
box
,
borderRadius:
BorderRadius
.
circular
(
6
),
borderRadius:
BorderRadius
.
circular
(
6
),
fieldHeight:
screenWidth
/
6
-
12
,
fieldHeight:
screenWidth
/
6
-
12
,
fieldWidth:
screenWidth
/
6
-
12
,
fieldWidth:
screenWidth
/
6
-
12
,
activeColor:
Colors
.
blue
,
activeColor:
Colors
.
blue
,
inactiveColor:
Colors
.
grey
.
shade300
,
inactiveColor:
Colors
.
grey
.
shade300
,
selectedColor:
Colors
.
blueAccent
,
selectedColor:
Colors
.
blueAccent
,
),
),
onChanged:
(
value
)
{
onChanged:
(
value
)
{
vm
.
otpCode
.
value
=
value
;
vm
.
otpCode
.
value
=
value
;
vm
.
errorMessage
.
value
=
"1111111"
;
// clear lỗi khi gõ
vm
.
errorMessage
.
value
=
""
;
},
},
onCompleted:
(
value
)
{
onCompleted:
(
value
)
{
vm
.
otpCode
.
value
=
value
;
vm
.
otpCode
.
value
=
value
;
vm
.
onSubmitOtp
;
vm
.
onSubmitOtp
();
},
},
);
);
// });
}
}
Widget
_buildErrorText
(
OtpViewModel
vm
)
{
Widget
_buildErrorText
(
OtpViewModel
vm
)
{
// Chỉ bọc Obx ở đây vì ta đọc vm.errorMessage
return
Obx
(()
{
return
Obx
(()
{
final
error
=
vm
.
errorMessage
.
value
;
final
error
=
vm
.
errorMessage
.
value
;
if
(
error
.
isEmpty
)
{
if
(
error
.
isEmpty
)
{
...
@@ -93,9 +96,7 @@ class _OtpScreenState extends BaseState<OtpScreen> with BasicState {
...
@@ -93,9 +96,7 @@ class _OtpScreenState extends BaseState<OtpScreen> with BasicState {
});
});
}
}
/// "Gửi lại OTP (02:30)"
Widget
_buildResendOtp
(
OtpViewModel
vm
)
{
Widget
_buildResendOtp
(
OtpViewModel
vm
)
{
// Bọc Obx vì ta đọc vm.currentCountdown
return
Obx
(()
{
return
Obx
(()
{
final
cd
=
vm
.
currentCountdown
.
value
;
final
cd
=
vm
.
currentCountdown
.
value
;
final
canResend
=
cd
==
0
;
final
canResend
=
cd
==
0
;
...
@@ -104,10 +105,15 @@ class _OtpScreenState extends BaseState<OtpScreen> with BasicState {
...
@@ -104,10 +105,15 @@ class _OtpScreenState extends BaseState<OtpScreen> with BasicState {
mainAxisAlignment:
MainAxisAlignment
.
center
,
mainAxisAlignment:
MainAxisAlignment
.
center
,
children:
[
children:
[
TextButton
(
TextButton
(
onPressed:
canResend
?
vm
.
onResendOtp
:
null
,
onPressed:
()
=>
{
canResend
?
vm
.
onResendOtp
()
:
null
,
vm
.
otpCode
.
value
=
""
,
vm
.
errorMessage
.
value
=
""
,
_pinController
.
clear
(),
},
child:
Text
(
child:
Text
(
"Gửi lại OTP
${!canResend ? "($textTime)" : ""}
"
,
"Gửi lại OTP
${!canResend ? "($textTime)" : ""}
"
,
style:
TextStyle
(
color:
canResend
?
Color
s
.
blue
:
Colors
.
grey
),
style:
TextStyle
(
color:
canResend
?
Base
Color
.
second700
:
BaseColor
.
second500
),
),
),
),
),
],
],
...
@@ -122,7 +128,7 @@ class _OtpScreenState extends BaseState<OtpScreen> with BasicState {
...
@@ -122,7 +128,7 @@ class _OtpScreenState extends BaseState<OtpScreen> with BasicState {
children:
[
children:
[
const
TextSpan
(
text:
"Mã OTP đã được gửi về số điện thoại "
),
const
TextSpan
(
text:
"Mã OTP đã được gửi về số điện thoại "
),
TextSpan
(
TextSpan
(
text:
"0999999999"
,
//"${vm
.phoneNumber
}"
,
text:
widget
.
repository
.
phoneNumber
,
style:
const
TextStyle
(
fontWeight:
FontWeight
.
w500
,
color:
BaseColor
.
primary500
),
style:
const
TextStyle
(
fontWeight:
FontWeight
.
w500
,
color:
BaseColor
.
primary500
),
),
),
],
],
...
...
Prev
1
2
Next
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment