Commit e41fc4fe authored by DatHV's avatar DatHV
Browse files

init

parent d87cb75e
import 'package:dio/dio.dart';
import 'package:mypoint_flutter_app/preference/data_preference.dart';
class ModifyRequestInterceptor extends Interceptor {
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
String authKey = 'Authorization';
String? token = DataPreference.instance.token;
if (token!= null) {
options.headers[authKey] = "Bearer $token";
}
super.onRequest(options, handler);
}
}
\ No newline at end of file
import 'package:flutter/cupertino.dart';
extension BuildContextAndAlert on BuildContext {
showAlertDialog(String message) {
showCupertinoDialog(
context: this,
builder: (context) {
return CupertinoAlertDialog(
title: Text('Error'),
content: Text(message),
actions: <Widget>[
CupertinoDialogAction(
isDefaultAction: true,
child: Text('OK'),
onPressed: () {
Navigator.of(context).pop();
},
)
],
);
},
);
}
}
\ No newline at end of file
extension PhoneValidator on String {
bool isPhoneValid() {
return RegExp(r'^0\d{9}$').hasMatch(this);
}
}
\ No newline at end of file
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:mypoint_flutter_app/splash_screen/splash_screen.dart';
import 'onboading/onboarding_screen.dart';
import 'onboading/onboarding_viewmodel.dart';
void main() {
Get.put(OnboardingViewModel());
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return GetMaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSwatch(primarySwatch: Colors.deepPurple),
primaryColor: Colors.deepPurple,
),
home: OnboardingScreen(), //SplashScreen(),
);
}
}
import 'package:json_annotation/json_annotation.dart';
part 'check_update_response_model.g.dart';
enum UpdateStatus { force, suggest, none }
@JsonSerializable()
class CheckUpdateResponseModel {
@JsonKey(name: 'update_mode')
String? updateMode;
@JsonKey(name: 'update_title')
String? updateTitle;
@JsonKey(name: 'update_message')
String? updateMessage;
@JsonKey(name: 'update_link')
String? updateLink;
UpdateStatus get status {
switch (updateMode?.toUpperCase() ?? "") {
case 'NOW':
return UpdateStatus.force;
default:
return UpdateStatus.suggest;
}
}
CheckUpdateResponseModel({
this.updateMode,
this.updateTitle,
this.updateMessage,
this.updateLink,
});
factory CheckUpdateResponseModel.fromJson(Map<String, dynamic> json) => _$CheckUpdateResponseModelFromJson(json);
Map<String, dynamic> toJson() => _$CheckUpdateResponseModelToJson(this);
}
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'check_update_response_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
CheckUpdateResponseModel _$CheckUpdateResponseModelFromJson(
Map<String, dynamic> json,
) => CheckUpdateResponseModel(
updateMode: json['update_mode'] as String?,
updateTitle: json['update_title'] as String?,
updateMessage: json['update_message'] as String?,
updateLink: json['update_link'] as String?,
);
Map<String, dynamic> _$CheckUpdateResponseModelToJson(
CheckUpdateResponseModel instance,
) => <String, dynamic>{
'update_mode': instance.updateMode,
'update_title': instance.updateTitle,
'update_message': instance.updateMessage,
'update_link': instance.updateLink,
};
import 'package:json_annotation/json_annotation.dart';
import 'package:mypoint_flutter_app/model/check_update_response_model.dart';
part 'update_response_model.g.dart';
@JsonSerializable()
class UpdateResponseModel {
@JsonKey(name: 'update_request')
List<CheckUpdateResponseModel?>? updateRequest;
UpdateResponseModel({this.updateRequest});
factory UpdateResponseModel.fromJson(Map<String, dynamic> json) => _$UpdateResponseModelFromJson(json);
Map<String, dynamic> toJson() => _$UpdateResponseModelToJson(this);
}
\ No newline at end of file
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'update_response_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
UpdateResponseModel _$UpdateResponseModelFromJson(Map<String, dynamic> json) =>
UpdateResponseModel(
updateRequest:
(json['update_request'] as List<dynamic>?)
?.map(
(e) =>
e == null
? null
: CheckUpdateResponseModel.fromJson(
e as Map<String, dynamic>,
),
)
.toList(),
);
Map<String, dynamic> _$UpdateResponseModelToJson(
UpdateResponseModel instance,
) => <String, dynamic>{'update_request': instance.updateRequest};
import 'package:json_annotation/json_annotation.dart';
import 'package:mypoint_flutter_app/model/update_response_model.dart';
part 'update_response_object.g.dart';
@JsonSerializable()
class UpdateResponseObject {
UpdateResponseModel? data;
UpdateResponseObject({this.data});
factory UpdateResponseObject.fromJson(Map<String, dynamic> json) => _$UpdateResponseObjectFromJson(json);
Map<String, dynamic> toJson() => _$UpdateResponseObjectToJson(this);
}
\ No newline at end of file
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'update_response_object.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
UpdateResponseObject _$UpdateResponseObjectFromJson(
Map<String, dynamic> json,
) => UpdateResponseObject(
data:
json['data'] == null
? null
: UpdateResponseModel.fromJson(json['data'] as Map<String, dynamic>),
);
Map<String, dynamic> _$UpdateResponseObjectToJson(
UpdateResponseObject instance,
) => <String, dynamic>{'data': instance.data};
import '../base/base_model.dart';
class User extends BaseModel {
final int id;
final String username;
final String? name;
final String? email;
final String? phone;
User({required this.id, required this.username, this.name, this.email, this.phone,});
factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json['id'],
username: json['username'],
name: json['name'],
email: json['email'],
phone: json['phone'],
);
}
}
\ No newline at end of file
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:mypoint_flutter_app/configs/api_paths.dart';
import 'package:mypoint_flutter_app/model/check_update_response_model.dart';
import 'package:mypoint_flutter_app/networking/request_manager.dart';
class ApiService {
final RequestManager _requestManager = RequestManager();
Future<CheckUpdateResponseModel?> checkUpdateWithRequestManager() async {
String version = Platform.version;
try {
Map<String, String> params = {
"operating_system": "iOS",
"software_model": "MyPoint",
"version": version,
"build_number": "1",
};
final response = await _requestManager.request(method: 'POST', path: APIPaths.checkUpdate);
return CheckUpdateResponseModel.fromJson(response.data);
} catch (e) {
throw Exception('Failed to check update');
}
}
}
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
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}");
}
}
}
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:mypoint_flutter_app/base/base_response_model.dart';
import '../configs/callbacks.dart';
import '../configs/constants.dart';
import 'model_maker.dart';
enum Method {
GET, POST, PUT
}
class RestfulAPIClient {
final Dio _dio;
RestfulAPIClient(this._dio);
Json header = {};
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 : {};
final option = Options(method: method.name)
.compose(
_dio.options,
path,
queryParameters: query,
data: body,
);
try {
final result = await _dio.fetch<Map<String, dynamic>>(option);
final json = result.data;
if (json == null) 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("===================\nAPI Error Parse false ${error}\n================");
}
}
}
\ No newline at end of file
import 'dart:io';
import 'package:mypoint_flutter_app/configs/api_paths.dart';
import 'package:mypoint_flutter_app/base/base_response_model.dart';
import 'package:mypoint_flutter_app/networking/restful_api.dart';
import '../model/update_response_object.dart';
import 'model_maker.dart';
extension RestfullAPIClientAllApi on RestfulAPIClient {
Future<BaseResponseModel<UpdateResponseObject>> checkUpdateApp() async {
String version = Platform.version;
final body = {"operating_system": "iOS", "software_model": "MyPoint", "version": version, "build_number": "1",};
return requestNormal(APIPaths.checkUpdate, Method.POST, body, (data) => UpdateResponseObject.fromJson(data as Json));
}
}
import 'package:flutter/material.dart';
import 'package:flutter_widget_from_html/flutter_widget_from_html.dart'; // Hiển thị HTML
import 'package:get/get.dart';
import 'onboarding_viewmodel.dart';
class OnboardingScreen extends StatelessWidget {
final OnboardingViewModel controller = Get.find<OnboardingViewModel>();
final FocusNode _focusNode = FocusNode();
void hideKeyboard() {
FocusScope.of(Get.context!).unfocus();
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: hideKeyboard,
child: Scaffold(
resizeToAvoidBottomInset: false,
body: Stack(
children: [
/// 📌 Hiển thị background từ API (hoặc ảnh mặc định)
Obx(() => Positioned.fill(
child: controller.backgroundUrl.value.isNotEmpty
? Image.network(controller.backgroundUrl.value, fit: BoxFit.cover)
: Image.asset("assets/images/bg_onboarding.png", fit: BoxFit.cover),
)),
/// 📌 Nội dung chính
AnimatedPadding(
duration: const Duration(milliseconds: 300),
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 30),
decoration: const BoxDecoration(
color: Colors.redAccent,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(30),
topRight: Radius.circular(30),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
/// 📌 Tiêu đề (Hiển thị nội dung HTML từ API hoặc mặc định)
Obx(() => Visibility(
visible: !_focusNode.hasFocus,
child: HtmlWidget(
controller.contentHtml.value.isNotEmpty
? controller.contentHtml.value
: """<h2 style="color: white;">Tiêu điểm dễ - Trừ tiền mê</h2>
<p style="color: white;">Đừng bỏ lỡ cơ hội tích tới 30% tất cả giao dịch viễn thông
của các nhà mạng và đổi phiếu giảm giá tại hơn 200 thương hiệu được yêu thích nhất.</p>""",
),
)),
/// 📌 Ô nhập số điện thoại
TextField(
focusNode: _focusNode,
keyboardType: TextInputType.phone,
style: const TextStyle(color: Colors.black),
decoration: InputDecoration(
filled: true,
fillColor: Colors.white,
hintText: "Nhập số điện thoại",
hintStyle: const TextStyle(color: Colors.grey),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide.none,
),
prefixIcon: const Icon(Icons.phone, color: Colors.black),
),
onChanged: controller.updatePhoneNumber,
),
const SizedBox(height: 20),
/// 📌 Nút Tiếp Tục
Obx(() => SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: controller.isButtonEnabled
? () {
debugPrint("Số điện thoại: ${controller.phoneNumber.value}");
}
: null,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 15),
backgroundColor: controller.isButtonEnabled ? Colors.white : Colors.white54,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: Text(
"Tiếp tục",
style: TextStyle(
color: controller.isButtonEnabled ? Colors.redAccent : Colors.grey,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
),
)),
const SizedBox(height: 10),
/// 📌 Checkbox + Điều khoản sử dụng + Chính sách bảo mật
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Obx(() => Checkbox(
value: controller.isChecked.value,
onChanged: controller.toggleCheckbox,
activeColor: Colors.white,
checkColor: Colors.red,
side: const BorderSide(color: Colors.white, width: 2),
)),
Expanded(
child: RichText(
text: TextSpan(
style: const TextStyle(color: Colors.white70, fontSize: 12),
children: [
const TextSpan(text: "Bằng việc tiếp tục, bạn đã đọc và đồng ý với "),
WidgetSpan(
child: GestureDetector(
onTap: () => debugPrint("Điều khoản sử dụng"),
child: const Text(
"Điều khoản sử dụng",
style: TextStyle(
decoration: TextDecoration.underline,
color: Colors.white,
),
),
),
),
const TextSpan(text: " và "),
WidgetSpan(
child: GestureDetector(
onTap: () => debugPrint("Chính sách bảo mật"),
child: const Text(
"Chính sách bảo mật",
style: TextStyle(
decoration: TextDecoration.underline,
color: Colors.white,
),
),
),
),
const TextSpan(text: " của MyPoint"),
],
),
),
),
],
),
],
),
),
],
),
),
],
),
),
);
}
}
import 'package:get/get.dart';
import 'package:mypoint_flutter_app/extensions/string_extension.dart';
class OnboardingViewModel extends GetxController {
var phoneNumber = "".obs;
var isChecked = false.obs;
var backgroundUrl = "".obs; // URL ảnh nền
var contentHtml = "".obs; // Nội dung HTML
bool get isButtonEnabled => isChecked.value && phoneNumber.value.isPhoneValid();
/// Cập nhật số điện thoại
void updatePhoneNumber(String value) {
phoneNumber.value = value;
}
/// Cập nhật trạng thái checkbox
void toggleCheckbox(bool? value) {
isChecked.value = value!;
}
/// Gọi API để lấy dữ liệu
Future<void> fetchOnboardingContent() async {
var data = {"": ""};
backgroundUrl.value = data["url_background"] ?? backgroundUrl.value; // Nếu API có ảnh nền, thay thế
contentHtml.value = data["content"] ?? contentHtml.value; // Nếu API có nội dung, thay thế
}
@override
void onInit() {
super.onInit();
fetchOnboardingContent(); // Gọi API khi khởi tạo ViewModel
}
}
class DataPreference {
static final DataPreference _instance = DataPreference._internal();
static DataPreference get instance => _instance;
DataPreference._internal();
String? get token => "";
}
\ No newline at end of file
import 'package:get/get.dart';
import 'package:mypoint_flutter_app/extensions/context_extensions.dart';
mixin navigateHelper {
void showPopupErrorMessage(String message){
Get.context?.showAlertDialog(message);
}
}
\ No newline at end of file
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