Commit a030a6e7 authored by DatHV's avatar DatHV
Browse files

update fix bug, selected avatar image

parent 682ab1de
......@@ -3,6 +3,9 @@
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<application
android:label="@string/app_name"
......@@ -104,5 +107,15 @@
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="mailto" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="tel" />
</intent>
</queries>
</manifest>
......@@ -42,6 +42,11 @@ post_install do |installer|
flutter_additional_ios_build_settings(target)
target.build_configurations.each do |config|
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.0'
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
'$(inherited)',
'PERMISSION_CAMERA=1',
'PERMISSION_PHOTOS=1',
]
end
end
end
......@@ -3,13 +3,11 @@
<plist version="1.0">
<dict>
<key>CFBundleDisplayName</key>
<string>$(APP_DISPLAY_NAME)</string>
<string>MyPoint</string>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>MyPoint</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
......@@ -33,7 +31,7 @@
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSCameraUsageDescription</key>
<string>Ứng dụng cần quyền Camera để quét mã.</string>
<string>Ứng dụng cần quyền Camera để xử dụng tính năng này</string>
<key>NSContactsUsageDescription</key>
<string>Ứng dụng cần quyền để truy cập danh bạ</string>
<key>NSFaceIDUsageDescription</key>
......@@ -44,6 +42,8 @@
<string>Ứng dụng cần Micro khi quay video.</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>Ứng dụng cần quyền Lưu ảnh vào thư viện.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Ứng dụng cần quyền truy cập thư viện ảnh.</string>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UIBackgroundModes</key>
......
......@@ -126,4 +126,5 @@ class APIPaths {//sandbox
static const String myProductMarkAsNotUsedYet = "/myProductMarkAsNotUsedYet/1.0.0";
static const String submitCampaignViewVoucherComplete = "/campaign/api/v3.0/view-voucher/complete";
static const String webUpdateProfile = "/user/api/v2.0/account/users/loginPartner/updateProfile";
static const String updateImageRequest = "/feedbackAddImageRequest/1.0.0";
}
......@@ -4,6 +4,9 @@ import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:mypoint_flutter_app/shared/preferences/data_preference.dart';
import 'package:uuid/uuid.dart';
import '../../services/device_info.dart';
import '../../services/package_info.dart';
import '../../../app/config/api_paths.dart';
class RequestInterceptor extends Interceptor {
@override
......@@ -28,6 +31,8 @@ class RequestInterceptor extends Interceptor {
options.headers.addAll(headers);
await _applyDeleteAccountHeadersIfNeeded(options);
if (kIsWeb) {
_normalizeWebBody(options);
}
......@@ -74,4 +79,23 @@ class RequestInterceptor extends Interceptor {
debugPrint('⚠️ RequestInterceptor: failed to normalize JSON body - $e');
}
}
Future<void> _applyDeleteAccountHeadersIfNeeded(RequestOptions options) async {
if (!options.path.contains(APIPaths.verifyDeleteAccount)) return;
try {
final deviceKey = await DeviceInfo.getDeviceId();
final details = await DeviceInfo.getDetails();
final version = await AppInfoHelper.version;
options.headers.addAll({
'device-key': deviceKey,
'operating-system': details.operatingSystem ?? 'Unknown',
'device-name': details.userDeviceName ?? 'Unknown',
'version': version,
'requestId': const Uuid().v4(),
'lang': 'vi',
});
} catch (e) {
debugPrint('⚠️ RequestInterceptor: failed to add delete headers - $e');
}
}
}
......@@ -71,6 +71,56 @@ class RestfulAPIClient {
}
}
Future<BaseResponseModel<T>> requestMultipart<T>(
String path,
FormData formData,
T Function(dynamic json) parser, {
bool silent = false,
bool allowRetry = false,
}) async {
final opt = _opts(Method.POST, silent, allowRetry)
.copyWith(contentType: 'multipart/form-data')
.compose(_dio.options, path, data: formData);
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 errorCode = map['error_code']?.toString() ?? map['errorCode']?.toString() ?? e.error?.toString();
if (errorCode != null && ErrorCodes.tokenInvalidCodes.contains(errorCode)) {
rethrow;
}
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, bool silent, bool allowRetry) => Options(
method: m.name,
validateStatus: (_) => true,
......
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:mypoint_flutter_app/app/config/api_paths.dart';
import 'package:mypoint_flutter_app/shared/widgets/base_view/base_response_model.dart';
import 'package:mypoint_flutter_app/app/config/constants.dart';
......@@ -7,6 +8,7 @@ import 'package:mypoint_flutter_app/core/network/restful_api_client.dart';
import 'package:mypoint_flutter_app/shared/preferences/data_preference.dart';
import 'package:mypoint_flutter_app/features/voucher/models/product_model.dart';
import '../../app/config/callbacks.dart';
import '../../features/personal/personal_edit_item_model.dart';
import '../services/device_info.dart';
import '../../shared/navigation/directional_screen.dart';
import '../../features/home/models/header_home_model.dart';
......@@ -49,8 +51,34 @@ import '../../features/transaction/history/transaction_category_model.dart';
import '../../features/transaction/history/transaction_history_model.dart';
import '../../features/transaction/history/transaction_history_response_model.dart';
extension RestfulAPIClientAllRequest on RestfulAPIClient {
Future<BaseResponseModel<UpdateImageResponseModel>> uploadImage(String imagePath, String feedbackId) async {
final token = DataPreference.instance.token ?? "";
final formData = FormData.fromMap({
"access_token": token,
"feedback_id": feedbackId,
"lang": "vi",
"image_data": await MultipartFile.fromFile(
imagePath,
filename: imagePath.split('/').last,
),
});
return requestMultipart(APIPaths.updateImageRequest, formData, _parseUpdateImageResponse);
}
UpdateImageResponseModel _parseUpdateImageResponse(dynamic data) {
if (data is Json) {
if (data['image_id'] != null) {
return UpdateImageResponseModel.fromJson(data);
}
final nested = data['data'];
if (nested is Json) {
return UpdateImageResponseModel.fromJson(nested);
}
}
return UpdateImageResponseModel(imageId: null);
}
Future<BaseResponseModel<UpdateResponseModel>> checkUpdateApp() async {
final operatingSystem = Platform.operatingSystem;
final version = await AppInfoHelper.version;
......
......@@ -71,7 +71,7 @@ class _AffiliateTabScreenState extends BaseState<AffiliateTabScreen> with BasicS
await viewModel.refreshData(isShowLoading: false);
},
child: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 100),
padding: const EdgeInsets.fromLTRB(16, 12, 16, 140),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
......
......@@ -59,12 +59,19 @@ class CreatePasswordScreen extends StatelessWidget {
return Text(err, style: const TextStyle(color: Colors.red));
}
}),
Obx(() {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(height: 8),
_buildInfoGuide(icon: Icons.info_outline, text: "Mật khẩu gồm 6 chữ số"),
_buildInfoGuide(icon: Icons.info_outline, text: "Mật khẩu gồm 6 chữ số", isValidate: vm.isSixDigits.value),
const SizedBox(height: 4),
_buildInfoGuide(icon: Icons.info_outline, text: "Không bao gồm dãy số trùng nhau"),
_buildInfoGuide(icon: Icons.info_outline, text: "Không bao gồm dãy số trùng nhau", isValidate: vm.isNotRepeatedSequence.value),
const SizedBox(height: 4),
_buildInfoGuide(icon: Icons.info_outline, text: "Không bao gồm dãy số liên tiếp"),
_buildInfoGuide(icon: Icons.info_outline, text: "Không bao gồm dãy số liên tiếp", isValidate: vm.isNotSequential.value),
],
);
}),
],
),
),
......@@ -100,13 +107,14 @@ class CreatePasswordScreen extends StatelessWidget {
});
}
Widget _buildInfoGuide({required IconData icon, required String text}) {
Widget _buildInfoGuide({required IconData icon, required String text, required bool isValidate}) {
final color = isValidate ? BaseColor.second400 : BaseColor.primary300;
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, color: BaseColor.second400, size: 24),
Icon(icon, color: color, size: 24),
SizedBox(width: 8),
Expanded(child: Text(text, style: const TextStyle(color: BaseColor.second400, fontSize: 14))),
Expanded(child: Text(text, style: TextStyle(color: color, fontSize: 14))),
],
);
}
......@@ -118,7 +126,11 @@ class CreatePasswordScreen extends StatelessWidget {
required ValueChanged<String> onChanged,
}) {
return TextField(
inputFormatters: [LengthLimitingTextInputFormatter(6)],
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(6),
],
obscureText: obscureText,
onChanged: onChanged,
decoration: InputDecoration(
......
......@@ -7,6 +7,9 @@ class CreatePasswordViewModel extends GetxController {
var confirmPassword = "".obs;
var errorMessage = "".obs;
var isButtonEnabled = false.obs;
var isSixDigits = false.obs;
var isNotRepeatedSequence = false.obs;
var isNotSequential = false.obs;
CreatePasswordViewModel(this.repository);
......@@ -21,7 +24,12 @@ class CreatePasswordViewModel extends GetxController {
}
void _validate() {
if (newPassword.value.length != 6) {
isSixDigits.value = _isSixDigits(newPassword.value);
isNotRepeatedSequence.value = _isNotAllSameDigits(newPassword.value);
isNotSequential.value = _isNotSequentialDigits(newPassword.value);
if (!isSixDigits.value || !isNotRepeatedSequence.value || !isNotSequential.value) {
errorMessage.value = "";
isButtonEnabled.value = false;
return;
}
......@@ -39,6 +47,29 @@ class CreatePasswordViewModel extends GetxController {
}
}
bool _isSixDigits(String value) => RegExp(r'^\d{6}$').hasMatch(value);
bool _isNotAllSameDigits(String value) {
if (!_isSixDigits(value)) return false;
final first = value[0];
for (var i = 1; i < value.length; i++) {
if (value[i] != first) return true;
}
return false;
}
bool _isNotSequentialDigits(String value) {
if (!_isSixDigits(value)) return false;
final digits = value.codeUnits.map((c) => c - 48).toList();
var asc = true;
var desc = true;
for (var i = 1; i < digits.length; i++) {
if (digits[i] != digits[i - 1] + 1) asc = false;
if (digits[i] != digits[i - 1] - 1) desc = false;
}
return !(asc || desc);
}
Future<void> onSubmit() async {
if (!isButtonEnabled.value) return;
try {
......
import 'package:flutter/widgets.dart';
import 'package:get/get.dart';
import 'package:mypoint_flutter_app/core/network/restful_api_client_all_request.dart';
import 'package:mypoint_flutter_app/features/create_pass/signup_create_password_repository.dart';
......@@ -18,7 +19,9 @@ class ResetCreatePasswordRepository extends RestfulApiViewModel implements ICrea
return client.accountPasswordReset(phoneNumber, password).then((value) {
hideLoading();
if (value.status == "success" || value.code == 200) {
WidgetsBinding.instance.addPostFrameCallback((_) {
Get.offNamed(loginScreen, arguments: {'phone': phoneNumber, 'password': password});
});
showToastMessage("Đặt lại mật khẩu thành công.");
}
return value;
......
......@@ -35,6 +35,10 @@ class SignUpCreatePasswordRepository extends RestfulApiViewModel implements ICre
});
}
void _postFrame(VoidCallback action) {
WidgetsBinding.instance.addPostFrameCallback((_) => action());
}
void _autoLogin(String password) {
showLoading();
client.login(phoneNumber, password).then((response) async {
......@@ -44,7 +48,9 @@ class SignUpCreatePasswordRepository extends RestfulApiViewModel implements ICre
await PushTokenService.uploadIfLogged();
_getUserProfile();
} else {
_postFrame(() {
Get.offNamed(loginScreen, arguments: {'phone': phoneNumber});
});
}
});
}
......@@ -57,13 +63,19 @@ class SignUpCreatePasswordRepository extends RestfulApiViewModel implements ICre
if (value.isSuccess && userProfile != null) {
await DataPreference.instance.saveUserProfile(userProfile);
if (await _biometricManager.canCheckBiometrics()) {
_postFrame(() {
Get.to(BiometricAuthScreen());
});
} else {
_postFrame(() {
Get.toNamed(mainScreen);
});
}
} else {
DataPreference.instance.clearLoginToken();
_postFrame(() {
Get.offNamed(loginScreen, arguments: {'phone': phoneNumber});
});
}
});
}
......
......@@ -6,11 +6,11 @@ class CheckInDataModel {
final List<Counter>? counters;
Counter? get dailyCounter {
return counters?.firstWhereOrNull((counter) => counter.counterName == 'CUSTOMER_CHECKIN_DAILY_TFC');
return counters?.where((counter) => counter.counterName == "CUSTOMER_CHECKIN_DAILY_TFC").last;
}
Counter? get currentCounter {
return counters?.firstWhereOrNull((counter) => counter.counterName == 'CUSTOMER_CHECKIN_NE_TFC');
return counters?.where((counter) => counter.counterName == "CUSTOMER_CHECKIN_NE_TFC").last;
}
CheckInDataModel({this.campaignCode, this.counters});
......
......@@ -40,7 +40,8 @@ class _DailyCheckInScreenState extends BaseState<DailyCheckInScreen> with BasicS
return Scaffold(
appBar: CustomNavigationBar(title: "Check-in nhận quà"),
body: Obx(() {
int point = _viewModel.submitData.value?.customerBalance?.amountActive?.toInt() ?? UserPointManager().point;
final reward = _viewModel.submitData.value?.reward?.toInt() ?? 0;
final point = _viewModel.submitData.value?.customerBalance?.amountActive?.toInt() ?? (UserPointManager().point + reward);
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
......
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