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
55151ba2
Commit
55151ba2
authored
Sep 05, 2025
by
DatHV
Browse files
update history point, manager
parent
f714cdcc
Changes
130
Show whitespace changes
Inline
Side-by-side
lib/networking/dio_http_service.dart
0 → 100644
View file @
55151ba2
import
'package:dio/dio.dart'
;
import
'package:flutter/foundation.dart'
;
import
'interceptor/auth_interceptor.dart'
;
import
'interceptor/exception_interceptor.dart'
;
import
'interceptor/logger_interceptor.dart'
;
import
'interceptor/request_interceptor.dart'
;
const
int
connectTimeout
=
30
;
const
int
receiveTimeout
=
30
;
class
DioHttpService
{
DioHttpService
.
_internal
();
static
final
DioHttpService
_instance
=
DioHttpService
.
_internal
();
factory
DioHttpService
()
=>
_instance
;
String
_baseUrl
=
''
;
Dio
get
dio
=>
_dio
;
late
final
Dio
_dio
=
Dio
(
BaseOptions
(
baseUrl:
_baseUrl
,
connectTimeout:
const
Duration
(
seconds:
connectTimeout
),
receiveTimeout:
const
Duration
(
seconds:
receiveTimeout
),
contentType:
'application/json'
,
responseType:
ResponseType
.
json
,
validateStatus:
(
_
)
=>
true
,
receiveDataWhenStatusError:
true
,
),
)
..
interceptors
.
add
(
AuthInterceptor
())
..
interceptors
.
add
(
RequestInterceptor
())
..
interceptors
.
add
(
ExceptionInterceptor
())
..
interceptors
.
add
(
InterceptorsWrapper
(
onError:
(
e
,
h
)
{
if
(
e
.
response
!=
null
)
return
h
.
resolve
(
e
.
response
!);
h
.
next
(
e
);
},
))
..
interceptors
.
addAll
(
kReleaseMode
?
const
[]
:
[
LoggerInterceptor
()]);
void
setBaseUrl
(
String
newUrl
)
{
_baseUrl
=
newUrl
;
_dio
.
options
.
baseUrl
=
newUrl
;
}
void
setDefaultHeaders
(
Map
<
String
,
dynamic
>
headers
)
{
dio
.
options
.
headers
.
addAll
(
headers
);
}
}
\ No newline at end of file
lib/networking/auth_interceptor.dart
→
lib/networking/
interceptor/
auth_interceptor.dart
View file @
55151ba2
import
'package:dio/dio.dart'
;
import
'../configs/constants.dart'
;
import
'app_navigator.dart'
;
import
'../
../
configs/constants.dart'
;
import
'
../
app_navigator.dart'
;
import
'package:mypoint_flutter_app/preference/data_preference.dart'
;
class
AuthInterceptor
extends
Interceptor
{
bool
_
h
andling
=
false
;
// chặn bắn 2 lần nếu nhiều request fail cùng lúc
bool
_
isH
andling
Auth
=
false
;
@override
void
onResponse
(
Response
response
,
ResponseInterceptorHandler
handler
)
{
final
data
=
response
.
data
;
if
(
_isInvalidTokenPayload
(
data
))
{
_handleInvalidToken
(
data
);
// chặn parse tiếp: reject error để upstream biết đã fail
return
handler
.
reject
(
if
(
_isTokenInvalid
(
data
))
{
_handleAuthError
(
data
);
handler
.
reject
(
DioException
(
requestOptions:
response
.
requestOptions
,
response:
response
,
...
...
@@ -20,24 +19,22 @@ class AuthInterceptor extends Interceptor {
error:
'ERR_AUTH_TOKEN_INVALID'
,
),
);
return
;
}
super
.
onResponse
(
response
,
handler
);
handler
.
next
(
response
);
}
@override
void
onError
(
DioException
err
,
ErrorInterceptorHandler
handler
)
{
final
data
=
err
.
response
?.
data
;
final
status
=
err
.
response
?.
statusCode
;
if
(
status
==
401
||
_isInvalidTokenPayload
(
data
))
{
_handleInvalidToken
(
data
);
// vẫn reject để caller biết yêu cầu đã fail
return
handler
.
reject
(
err
);
final
statusCode
=
err
.
response
?.
statusCode
;
if
(
statusCode
==
401
||
_isTokenInvalid
(
data
))
{
_handleAuthError
(
data
);
}
super
.
onError
(
err
,
handle
r
);
handler
.
next
(
er
r
);
}
bool
_isInvali
dTokenPayloa
d
(
dynamic
data
)
{
bool
_is
Token
Invalid
(
dynamic
data
)
{
if
(
data
is
Map
<
String
,
dynamic
>)
{
final
code
=
data
[
'error_code'
]
??
data
[
'errorCode'
];
return
ErrorCodes
.
tokenInvalidCodes
.
contains
(
code
);
...
...
@@ -45,20 +42,20 @@ class AuthInterceptor extends Interceptor {
return
false
;
}
void
_handleInvalidToken
(
dynamic
data
)
async
{
if
(
_handling
)
return
;
_handling
=
true
;
// Xoá token / session
Future
<
void
>
_handleAuthError
(
dynamic
data
)
async
{
if
(
_isHandlingAuth
)
return
;
_isHandlingAuth
=
true
;
try
{
await
DataPreference
.
instance
.
clearData
();
final
message
=
(
data
is
Map
&&
data
[
'error_message'
]
is
String
)
?
data
[
'error_message'
]
as
String
:
''
;
// Hiện alert + điều hướng login
await
AppNavigator
.
showAuthAlertAndGoLogin
(
message
);
_handling
=
false
;
String
?
message
;
if
(
data
is
Map
<
String
,
dynamic
>)
{
message
=
data
[
'error_message'
]?.
toString
()
??
data
[
'errorMessage'
]?.
toString
()
??
data
[
'message'
]?.
toString
();
}
await
AppNavigator
.
showAuthAlertAndGoLogin
(
message
??
ErrorCodes
.
tokenInvalidMessage
);
}
finally
{
_isHandlingAuth
=
false
;
}
}
}
lib/networking/interceptor/exception_interceptor.dart
0 → 100644
View file @
55151ba2
import
'package:dio/dio.dart'
;
import
'../app_navigator.dart'
;
class
ExceptionInterceptor
extends
Interceptor
{
@override
void
onError
(
DioException
err
,
ErrorInterceptorHandler
handler
)
{
_handleError
(
err
);
handler
.
next
(
err
);
}
void
_handleError
(
DioException
error
)
{
String
message
;
switch
(
error
.
type
)
{
case
DioExceptionType
.
connectionTimeout
:
case
DioExceptionType
.
receiveTimeout
:
case
DioExceptionType
.
sendTimeout
:
message
=
'Kết nối mạng không ổn định.
\n
Vui lòng thử lại!'
;
break
;
case
DioExceptionType
.
connectionError
:
message
=
'Không thể kết nối đến máy chủ.
\n
Vui lòng kiểm tra kết nối mạng!'
;
break
;
case
DioExceptionType
.
badResponse
:
final
statusCode
=
error
.
response
?.
statusCode
;
if
(
statusCode
==
401
)
{
// Auth error - handled by AuthInterceptor
return
;
}
message
=
_extractErrorMessage
(
error
.
response
?.
data
)
??
'Đã xảy ra lỗi. Vui lòng thử lại!'
;
break
;
default
:
message
=
'Đã xảy ra lỗi không xác định. Vui lòng thử lại!'
;
}
// Show alert dialog with the error message
AppNavigator
.
showAlert
(
message
);
}
String
?
_extractErrorMessage
(
dynamic
data
)
{
if
(
data
is
Map
<
String
,
dynamic
>)
{
return
data
[
'message'
]?.
toString
()
??
data
[
'error_message'
]?.
toString
()
??
data
[
'errorMessage'
]?.
toString
();
}
return
null
;
}
}
//
// typedef RetryPrompt = Future<bool> Function(DioException e);
//
// class ExceptionInterceptor extends Interceptor {
// ExceptionInterceptor({
// required Dio dio,
// RetryPrompt? prompt,
// this.shouldRetryWhen = _defaultShouldRetry,
// }) : _dio = dio,
// _prompt = prompt ?? _defaultPrompt;
//
// final Dio _dio;
// final RetryPrompt _prompt;
//
// /// Quyết định lỗi nào là lỗi mạng đáng retry.
// final bool Function(DioException e) shouldRetryWhen;
//
// static bool _defaultShouldRetry(DioException e) {
// return e.type == DioExceptionType.connectionTimeout ||
// e.type == DioExceptionType.sendTimeout ||
// e.type == DioExceptionType.receiveTimeout ||
// e.type == DioExceptionType.unknown ||
// e.type == DioExceptionType.connectionError;
// }
//
// static Future<bool> _defaultPrompt(DioException e) async {
// // Dialog đơn giản với GetX. Bạn có thể thay Alert tuỳ UI của app.
// final result = await Get.dialog<bool>(
// AlertDialog(
// title: const Text('Mất kết nối'),
// content: const Text('Kết nối mạng không ổn định. Bạn có muốn thử lại không?'),
// actions: [
// TextButton(onPressed: () => Get.back(result: false), child: const Text('Đóng')),
// ElevatedButton(onPressed: () => Get.back(result: true), child: const Text('Thử lại')),
// ],
// ),
// barrierDismissible: false,
// );
// return result == true;
// }
//
// @override
// void onError(DioException err, ErrorInterceptorHandler handler) async {
// if (!shouldRetryWhen(err)) {
// return handler.next(err);
// }
// if (err.requestOptions.extra['__retried__'] == true) {
// return handler.next(err);
// }
// final wantRetry = await _prompt(err);
// if (!wantRetry) {
// return handler.next(err);
// }
// try {
// final resp = await _retryRequest(err.requestOptions);
// return handler.resolve(resp);
// } catch (_) {
// return handler.next(err);
// }
// }
//
// Future<Response<dynamic>> _retryRequest(RequestOptions ro) {
// // Đánh dấu đã retry để tránh vòng lặp
// ro.extra['__retried__'] = true;
// final options = Options(
// method: ro.method,
// headers: ro.headers,
// responseType: ro.responseType,
// contentType: ro.contentType,
// followRedirects: ro.followRedirects,
// receiveDataWhenStatusError: ro.receiveDataWhenStatusError,
// validateStatus: ro.validateStatus,
// sendTimeout: ro.sendTimeout,
// receiveTimeout: ro.receiveTimeout,
// extra: ro.extra,
// );
//
// return _dio.request<dynamic>(
// ro.path,
// data: ro.data, // Lưu ý: nếu dùng FormData với stream tự chế, có thể không retry được
// queryParameters: ro.queryParameters,
// options: options,
// cancelToken: ro.cancelToken,
// onSendProgress: ro.onSendProgress,
// onReceiveProgress: ro.onReceiveProgress,
// );
// }
// }
lib/networking/interceptor/logger_interceptor.dart
0 → 100644
View file @
55151ba2
import
'package:dio/dio.dart'
;
import
'package:logger/logger.dart'
;
class
LoggerInterceptor
extends
Interceptor
{
final
Logger
_logger
=
Logger
(
printer:
PrettyPrinter
(
methodCount:
0
,
lineLength:
120
,
printTime:
true
,
colors:
true
,
),
);
@override
void
onRequest
(
RequestOptions
options
,
RequestInterceptorHandler
handler
)
{
final
uri
=
options
.
uri
;
_logger
.
i
(
'🚀
${options.method}
$uri
\n
'
'Headers:
${_formatHeaders(options.headers)}
\n
'
'Query:
${options.queryParameters}
\n
'
'Body:
${_formatData(options.data)}
'
,
);
handler
.
next
(
options
);
}
@override
void
onResponse
(
Response
response
,
ResponseInterceptorHandler
handler
)
{
final
uri
=
response
.
requestOptions
.
uri
;
final
statusCode
=
response
.
statusCode
;
_logger
.
d
(
'✅
$statusCode
${response.requestOptions.method}
$uri
\n
'
'Response:
${_formatData(response.data)}
'
,
);
handler
.
next
(
response
);
}
@override
void
onError
(
DioException
err
,
ErrorInterceptorHandler
handler
)
{
final
uri
=
err
.
requestOptions
.
uri
;
final
statusCode
=
err
.
response
?.
statusCode
??
'Unknown'
;
_logger
.
e
(
'❌
$statusCode
${err.requestOptions.method}
$uri
\n
'
'Error:
${err.message}
\n
'
'Response:
${_formatData(err.response?.data)}
'
,
);
handler
.
next
(
err
);
}
String
_formatHeaders
(
Map
<
String
,
dynamic
>
headers
)
{
final
filtered
=
Map
<
String
,
dynamic
>.
from
(
headers
);
// Hide sensitive headers
if
(
filtered
.
containsKey
(
'Authorization'
))
{
filtered
[
'Authorization'
]
=
'***'
;
}
return
filtered
.
toString
();
}
String
_formatData
(
dynamic
data
)
{
if
(
data
==
null
)
return
'null'
;
if
(
data
is
String
&&
data
.
length
>
1000
)
{
return
'
${data.substring(0, 1000)}
... (truncated)'
;
}
return
data
.
toString
();
}
}
\ No newline at end of file
lib/
dio_http_service/modify_
request_interceptor.dart
→
lib/
networking/interceptor/
request_interceptor.dart
View file @
55151ba2
import
'package:dio/dio.dart'
;
import
'package:mypoint_flutter_app/preference/data_preference.dart'
;
class
ModifyRequestInterceptor
extends
Interceptor
{
class
RequestInterceptor
extends
Interceptor
{
@override
Future
<
void
>
onRequest
(
RequestOptions
options
,
RequestInterceptorHandler
handler
)
async
{
String
authKey
=
'Authorization'
;
String
?
token
=
await
DataPreference
.
instance
.
token
;
String
?
token
=
DataPreference
.
instance
.
token
;
if
(
token
!=
null
)
{
options
.
headers
[
authKey
]
=
"Bearer
$token
"
;
}
super
.
onRequest
(
options
,
handler
);
options
.
headers
.
addAll
({
'Accept'
:
'application/json'
,
'Content-Type'
:
'application/json'
,
'Accept-Language'
:
'vi'
,
});
handler
.
next
(
options
);
}
}
\ No newline at end of file
lib/networking/model_maker.dart
deleted
100644 → 0
View file @
f714cdcc
typedef
Json
=
Map
<
String
,
dynamic
>;
abstract
class
Encodable
{
Json
toJson
();
}
abstract
class
Fillable
extends
Encodable
{
void
fill
(
Json
data
);
}
extension
JsonEncode
on
Json
{
Json
toJson
()
{
return
this
;
}
}
class
ModelMaker
{
static
T
?
makeFrom
<
T
extends
Encodable
>(
dynamic
json
)
{
try
{
switch
(
T
)
{
// case WorkerSiteResponse: return WorkerSiteResponse.fromJson(json as Json) as T;
// case LoginResponse: return LoginResponse.fromJson(json as Json) as T;
default
:
return
null
;
}
}
catch
(
e
)
{
print
(
"API ERROR parser:
${e.toString()}
"
);
return
null
;
}
}
}
extension
L
on
List
{
}
\ No newline at end of file
lib/networking/request_manager.dart
deleted
100644 → 0
View file @
f714cdcc
import
'dart:io'
;
import
'package:dio/dio.dart'
;
import
'package:flutter/material.dart'
;
import
'../configs/api_paths.dart'
;
final
GlobalKey
<
NavigatorState
>
navigatorKey
=
GlobalKey
<
NavigatorState
>();
class
RequestManager
{
static
final
RequestManager
_instance
=
RequestManager
.
_internal
();
factory
RequestManager
()
=>
_instance
;
late
Dio
_dio
;
bool
_isErrorDialogShown
=
false
;
final
List
<
Future
Function
()>
_pendingRetries
=
[];
RequestManager
.
_internal
()
{
BaseOptions
options
=
BaseOptions
(
connectTimeout:
const
Duration
(
seconds:
10
),
receiveTimeout:
const
Duration
(
seconds:
10
),
responseType:
ResponseType
.
json
,
headers:
{
'Content-Type'
:
'application/json'
,
},
);
_dio
=
Dio
(
options
);
// interceptor handle errors
_dio
.
interceptors
.
add
(
InterceptorsWrapper
(
onRequest:
(
options
,
handler
)
{
// Log request
print
(
"REQUEST[
${options.method}
] =>
${options.baseUrl}${options.path}
"
);
return
handler
.
next
(
options
);
},
onResponse:
(
response
,
handler
)
{
// Log response
print
(
"RESPONSE[
${response.statusCode}
] =>
${response.data}
"
);
return
handler
.
next
(
response
);
},
onError:
(
DioException
error
,
handler
)
async
{
if
(
error
.
response
?.
statusCode
==
401
||
error
.
type
==
DioExceptionType
.
connectionTimeout
||
error
.
type
==
DioExceptionType
.
receiveTimeout
||
error
.
error
is
SocketException
)
{
_pendingRetries
.
add
(()
async
{
return
await
_retryRequest
(
error
.
requestOptions
);
});
if
(!
_isErrorDialogShown
)
{
_isErrorDialogShown
=
true
;
bool
retry
=
await
_showNetworkErrorAlert
();
if
(
retry
)
{
await
_retryPendingRequests
();
}
else
{
_pendingRetries
.
clear
();
}
_isErrorDialogShown
=
false
;
}
}
return
handler
.
next
(
error
);
},
));
}
// retry request từ RequestOptions
Future
<
Response
<
T
>>
_retryRequest
<
T
>(
RequestOptions
options
)
async
{
try
{
Response
<
T
>
response
=
await
_dio
.
request
<
T
>(
options
.
path
,
data:
options
.
data
,
queryParameters:
options
.
queryParameters
,
options:
Options
(
method:
options
.
method
,
headers:
options
.
headers
,
extra:
options
.
extra
,
),
);
return
response
;
}
catch
(
e
)
{
rethrow
;
}
}
// retry all request errors
Future
<
void
>
_retryPendingRequests
()
async
{
List
<
Future
Function
()>
retries
=
List
.
from
(
_pendingRetries
);
_pendingRetries
.
clear
();
await
Future
.
wait
(
retries
.
map
((
f
)
=>
f
()));
}
Future
<
bool
>
_showNetworkErrorAlert
()
async
{
BuildContext
?
context
=
navigatorKey
.
currentState
?.
overlay
?.
context
;
if
(
context
==
null
)
return
false
;
return
showDialog
<
bool
>(
context:
context
,
barrierDismissible:
false
,
builder:
(
context
)
=>
AlertDialog
(
title:
Text
(
"Lỗi mạng"
),
content:
Text
(
"Đã xảy ra lỗi mạng hoặc xác thực. Bạn có muốn thử lại các request lỗi không?"
),
actions:
[
TextButton
(
onPressed:
()
=>
Navigator
.
of
(
context
).
pop
(
false
),
child:
Text
(
"Hủy"
),
),
TextButton
(
onPressed:
()
=>
Navigator
.
of
(
context
).
pop
(
true
),
child:
Text
(
"Thử lại"
),
),
],
),
).
then
((
value
)
=>
value
??
false
);
}
/// [cancelToken]: Token hủy request.
/// [converter]: Hàm chuyển đổi từ Map<String, dynamic> sang model T (nếu T kế thừa BaseModel).
Future
<
T
>
request
<
T
>({
String
?
host
,
required
String
path
,
required
String
method
,
Map
<
String
,
dynamic
>?
queryParameters
,
dynamic
data
,
Map
<
String
,
dynamic
>?
headers
,
CancelToken
?
cancelToken
,
T
Function
(
Map
<
String
,
dynamic
>
json
)?
converter
,
})
async
{
_dio
.
options
.
baseUrl
=
host
??
APIPaths
.
baseUrl
;
if
(
headers
!=
null
)
{
_dio
.
options
.
headers
.
addAll
(
headers
);
}
try
{
Response
response
=
await
_dio
.
request
(
path
,
data:
data
,
queryParameters:
queryParameters
,
options:
Options
(
method:
method
),
cancelToken:
cancelToken
,
);
if
(
converter
!=
null
&&
response
.
data
is
Map
<
String
,
dynamic
>)
{
return
converter
(
response
.
data
as
Map
<
String
,
dynamic
>);
}
return
response
.
data
as
T
;
}
on
DioException
catch
(
e
)
{
throw
Exception
(
"Request error:
${e.message}
"
);
}
}
}
lib/networking/restful_api.dart
deleted
100644 → 0
View file @
f714cdcc
import
'package:dio/dio.dart'
;
import
'package:flutter/foundation.dart'
;
import
'package:mypoint_flutter_app/base/base_response_model.dart'
;
import
'package:mypoint_flutter_app/networking/auth_interceptor.dart'
;
import
'package:mypoint_flutter_app/preference/data_preference.dart'
;
import
'../configs/callbacks.dart'
;
import
'../configs/constants.dart'
;
import
'model_maker.dart'
;
enum
Method
{
GET
,
POST
,
PUT
,
DELETE
}
class
RestfulAPIClient
{
final
Dio
_dio
;
RestfulAPIClient
(
this
.
_dio
)
{
_dio
.
interceptors
.
add
(
AuthInterceptor
());
}
Json
header
=
{};
Future
<
BaseResponseModel
<
T
>>
fetchObject
<
T
>(
String
path
,
T
Function
(
dynamic
json
)
fromJsonT
,
)
async
{
final
response
=
await
_dio
.
get
(
path
);
return
BaseResponseModel
<
T
>.
fromJson
(
response
.
data
,
fromJsonT
);
}
Future
<
BaseResponseModel
<
List
<
T
>>>
fetchList
<
T
>(
String
path
,
T
Function
(
dynamic
json
)
fromJson
,
)
async
{
final
res
=
await
_dio
.
get
(
path
);
return
BaseResponseModel
<
List
<
T
>>.
fromJson
(
res
.
data
,
(
json
)
{
return
(
json
as
List
).
map
((
e
)
=>
fromJson
(
e
)).
toList
();
});
}
Future
<
BaseResponseModel
<
T
>>
requestNormal
<
T
>(
String
path
,
Method
method
,
Json
params
,
CallbackReturn
<
T
,
dynamic
>
parser
)
async
{
final
result
=
await
request
<
BaseResponseModel
<
T
>>(
path
,
method
,
params
,
(
data
)
{
return
BaseResponseModel
<
T
>.
fromJson
(
data
,
(
json
)
=>
parser
(
json
));
});
return
result
??
BaseResponseModel
<
T
>(
errorMessage:
Constants
.
commonError
);
}
Future
<
T
?>
request
<
T
>(
String
path
,
Method
method
,
Json
params
,
CallbackReturn
<
T
,
Json
>
parser
)
async
{
final
isGet
=
method
==
Method
.
GET
;
Json
query
=
isGet
?
params
:
{};
Json
body
=
!
isGet
?
params
:
{};
// body["lang"] = 'vi';
final
option
=
Options
(
method:
method
.
name
)
.
compose
(
_dio
.
options
,
path
,
queryParameters:
query
,
data:
body
,
);
String
?
token
=
await
DataPreference
.
instance
.
token
;
if
(
token
!=
null
)
{
option
.
headers
[
"Authorization"
]
=
"Bearer
$token
"
;
}
try
{
final
result
=
await
_dio
.
fetch
<
Map
<
String
,
dynamic
>>(
option
);
final
json
=
result
.
data
;
if
(
json
==
null
)
return
null
;
final
code
=
json
[
'error_code'
]
??
json
[
'errorCode'
];
if
(
ErrorCodes
.
tokenInvalidCodes
.
contains
(
code
))
{
return
null
;
}
return
parser
(
json
);
}
on
DioException
catch
(
e
)
{
_print
(
e
.
toString
());
final
data
=
e
.
response
?.
data
;
if
(
data
is
Json
)
{
try
{
return
parser
(
data
);
}
catch
(
e
)
{
_print
(
e
.
toString
());
return
null
;
}
}
else
{
return
null
;
}
}
catch
(
e
)
{
_print
(
e
.
toString
());
return
null
;
}
}
void
_print
(
String
error
)
{
if
(
kDebugMode
)
{
print
(
"===================
\n
API Error Parse false
${error}
\n
================"
);
}
}
}
\ No newline at end of file
lib/networking/restful_api_client.dart
0 → 100644
View file @
55151ba2
import
'dart:convert'
;
import
'package:dio/dio.dart'
;
import
'package:flutter/foundation.dart'
;
import
'package:mypoint_flutter_app/base/base_response_model.dart'
;
import
'package:mypoint_flutter_app/networking/interceptor/auth_interceptor.dart'
;
import
'../configs/callbacks.dart'
;
import
'../configs/constants.dart'
;
enum
Method
{
GET
,
POST
,
PUT
,
DELETE
}
class
RestfulAPIClient
{
final
Dio
_dio
;
Json
header
=
{};
RestfulAPIClient
(
this
.
_dio
);
Future
<
BaseResponseModel
<
T
>>
fetchObject
<
T
>(
String
path
,
T
Function
(
dynamic
json
)
fromJsonT
)
=>
requestNormal
<
T
>(
path
,
Method
.
GET
,
const
{},
fromJsonT
);
Future
<
BaseResponseModel
<
List
<
T
>>>
fetchList
<
T
>(
String
path
,
T
Function
(
dynamic
json
)
fromJsonT
)
=>
requestNormal
<
List
<
T
>>(
path
,
Method
.
GET
,
const
{},
(
jsonRoot
)
{
if
(
jsonRoot
is
List
)
{
return
jsonRoot
.
map
(
fromJsonT
).
toList
();
}
if
(
jsonRoot
is
Map
&&
jsonRoot
[
'data'
]
is
List
)
{
return
(
jsonRoot
[
'data'
]
as
List
).
map
(
fromJsonT
).
toList
();
}
return
<
T
>[];
});
Future
<
BaseResponseModel
<
T
>>
requestNormal
<
T
>(
String
path
,
Method
method
,
Json
params
,
T
Function
(
dynamic
json
)
parser
,
)
async
{
final
isGet
=
method
==
Method
.
GET
;
final
query
=
isGet
?
params
:
const
<
String
,
dynamic
>{};
final
body
=
isGet
?
const
<
String
,
dynamic
>{}
:
params
;
final
opt
=
_opts
(
method
).
compose
(
_dio
.
options
,
path
,
queryParameters:
query
,
data:
body
);
try
{
final
res
=
await
_dio
.
fetch
<
dynamic
>(
opt
);
final
status
=
res
.
statusCode
??
0
;
final
map
=
_asJson
(
res
.
data
);
try
{
final
model
=
BaseResponseModel
<
T
>.
fromJson
(
map
,
parser
);
return
model
;
}
catch
(
_
)
{
final
msg
=
_extractMessage
(
map
,
status
)
??
'HTTP
$status
'
;
if
(
_isOk
(
status
))
{
T
?
data
;
try
{
data
=
parser
(
map
);
}
catch
(
_
)
{}
return
BaseResponseModel
<
T
>(
status:
"success"
,
message:
map
[
'message'
]?.
toString
(),
data:
data
,
code:
status
);
}
else
{
return
BaseResponseModel
<
T
>(
status:
"fail"
,
message:
msg
,
data:
null
,
code:
status
);
}
}
}
on
DioException
catch
(
e
)
{
_debug
(
'DioException:
$e
'
);
final
status
=
e
.
response
?.
statusCode
;
final
map
=
_asJson
(
e
.
response
?.
data
);
final
msg
=
_extractMessage
(
map
,
status
)
??
e
.
message
??
Constants
.
commonError
;
return
BaseResponseModel
<
T
>(
status:
"fail"
,
message:
msg
,
data:
null
,
code:
status
);
}
catch
(
e
)
{
_debug
(
'Unknown exception:
$e
'
);
return
BaseResponseModel
<
T
>(
status:
"fail"
,
message:
Constants
.
commonError
,
data:
null
);
}
}
Options
_opts
(
Method
m
)
=>
Options
(
method:
m
.
name
,
validateStatus:
(
_
)
=>
true
,
receiveDataWhenStatusError:
true
);
bool
_isOk
(
int
?
code
)
=>
code
!=
null
&&
code
>=
200
&&
code
<
300
;
/// Ép mọi kiểu body về Map:
/// - Map: trả nguyên
/// - String: cố gắng jsonDecode → Map; nếu không, bọc thành {'message': '...'}
/// - List: bọc thành {'data': List}
/// - null/khác: {'message': '...'} hoặc message chung
Json
_asJson
(
dynamic
data
)
{
if
(
data
is
Json
)
return
data
;
if
(
data
is
String
)
{
try
{
final
decoded
=
jsonDecode
(
data
);
if
(
decoded
is
Json
)
return
decoded
;
if
(
decoded
is
List
)
return
<
String
,
dynamic
>{
'data'
:
decoded
};
return
<
String
,
dynamic
>{
'message'
:
data
};
}
catch
(
_
)
{
return
<
String
,
dynamic
>{
'message'
:
data
};
}
}
if
(
data
is
List
)
{
return
<
String
,
dynamic
>{
'data'
:
data
};
}
if
(
data
==
null
)
{
return
<
String
,
dynamic
>{
'message'
:
Constants
.
commonError
};
}
return
<
String
,
dynamic
>{
'message'
:
data
.
toString
()};
}
/// Rút message từ nhiều key phổ biến; fallback HTTP code
String
?
_extractMessage
(
Json
j
,
[
int
?
status
])
{
const
keys
=
[
'message'
,
'errorMessage'
,
'error_message'
,
'error'
,
'msg'
,
'detail'
,
'description'
];
for
(
final
k
in
keys
)
{
final
v
=
j
[
k
];
if
(
v
!=
null
)
{
final
s
=
v
.
toString
().
trim
();
if
(
s
.
isNotEmpty
)
return
s
;
}
}
return
status
==
null
?
null
:
'HTTP
$status
'
;
}
void
_debug
(
Object
e
)
{
if
(
kDebugMode
)
{
print
(
'=== API DEBUG ===
$e
'
);
}
}
}
\ No newline at end of file
lib/networking/restful_api_request.dart
→
lib/networking/restful_api_
client_all_
request.dart
View file @
55151ba2
...
...
@@ -3,15 +3,18 @@ import 'package:mypoint_flutter_app/configs/api_paths.dart';
import
'package:mypoint_flutter_app/base/base_response_model.dart'
;
import
'package:mypoint_flutter_app/configs/constants.dart'
;
import
'package:mypoint_flutter_app/extensions/string_extension.dart'
;
import
'package:mypoint_flutter_app/networking/restful_api.dart'
;
import
'package:mypoint_flutter_app/networking/restful_api
_client
.dart'
;
import
'package:mypoint_flutter_app/preference/data_preference.dart'
;
import
'package:mypoint_flutter_app/screen/game/models/game_bundle_response.dart'
;
import
'package:mypoint_flutter_app/screen/voucher/models/product_model.dart'
;
import
'../configs/callbacks.dart'
;
import
'../configs/device_info.dart'
;
import
'../model/auth/biometric_register_response_model.dart'
;
import
'../model/auth/login_token_response_model.dart'
;
import
'../model/auth/profile_response_model.dart'
;
import
'../model/update_response_model.dart'
;
import
'../screen/history_point/models/history_point_models.dart'
;
import
'../screen/history_point/models/transaction_summary_by_date_model.dart'
;
import
'../screen/splash/models/update_response_model.dart'
;
import
'../preference/point/header_home_model.dart'
;
import
'../screen/affiliate/model/affiliate_brand_model.dart'
;
import
'../screen/affiliate/model/affiliate_category_model.dart'
;
...
...
@@ -73,9 +76,8 @@ import '../screen/voucher/models/product_brand_model.dart';
import
'../screen/voucher/models/product_store_model.dart'
;
import
'../screen/voucher/models/product_type.dart'
;
import
'../screen/voucher/models/search_product_response_model.dart'
;
import
'model_maker.dart'
;
extension
Restful
l
APIClientAll
Api
on
RestfulAPIClient
{
extension
RestfulAPIClientAll
Request
on
RestfulAPIClient
{
Future
<
BaseResponseModel
<
UpdateResponseModel
>>
checkUpdateApp
()
async
{
String
version
=
Platform
.
version
;
final
body
=
{
"operating_system"
:
"iOS"
,
"software_model"
:
"MyPoint"
,
"version"
:
version
,
"build_number"
:
"1"
};
...
...
@@ -624,7 +626,7 @@ extension RestfullAPIClientAllApi on RestfulAPIClient {
Future
<
BaseResponseModel
<
List
<
ProductBrandModel
>>>
productTopUpBrands
()
async
{
final
body
=
{
"topup_type"
:
"PRODUCT_MODEL_MOBILE_SERVICE"
,
"page_size"
:
"999"
,
"page_index"
:
0
};
return
requestNormal
(
APIPaths
.
ge
tTopUpBrands
,
Method
.
GET
,
body
,
(
data
)
{
return
requestNormal
(
APIPaths
.
produc
tTopUp
s
Brands
,
Method
.
GET
,
body
,
(
data
)
{
final
list
=
data
as
List
<
dynamic
>;
return
list
.
map
((
e
)
=>
ProductBrandModel
.
fromJson
(
e
)).
toList
();
});
...
...
@@ -675,9 +677,9 @@ extension RestfullAPIClientAllApi on RestfulAPIClient {
});
}
Future
<
BaseResponseModel
<
EmptyCodable
>>
redeemProductTopUps
(
Str
in
g
productId
,
String
phoneNumber
)
async
{
Future
<
BaseResponseModel
<
EmptyCodable
>>
redeemProductTopUps
(
in
t
productId
,
String
phoneNumber
)
async
{
String
?
token
=
DataPreference
.
instance
.
token
??
""
;
final
body
=
{
"access_token"
:
token
,
"product_id"
:
productId
,
"quantity"
:
1
,
"phone_number"
:
phoneNumber
};
final
body
=
{
"access_token"
:
token
,
"product_id"
:
productId
,
"quantity"
:
1
,
"phone_number"
:
phoneNumber
,
"lang"
:
"vi"
};
return
requestNormal
(
APIPaths
.
redeemProductTopUps
,
Method
.
POST
,
body
,
(
data
)
{
return
EmptyCodable
.
fromJson
(
data
as
Json
);
});
...
...
@@ -926,11 +928,36 @@ extension RestfullAPIClientAllApi on RestfulAPIClient {
}
Future
<
BaseResponseModel
<
List
<
BankAccountInfoModel
>>>
getOrderPaymentMyAccounts
()
async
{
// String? token = DataPreference.instance.token ?? "";
// final body = {"access_token": token, "lang": "vi"};
return
requestNormal
(
APIPaths
.
orderPaymentMyAccounts
,
Method
.
GET
,
{},
(
data
)
{
final
list
=
data
as
List
<
dynamic
>;
return
list
.
map
((
e
)
=>
BankAccountInfoModel
.
fromJson
(
e
)).
toList
();
});
}
Future
<
BaseResponseModel
<
String
>>
setDefaultBankAccount
(
String
id
,
bool
isDefault
)
async
{
final
path
=
APIPaths
.
bankAccountSetDefault
.
replaceFirst
(
'%@'
,
id
);
final
body
=
{
"is_default"
:
isDefault
?
1
:
0
};
return
requestNormal
(
path
,
Method
.
POST
,
body
,
(
data
)
=>
data
as
String
);
}
Future
<
BaseResponseModel
<
String
>>
deleteBankAccount
(
String
id
)
async
{
final
path
=
APIPaths
.
bankAccountDelete
.
replaceFirst
(
'%@'
,
id
);
return
requestNormal
(
path
,
Method
.
DELETE
,
{},
(
data
)
=>
data
as
String
);
}
Future
<
BaseResponseModel
<
TransactionSummaryByDateModel
>>
transactionGetSummaryByDate
(
Json
body
)
async
{
String
?
token
=
DataPreference
.
instance
.
token
??
""
;
body
[
"access_token"
]
=
token
;
return
requestNormal
(
APIPaths
.
transactionGetSummaryByDate
,
Method
.
POST
,
body
,
(
data
)
{
return
TransactionSummaryByDateModel
.
fromJson
(
data
as
Json
);
});
}
Future
<
BaseResponseModel
<
ListHistoryResponseModel
>>
transactionHistoryGetList
(
Json
body
)
async
{
String
?
token
=
DataPreference
.
instance
.
token
??
""
;
body
[
"access_token"
]
=
token
;
return
requestNormal
(
APIPaths
.
transactionHistoryGetList
,
Method
.
POST
,
body
,
(
data
)
{
return
ListHistoryResponseModel
.
fromJson
(
data
as
Json
);
});
}
}
\ No newline at end of file
lib/
base
/restful_api_viewmodel.dart
→
lib/
networking
/restful_api_viewmodel.dart
View file @
55151ba2
import
'package:mypoint_flutter_app/networking/restful_api.dart'
;
import
'../
dio_http_service/dio_http_
se
r
vi
c
e.dart'
;
import
'
ba
se
_
vie
w_model
.dart'
;
import
'package:mypoint_flutter_app/networking/restful_api
_client
.dart'
;
import
'../
base/ba
se
_
vie
w_model
.dart'
;
import
'
dio_http_
se
r
vi
c
e.dart'
;
class
RestfulApiViewModel
extends
BaseViewModel
{
final
RestfulAPIClient
_apiService
=
RestfulAPIClient
(
DioHttpService
().
getDio
()
);
final
RestfulAPIClient
_apiService
=
RestfulAPIClient
(
DioHttpService
().
dio
);
RestfulAPIClient
get
client
=>
_apiService
;
@override
void
onInit
()
{
super
.
onInit
();
}
}
\ No newline at end of file
lib/preference/data_preference.dart
View file @
55151ba2
import
'dart:convert'
;
import
'package:flutter/foundation.dart'
;
import
'package:shared_preferences/shared_preferences.dart'
;
import
'../model/auth/login_token_response_model.dart'
;
import
'../model/auth/profile_response_model.dart'
;
...
...
@@ -14,16 +15,37 @@ class DataPreference {
String
phoneNumberUsedForLoginScreen
=
""
;
Future
<
void
>
init
()
async
{
try
{
final
prefs
=
await
SharedPreferences
.
getInstance
();
final
tokenJson
=
prefs
.
getString
(
'login_token'
);
if
(
tokenJson
!=
null
)
{
if
(
tokenJson
!=
null
&&
tokenJson
.
isNotEmpty
)
{
try
{
_loginToken
=
LoginTokenResponseModel
.
fromJson
(
jsonDecode
(
tokenJson
));
}
catch
(
e
)
{
if
(
kDebugMode
)
{
print
(
'Failed to parse login token:
$e
'
);
}
// Clear invalid token
await
prefs
.
remove
(
'login_token'
);
}
}
final
profileJson
=
prefs
.
getString
(
'user_profile'
);
if
(
profileJson
!=
null
)
{
if
(
profileJson
!=
null
&&
profileJson
.
isNotEmpty
)
{
try
{
_profile
=
ProfileResponseModel
.
fromJson
(
jsonDecode
(
profileJson
));
phoneNumberUsedForLoginScreen
=
_profile
?.
workerSite
?.
phoneNumber
??
""
;
}
catch
(
e
)
{
if
(
kDebugMode
)
{
print
(
'Failed to parse user profile:
$e
'
);
}
// Clear invalid profile
await
prefs
.
remove
(
'user_profile'
);
}
}
}
catch
(
e
)
{
if
(
kDebugMode
)
{
print
(
'DataPreference init failed:
$e
'
);
}
}
}
String
get
displayName
{
...
...
@@ -77,9 +99,28 @@ class DataPreference {
}
Future
<
String
?>
getBioToken
(
String
phone
)
async
{
try
{
final
prefs
=
await
SharedPreferences
.
getInstance
();
final
jsonString
=
prefs
.
getString
(
'biometric_login_token_
$phone
'
);
return
jsonString
!=
null
?
jsonDecode
(
jsonString
)
:
null
;
if
(
jsonString
!=
null
&&
jsonString
.
isNotEmpty
)
{
try
{
return
jsonDecode
(
jsonString
)
as
String
?;
}
catch
(
e
)
{
if
(
kDebugMode
)
{
print
(
'Failed to parse bio token for
$phone
:
$e
'
);
}
// Clear invalid bio token
await
prefs
.
remove
(
'biometric_login_token_
$phone
'
);
return
null
;
}
}
return
null
;
}
catch
(
e
)
{
if
(
kDebugMode
)
{
print
(
'getBioToken failed for
$phone
:
$e
'
);
}
return
null
;
}
}
Future
<
void
>
clearBioToken
(
String
phone
)
async
{
...
...
lib/preference/point/point_manager.dart
View file @
55151ba2
import
'package:get/get.dart'
;
import
'package:mypoint_flutter_app/networking/restful_api_request.dart'
;
import
'../../
base
/restful_api_viewmodel.dart'
;
import
'package:mypoint_flutter_app/networking/restful_api_
client_all_
request.dart'
;
import
'../../
networking
/restful_api_viewmodel.dart'
;
import
'../data_preference.dart'
;
import
'header_home_model.dart'
;
...
...
lib/screen/achievement/achievement_list_screen.dart
View file @
55151ba2
import
'package:flutter/material.dart'
;
import
'package:get/get.dart'
;
import
'package:mypoint_flutter_app/widgets/custom_empty_widget.dart'
;
import
'package:mypoint_flutter_app/widgets/image_loader.dart'
;
import
'../../widgets/custom_navigation_bar.dart'
;
import
'../home/models/achievement_model.dart'
;
...
...
@@ -46,6 +47,9 @@ class _AchievementListScreenState extends State<AchievementListScreen> {
Widget
_buildAchievementContent
()
{
final
items
=
_viewModel
.
achievements
;
if
(
items
.
isEmpty
)
{
return
EmptyWidget
();
}
return
Padding
(
padding:
const
EdgeInsets
.
all
(
16.0
),
child:
GridView
.
builder
(
...
...
@@ -77,7 +81,9 @@ class _AchievementListScreenState extends State<AchievementListScreen> {
Widget
_buildPointHuntingContent
()
{
final
items
=
_viewModel
.
achievements
;
return
RefreshIndicator
(
return
items
.
isEmpty
?
EmptyWidget
()
:
RefreshIndicator
(
onRefresh:
()
=>
_viewModel
.
fetchAchievements
(),
child:
CustomScrollView
(
physics:
const
AlwaysScrollableScrollPhysics
(),
...
...
lib/screen/achievement/achievement_viewmodel.dart
View file @
55151ba2
import
'package:get/get.dart'
;
import
'package:mypoint_flutter_app/networking/restful_api_request.dart'
;
import
'../../
base
/restful_api_viewmodel.dart'
;
import
'package:mypoint_flutter_app/networking/restful_api_
client_all_
request.dart'
;
import
'../../
networking
/restful_api_viewmodel.dart'
;
import
'../../preference/data_preference.dart'
;
import
'../home/models/achievement_model.dart'
;
...
...
lib/screen/affiliate/affiliate_tab_viewmodel.dart
View file @
55151ba2
import
'package:get/get_rx/src/rx_types/rx_types.dart'
;
import
'package:mypoint_flutter_app/networking/restful_api_request.dart'
;
import
'../../
base
/restful_api_viewmodel.dart'
;
import
'package:mypoint_flutter_app/networking/restful_api_
client_all_
request.dart'
;
import
'../../
networking
/restful_api_viewmodel.dart'
;
import
'affiliate_popup_brands.dart'
;
import
'model/affiliate_brand_model.dart'
;
import
'model/affiliate_category_model.dart'
;
...
...
lib/screen/affiliate_brand_detail/affiliate_brand_detail_viewmodel.dart
View file @
55151ba2
import
'package:get/get.dart'
;
import
'package:mypoint_flutter_app/configs/constants.dart'
;
import
'package:mypoint_flutter_app/networking/restful_api_request.dart'
;
import
'../../
base
/restful_api_viewmodel.dart'
;
import
'package:mypoint_flutter_app/networking/restful_api_
client_all_
request.dart'
;
import
'../../
networking
/restful_api_viewmodel.dart'
;
import
'models/affiliate_brand_detail_model.dart'
;
class
AffiliateBrandDetailViewModel
extends
RestfulApiViewModel
{
...
...
lib/screen/affiliate_brand_detail/affiliate_category_grid_viewmodel.dart
View file @
55151ba2
import
'package:get/get_rx/src/rx_types/rx_types.dart'
;
import
'package:mypoint_flutter_app/networking/restful_api_request.dart'
;
import
'../../
base
/restful_api_viewmodel.dart'
;
import
'package:mypoint_flutter_app/networking/restful_api_
client_all_
request.dart'
;
import
'../../
networking
/restful_api_viewmodel.dart'
;
import
'../affiliate/model/affiliate_brand_model.dart'
;
import
'../affiliate/model/affiliate_category_model.dart'
;
...
...
lib/screen/bank_account_manager/bank_account_detail_screen.dart
View file @
55151ba2
import
'package:flutter/material.dart'
;
import
'package:get/get.dart'
;
import
'../../base/base_screen.dart'
;
import
'../../base/basic_state.dart'
;
import
'../../resources/base_color.dart'
;
import
'../../widgets/alert/data_alert_model.dart'
;
import
'../../widgets/custom_navigation_bar.dart'
;
import
'../../widgets/custom_toast_message.dart'
;
import
'bank_account_detail_viewmodel.dart'
;
import
'bank_account_info_model.dart'
;
class
BankAccountDetailScreen
extends
StatefulWidget
{
class
BankAccountDetailScreen
extends
BaseScreen
{
final
BankAccountInfoModel
model
;
const
BankAccountDetailScreen
({
super
.
key
,
required
this
.
model
});
...
...
@@ -10,30 +17,33 @@ class BankAccountDetailScreen extends StatefulWidget {
State
<
BankAccountDetailScreen
>
createState
()
=>
_BankAccountDetailScreenState
();
}
class
_BankAccountDetailScreenState
extends
State
<
BankAccountDetailScreen
>
{
late
bool
_isDefault
;
class
_BankAccountDetailScreenState
extends
Base
State
<
BankAccountDetailScreen
>
with
BasicState
{
late
final
BankAccountDetailViewModel
_viewModel
;
@override
void
initState
()
{
super
.
initState
();
_isDefault
=
widget
.
model
.
isDefault
??
false
;
_viewModel
=
Get
.
put
(
BankAccountDetailViewModel
(
model:
widget
.
model
));
_viewModel
.
isDefault
.
value
=
widget
.
model
.
isDefault
??
false
;
_viewModel
.
deleteBackAccountSuccess
=
(
message
)
{
showToastMessage
(
message
);
Get
.
back
(
result:
true
);
};
_viewModel
.
onShowAlertError
=
(
message
)
{
if
(
message
.
isNotEmpty
)
{
showAlertError
(
content:
message
);
}
};
}
@override
Widget
build
(
BuildContext
context
)
{
final
title
=
widget
.
model
.
cardName
?.
isNotEmpty
==
true
?
widget
.
model
.
cardName
!.
toUpperCase
()
:
(
widget
.
model
.
paymentMethod
??
'THẺ/TÀI KHOẢN'
);
Widget
createBody
()
{
return
Scaffold
(
appBar:
CustomNavigationBar
(
title:
'Thông
ti
n
thẻ'
),
appBar:
CustomNavigationBar
(
title:
widget
.
model
.
formBankTitle
??
'Chi
ti
ết
thẻ
/tài khoản
'
),
body:
Column
(
children:
[
const
SizedBox
(
height:
8
),
_CardPreview
(
title:
title
,
maskedNumber:
widget
.
model
.
cardNumber
??
''
,
),
_CardPreview
(
model:
widget
.
model
),
const
SizedBox
(
height:
12
),
_defaultRow
(
context
),
const
Spacer
(),
...
...
@@ -47,13 +57,13 @@ class _BankAccountDetailScreenState extends State<BankAccountDetailScreen> {
}
Widget
_defaultRow
(
BuildContext
context
)
{
return
Container
(
margin:
const
EdgeInsets
.
symmetric
(
horizontal:
16
),
return
Obx
(()
=>
Container
(
margin:
const
EdgeInsets
.
symmetric
(
horizontal:
0
),
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
16
),
height:
56
,
decoration:
BoxDecoration
(
color:
Colors
.
white
,
borderRadius:
BorderRadius
.
circular
(
12
),
),
child:
Row
(
children:
[
...
...
@@ -64,18 +74,18 @@ class _BankAccountDetailScreenState extends State<BankAccountDetailScreen> {
),
),
Switch
(
value:
_isDefault
,
activeColor:
Colors
.
white
,
activeTrackColor:
const
Color
(
0xFF34C759
),
onChanged:
(
val
)
async
{
setState
(()
=>
_isDefault
=
val
);
// if (widget.onMakeDefault != null) {
// await widget.onMakeDefault!(widget.model);
// }
value:
_viewModel
.
isDefault
.
value
,
onChanged:
(
value
)
async
{
_viewModel
.
changeDefaultBankAccount
();
},
),
activeColor:
Colors
.
white
,
activeTrackColor:
Colors
.
green
,
inactiveThumbColor:
Colors
.
white
,
inactiveTrackColor:
Colors
.
grey
.
shade400
,
)
],
),
),
);
}
...
...
@@ -83,28 +93,9 @@ class _BankAccountDetailScreenState extends State<BankAccountDetailScreen> {
return
Padding
(
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
16
),
child:
ElevatedButton
(
onPressed:
()
async
{
final
ok
=
await
showDialog
<
bool
>(
context:
context
,
builder:
(
_
)
=>
AlertDialog
(
title:
const
Text
(
'Xoá thẻ?'
),
content:
Text
(
widget
.
model
.
formMessageDelete
??
'Bạn có chắc muốn xoá thẻ/tài khoản này?'
,
),
actions:
[
TextButton
(
onPressed:
()
=>
Navigator
.
pop
(
context
,
false
),
child:
const
Text
(
'Huỷ'
)),
TextButton
(
onPressed:
()
=>
Navigator
.
pop
(
context
,
true
),
child:
const
Text
(
'Xoá'
)),
],
),
);
// if (ok == true && widget.onDelete != null) {
// await widget.onDelete!(widget.model);
// if (context.mounted) Navigator.pop(context);
// }
},
onPressed:
_showAlertConfirmLogout
,
style:
ElevatedButton
.
styleFrom
(
backgroundColor:
const
Color
(
0xFFF2F2F2
)
,
backgroundColor:
Colors
.
grey
.
shade400
,
elevation:
0
,
minimumSize:
const
Size
.
fromHeight
(
52
),
shape:
RoundedRectangleBorder
(
borderRadius:
BorderRadius
.
circular
(
16
)),
...
...
@@ -116,21 +107,39 @@ class _BankAccountDetailScreenState extends State<BankAccountDetailScreen> {
),
);
}
_showAlertConfirmLogout
()
{
final
dataAlert
=
DataAlertModel
(
title:
"Xoá thẻ?"
,
description:
"Bạn có chắc muốn xoá thẻ/tài khoản này?"
,
localHeaderImage:
"assets/images/ic_pipi_03.png"
,
buttons:
[
AlertButton
(
text:
"Đồng ý"
,
onPressed:
()
{
Get
.
back
();
_viewModel
.
deleteBankAccount
();
},
bgColor:
BaseColor
.
primary500
,
textColor:
Colors
.
white
,
),
AlertButton
(
text:
"Huỷ"
,
onPressed:
()
=>
Get
.
back
(),
bgColor:
Colors
.
white
,
textColor:
BaseColor
.
second500
),
],
);
showAlert
(
data:
dataAlert
);
}
}
class
_CardPreview
extends
StatelessWidget
{
final
String
title
;
final
String
maskedNumber
;
const
_CardPreview
({
required
this
.
title
,
required
this
.
maskedNumber
,
});
final
BankAccountInfoModel
model
;
const
_CardPreview
({
required
this
.
model
});
@override
Widget
build
(
BuildContext
context
)
{
final
widthItem
=
MediaQuery
.
of
(
context
).
size
.
width
-
32
;
return
Container
(
height:
180
,
height:
widthItem
*
9
/
16
,
width:
widthItem
,
decoration:
BoxDecoration
(
borderRadius:
BorderRadius
.
circular
(
16
),
gradient:
const
LinearGradient
(
...
...
@@ -145,25 +154,44 @@ class _CardPreview extends StatelessWidget {
boxShadow:
const
[
BoxShadow
(
color:
Colors
.
black26
,
blurRadius:
12
,
offset:
Offset
(
0
,
6
)),
],
image:
DecorationImage
(
image:
bgImageProvider
(
url:
model
.
formBankBackground
),
fit:
BoxFit
.
cover
,
colorFilter:
ColorFilter
.
mode
(
Colors
.
black
.
withOpacity
(
0.15
),
BlendMode
.
darken
),
),
margin:
const
EdgeInsets
.
all
(
16
),
),
margin:
const
EdgeInsets
.
symmetric
(
horizontal:
16
,
vertical:
8
),
padding:
const
EdgeInsets
.
all
(
16
),
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
const
Spacer
(),
Text
(
title
,
// VISA / MASTER / BANK NAME
style:
const
TextStyle
(
color:
Colors
.
white
,
fontSize:
2
0
,
fontWeight:
FontWeight
.
w700
,
letterSpacing:
1.2
),
model
.
formBankName
??
'THẺ/TÀI KHOẢN'
,
// VISA / MASTER / BANK NAME
style:
const
TextStyle
(
color:
Colors
.
white
,
fontSize:
2
8
,
fontWeight:
FontWeight
.
w700
,
letterSpacing:
1.2
),
),
const
Spacer
(),
const
Text
(
'Số thẻ'
,
style:
TextStyle
(
color:
Colors
.
white70
,
fontSize:
13
)),
Text
(
model
.
formBankNumberTitle
??
'Số thẻ/Tài khoản'
,
style:
TextStyle
(
color:
Colors
.
white70
,
fontSize:
16
),
),
const
SizedBox
(
height:
6
),
Text
(
m
askedNumber
,
m
odel
.
formBankNumber
??
''
,
style:
const
TextStyle
(
color:
Colors
.
white
,
fontSize:
22
,
fontWeight:
FontWeight
.
w700
,
letterSpacing:
2
),
),
],
),
);
}
ImageProvider
<
Object
>
bgImageProvider
({
required
String
?
url
,
String
placeholderAsset
=
'assets/images/bg_card_bank_account.png'
,
})
{
if
(
url
==
null
||
url
.
isEmpty
)
{
return
AssetImage
(
placeholderAsset
);
}
return
NetworkImage
(
url
);
}
}
\ No newline at end of file
lib/screen/bank_account_manager/bank_account_detail_viewmodel.dart
0 → 100644
View file @
55151ba2
import
'package:get/get.dart'
;
import
'package:mypoint_flutter_app/configs/constants.dart'
;
import
'package:mypoint_flutter_app/networking/restful_api_client_all_request.dart'
;
import
'package:mypoint_flutter_app/widgets/custom_toast_message.dart'
;
import
'../../networking/restful_api_viewmodel.dart'
;
import
'bank_account_info_model.dart'
;
class
BankAccountDetailViewModel
extends
RestfulApiViewModel
{
final
BankAccountInfoModel
model
;
void
Function
(
String
message
)?
onShowAlertError
;
void
Function
(
String
message
)?
deleteBackAccountSuccess
;
var
isDefault
=
false
.
obs
;
BankAccountDetailViewModel
({
required
this
.
model
});
changeDefaultBankAccount
()
async
{
final
revertDefault
=
!
isDefault
.
value
;
final
accountId
=
model
.
id
.
toString
();
showLoading
();
try
{
final
response
=
await
client
.
setDefaultBankAccount
(
accountId
,
revertDefault
);
hideLoading
();
if
(
response
.
isSuccess
)
{
isDefault
.
value
=
revertDefault
;
showToastMessage
(
response
.
data
??
response
.
message
??
"Cập nhật thành công"
);
}
else
{
onShowAlertError
?.
call
(
response
.
message
??
Constants
.
commonError
);
}
}
catch
(
error
)
{
hideLoading
();
onShowAlertError
?.
call
(
Constants
.
commonError
);
}
}
deleteBankAccount
()
async
{
final
accountId
=
model
.
id
.
toString
();
showLoading
();
try
{
final
response
=
await
client
.
deleteBankAccount
(
accountId
);
hideLoading
();
if
(
response
.
isSuccess
)
{
deleteBackAccountSuccess
?.
call
(
response
.
data
??
response
.
message
??
"Xoá tài khoản thành công"
);
}
else
{
onShowAlertError
?.
call
(
response
.
message
??
Constants
.
commonError
);
}
}
catch
(
error
)
{
hideLoading
();
onShowAlertError
?.
call
(
Constants
.
commonError
);
}
finally
{
hideLoading
();
}
}
}
\ No newline at end of file
Prev
1
2
3
4
5
6
7
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