Merge pull request 'Build 66' (#617) from main into test-flight

Reviewed-on: eatvio/eatvio_app#617
This commit is contained in:
mso7nueh
2025-08-08 08:48:12 +00:00
7 changed files with 290 additions and 146 deletions

View File

@@ -11,6 +11,30 @@ jobs:
- name: Checkout
uses: actions/checkout@v2
- name: Checkout env repository
uses: actions/checkout@v2
with:
repository: mso7nueh/eatvio_app_env
path: env_repo
token: ${{ vars.MSO7NUEH_TOKEN }}
ref: main
- name: Copy .env file
run: |
mkdir -p env
cp env_repo/.env env/.env
- name: Load environment variables from .env
run: |
set -a
source env/.env
set +a
# Export all variables to GITHUB_ENV
while IFS= read -r line; do
if [[ $line =~ ^[A-Za-z_][A-Za-z0-9_]*= ]]; then
echo "$line" >> $GITHUB_ENV
fi
done < env/.env
- name: Unlock login keychain
run: |
@@ -18,9 +42,19 @@ jobs:
env:
KEYCHAIN_PASSWORD: ${{ vars.IOS_RUNNER_CI_PASSWORD }}
- name: Setup API key
run: |
echo "${API_APPSTORE_KEY}" | tr -d '\n' | base64 --decode > ios/AuthKey.p8
chmod 600 ios/AuthKey.p8
ls -la ios/AuthKey.p8
echo "File size: $(wc -c < ios/AuthKey.p8) bytes"
echo "Contents of ios directory:"
ls -la ios/
env:
API_APPSTORE_KEY: ${{ vars.API_APPSTORE_KEY }}
- name: Flutter build
run: |
echo "${API_APPSTORE_KEY}" | base64 --decode > ios/AuthKey.p8
flutter clean
flutter pub get
flutter pub upgrade
@@ -36,4 +70,4 @@ jobs:
MATCH_GIT_URL: http://gitea.office.local:3000/mso7nueh/fastlane
FASTLANE_USER: eatvio2023@icloud.com
FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: ${{ vars.FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD }}
API_APPSTORE_KEY: ${{ vars.API_APPSTORE_KEY }}
API_APPSTORE_KEY: ${{ vars.API_APPSTORE_KEY }}

View File

@@ -396,7 +396,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 65;
CURRENT_PROJECT_VERSION = 67;
DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
@@ -546,7 +546,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 65;
CURRENT_PROJECT_VERSION = 67;
DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
@@ -584,7 +584,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 65;
CURRENT_PROJECT_VERSION = 67;
DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (

View File

@@ -21,7 +21,7 @@
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>65</string>
<string>67</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSRequiresIPhoneOS</key>

View File

@@ -18,8 +18,35 @@ default_platform(:ios)
platform :ios do
desc "Push a new beta build to TestFlight"
lane :beta do
UI.message "Current working directory: #{Dir.pwd}"
UI.message "Checking API key file..."
possible_paths = [
"../AuthKey.p8",
"./AuthKey.p8",
"../../AuthKey.p8",
File.expand_path("../AuthKey.p8"),
File.expand_path("./AuthKey.p8")
]
auth_key_path = nil
possible_paths.each do |path|
if File.exist?(path)
UI.message "Found AuthKey.p8 at: #{path}"
auth_key_path = File.expand_path(path)
break
end
end
if auth_key_path.nil?
UI.user_error!("API key file not found. Tried paths: #{possible_paths.join(', ')}")
end
UI.message "Using auth_key_path: #{auth_key_path}"
match(type: "appstore", readonly: true)
increment_build_number(xcodeproj: "Runner.xcodeproj")
build_app(
workspace: "Runner.xcworkspace",
scheme: "Runner",
@@ -27,7 +54,30 @@ platform :ios do
codesigning_identity: "Apple Distribution",
xcargs: "--dart-define=HIDE_PLANS=true"
)
app_store_connect_api_key(key_id: "325JS9JCZH", issuer_id: "dfa27d1d-2bb8-4d4d-847e-0b32f9b880b3", key_filepath: "./AuthKey.p8")
upload_to_testflight
app_store_connect_api_key(
key_id: "325JS9JCZH",
issuer_id: "dfa27d1d-2bb8-4d4d-847e-0b32f9b880b3",
key_filepath: auth_key_path,
duration: 1200,
in_house: false
)
begin
upload_to_testflight(
skip_waiting_for_build_processing: true,
skip_submission: true,
distribute_external: false,
notify_external_testers: false,
changelog: "TestFlight build #{get_build_number(xcodeproj: 'Runner.xcodeproj')}",
verbose: true
)
rescue => ex
UI.error "Upload to TestFlight failed: #{ex.message}"
UI.error "Full error: #{ex.backtrace.join("\n")}"
raise ex
end
end
end

View File

@@ -136,15 +136,7 @@ class DiaryCubit extends Cubit<DiaryState> {
final fifthMeal = MealModel(id: 0, time: '0', typeNumber: 5, satiety: 0, mood: 0, portions: fifthMealPortions ?? [], photos: []);
emit(
DiaryState.loadedTomorrowWithDiet(
diary: diaryDay.copyWith(
meals: [
firstMeal,
secondMeal,
thirdMeal,
fourthMeal,
fifthMeal,
],
),
diary: diaryDay.copyWith(meals: [firstMeal, secondMeal, thirdMeal, fourthMeal, fifthMeal]),
selectedDay: selectedDay,
selectedMeal: selectedMeal,
),
@@ -156,7 +148,7 @@ class DiaryCubit extends Cubit<DiaryState> {
Future<void> fetchDiary([bool needLoading = false, int? diaryId, int? currentDay]) async {
this.diaryId ??= diaryId;
final isConsultantView = this.diaryId != null;
if (needLoading) {
emit(const DiaryState.loading());
selectedMeal = 1;
@@ -180,7 +172,8 @@ class DiaryCubit extends Cubit<DiaryState> {
String? planName;
if (diaryDay.chosenPlanId != null) {
final chosenPlanModel = await chatRepository.getChosenPlan(id: diaryDay.chosenPlanId!);
if (diaryDay.typeOfDiary.diaryState == DiaryStateEnum.lastDayCommented || diaryDay.typeOfDiary.diaryState == DiaryStateEnum.commented) {
if (!isConsultantView &&
(diaryDay.typeOfDiary.diaryState == DiaryStateEnum.lastDayCommented || diaryDay.typeOfDiary.diaryState == DiaryStateEnum.commented)) {
await diaryRepository.viewCommentOfDiaryDayByConsultant(diaryId: diaryDay.id);
}
consultantReview = await diaryRepository.getDiaryComments(planId: diaryDay.chosenPlanId!, diaryId: diaryDay.id);
@@ -192,24 +185,26 @@ class DiaryCubit extends Cubit<DiaryState> {
final isDiaryFinished = diaryDay.typeOfDiary.diaryState == DiaryStateEnum.finished || diaryDay.typeOfDiary.diaryState == DiaryStateEnum.lastDayFinished;
final isDiaryCommented = diaryDay.typeOfDiary.diaryState == DiaryStateEnum.commented ||
final isDiaryCommented =
diaryDay.typeOfDiary.diaryState == DiaryStateEnum.commented ||
diaryDay.typeOfDiary.diaryState == DiaryStateEnum.lastDayCommented ||
diaryDay.typeOfDiary.diaryState == DiaryStateEnum.reviewedByUser;
emit(
DiaryState.loaded(
diary: diaryDay,
nutrition: nutrition,
selectedDay: selectedDay,
selectedMeal: selectedMeal,
isConsultantView: this.diaryId != null,
consultantReview: consultantReview,
isDiaryFinished: isDiaryFinished,
isDiaryCommented: isDiaryCommented,
chosenPlanCurrentDay: currentPlanDay,
chosenPlanDuration: planDuration,
chosenPlanName: planName,
isMoodEnabled: isMoodEnabled,
isWakeUpAndRetireEnabled: isWakeUpAndRetireTimeEnabled),
diary: diaryDay,
nutrition: nutrition,
selectedDay: selectedDay,
selectedMeal: selectedMeal,
isConsultantView: isConsultantView,
consultantReview: consultantReview,
isDiaryFinished: isDiaryFinished,
isDiaryCommented: isDiaryCommented,
chosenPlanCurrentDay: currentPlanDay,
chosenPlanDuration: planDuration,
chosenPlanName: planName,
isMoodEnabled: isMoodEnabled,
isWakeUpAndRetireEnabled: isWakeUpAndRetireTimeEnabled,
),
);
} catch (e, st) {
log(e.toString(), stackTrace: st);
@@ -217,17 +212,8 @@ class DiaryCubit extends Cubit<DiaryState> {
}
}
Future<void> finishDiaryDay({
required int diaryId,
required String wakeUp,
required String retire,
}) async {
await diaryRepository.finishDiaryDay(
diaryId: diaryId,
wakeUp: wakeUp,
retire: retire,
comment: comment.isEmpty ? null : comment,
);
Future<void> finishDiaryDay({required int diaryId, required String wakeUp, required String retire}) async {
await diaryRepository.finishDiaryDay(diaryId: diaryId, wakeUp: wakeUp, retire: retire, comment: comment.isEmpty ? null : comment);
await fetchDiary();
}

View File

@@ -760,76 +760,92 @@ class _ProfileScreenState extends State<ProfileScreen> {
child: Row(
children: [
PopupMenuButton(
onSelected: (value) async {
if (value == 'view') {
context.pushTransparentRoute(rootNavigator: true, PhotoViewScreen(imageUrls: [baseUrl + state.profile.profilePhotoUrl], index: 0));
}
if (value == 'replace') {
showCupertinoModalPopup<void>(
context: context,
builder: (context) {
final s = S.of(context);
return CupertinoActionSheet(
actions: [
CupertinoActionSheetAction(
onPressed: () async {
PermissionHandler.checkPermission(
permission: Permission.camera,
title: s.allowAccessToCamera,
description: s.allowAccessToCameraDescription,
onGranted: () async {
AppRouter.navigatorKey.currentContext?.pop();
final picker = ImagePicker();
XFile? image = await picker.pickImage(source: ImageSource.camera);
if (image != null) {
await profileCubit.uploadPhoto(File(image.path));
await sl<ProfileAvatarCubit>().getMyProfile();
}
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
color: Colors.white,
constraints: const BoxConstraints(minWidth: 260),
surfaceTintColor: Colors.transparent,
elevation: 2,
itemBuilder: (context) => [
_buildCompactItem(
icon: 'assets/icons/eye.svg',
text: 'Посмотреть',
onTap: () async {
context.pushTransparentRoute(rootNavigator: true, PhotoViewScreen(imageUrls: [baseUrl + state.profile.profilePhotoUrl], index: 0));
},
),
if (widget.author == null || widget.author!.authorIsMe) ...[
_buildDivider(),
_buildCompactItem(
icon: 'assets/icons/edit_paint.svg',
text: 'Заменить',
onTap: () async {
showCupertinoModalPopup<void>(
context: context,
builder: (context) {
final s = S.of(context);
return CupertinoActionSheet(
actions: [
CupertinoActionSheetAction(
onPressed: () async {
PermissionHandler.checkPermission(
permission: Permission.camera,
title: s.allowAccessToCamera,
description: s.allowAccessToCameraDescription,
onGranted: () async {
AppRouter.navigatorKey.currentContext?.pop();
final picker = ImagePicker();
XFile? image = await picker.pickImage(source: ImageSource.camera);
if (image != null) {
await profileCubit.uploadPhoto(File(image.path));
await sl<ProfileAvatarCubit>().getMyProfile();
}
},
context: context,
);
},
context: context,
);
},
child: const Row(children: [Icon(Icons.photo_camera), SizedBox(width: 8), Text('Фото')]),
),
CupertinoActionSheetAction(
onPressed: () async {
PermissionHandler.checkPermission(
permission: Permission.photos,
title: s.allowAcessToGalery,
description: s.allowAcessToGaleryDescription,
onGranted: () async {
AppRouter.navigatorKey.currentContext?.pop();
final picker = ImagePicker();
XFile? image = await picker.pickImage(source: ImageSource.gallery);
if (image != null) {
await profileCubit.uploadPhoto(File(image.path));
await sl<ProfileAvatarCubit>().getMyProfile();
}
child: const Row(children: [Icon(Icons.photo_camera), SizedBox(width: 8), Text('Фото')]),
),
CupertinoActionSheetAction(
onPressed: () async {
PermissionHandler.checkPermission(
permission: Permission.photos,
title: s.allowAcessToGalery,
description: s.allowAcessToGaleryDescription,
onGranted: () async {
AppRouter.navigatorKey.currentContext?.pop();
final picker = ImagePicker();
XFile? image = await picker.pickImage(source: ImageSource.gallery);
if (image != null) {
await profileCubit.uploadPhoto(File(image.path));
await sl<ProfileAvatarCubit>().getMyProfile();
}
},
context: context,
);
},
context: context,
);
},
child: const Row(children: [Icon(Icons.photo), SizedBox(width: 8), Text('Галерея')]),
),
],
cancelButton: CupertinoActionSheetAction(
onPressed: () => Navigator.of(context).pop(),
isDestructiveAction: true,
child: const Text('Отмена'),
),
child: const Row(children: [Icon(Icons.photo), SizedBox(width: 8), Text('Галерея')]),
),
],
cancelButton: CupertinoActionSheetAction(
onPressed: () => Navigator.of(context).pop(),
isDestructiveAction: true,
child: const Text('Отмена'),
),
);
},
);
},
);
}
if (value == 'delete') {
await profileCubit.deletePhoto();
await sl<ProfileAvatarCubit>().getMyProfile();
}
},
itemBuilder: (context) => [
const PopupMenuItem(value: 'view', child: Text('Посмотреть')),
if (widget.author == null || widget.author!.authorIsMe) const PopupMenuItem(value: 'replace', child: Text('Заменить')),
if (widget.author == null || widget.author!.authorIsMe) const PopupMenuItem(value: 'delete', child: Text('Удалить')),
),
_buildDivider(),
_buildCompactItem(
icon: 'assets/icons/red_trash.svg',
text: 'Удалить',
onTap: () async {
await profileCubit.deletePhoto();
await sl<ProfileAvatarCubit>().getMyProfile();
},
),
],
],
child: Hero(
tag: state.profile.profilePhotoUrl,
@@ -899,41 +915,68 @@ class _ProfileScreenState extends State<ProfileScreen> {
),
const Spacer(),
PopupMenuButton(
onSelected: (value) {
if (value == 'share') {
final box = context.findRenderObject() as RenderBox?;
SharePlus.instance.share(
ShareParams(uri: Uri.parse('$baseUrl/ru/profile/${state.profile.slug}'), sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size),
);
} else if (value == 'block') {
isFirstFetch = true;
sl<BlockedUsersCubit>().blockUser(state.profile.slug);
widget.author == null || widget.author!.authorIsMe ? profileCubit.getMyProfile() : profileCubit.getProfileBySlug(widget.author!.authorSlug);
if (state.profile.roles.contains('consultant')) {
plansCubit.getPlans(state.profile.slug);
}
isFirstFetch = false;
} else if (value == 'unblock') {
isFirstFetch = true;
sl<BlockedUsersCubit>().unblockUser(state.profile.slug);
widget.author == null || widget.author!.authorIsMe ? profileCubit.getMyProfile() : profileCubit.getProfileBySlug(widget.author!.authorSlug);
if (state.profile.roles.contains('consultant')) {
plansCubit.getPlans(state.profile.slug);
}
isFirstFetch = false;
} else {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (context) => ComplainingBottomSheet(complainedContentType: ComplainedContent.profile, slug: widget.author?.authorSlug),
);
}
},
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
color: Colors.white,
constraints: const BoxConstraints(minWidth: 260),
surfaceTintColor: Colors.transparent,
elevation: 2,
itemBuilder: (context) => [
const PopupMenuItem(value: 'share', child: Text('Поделиться')),
if (widget.author != null) const PopupMenuItem(value: 'complain', child: Text('Пожаловаться')),
if (widget.author != null && !state.profile.isBlockedByMe) const PopupMenuItem(value: 'block', child: Text('Заблокировать')),
if (widget.author != null && state.profile.isBlockedByMe) const PopupMenuItem(value: 'unblock', child: Text('Разблокировать')),
_buildCompactItem(
icon: 'assets/icons/share2.svg',
text: 'Поделиться',
onTap: () {
final box = context.findRenderObject() as RenderBox?;
SharePlus.instance.share(
ShareParams(uri: Uri.parse('$baseUrl/ru/profile/${state.profile.slug}'), sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size),
);
},
),
if (widget.author != null) ...[
_buildDivider(),
_buildCompactItem(
icon: 'assets/icons/complain.svg',
text: 'Пожаловаться',
onTap: () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (context) => ComplainingBottomSheet(complainedContentType: ComplainedContent.profile, slug: widget.author?.authorSlug),
);
},
),
],
if (widget.author != null && !state.profile.isBlockedByMe) ...[
_buildDivider(),
_buildCompactItem(
icon: 'assets/icons/complain.svg',
text: 'Заблокировать',
onTap: () {
isFirstFetch = true;
sl<BlockedUsersCubit>().blockUser(state.profile.slug);
widget.author == null || widget.author!.authorIsMe ? profileCubit.getMyProfile() : profileCubit.getProfileBySlug(widget.author!.authorSlug);
if (state.profile.roles.contains('consultant')) {
plansCubit.getPlans(state.profile.slug);
}
isFirstFetch = false;
},
),
],
if (widget.author != null && state.profile.isBlockedByMe) ...[
_buildDivider(),
_buildCompactItem(
icon: 'assets/icons/complain.svg',
text: 'Разблокировать',
onTap: () {
isFirstFetch = true;
sl<BlockedUsersCubit>().unblockUser(state.profile.slug);
widget.author == null || widget.author!.authorIsMe ? profileCubit.getMyProfile() : profileCubit.getProfileBySlug(widget.author!.authorSlug);
if (state.profile.roles.contains('consultant')) {
plansCubit.getPlans(state.profile.slug);
}
isFirstFetch = false;
},
),
],
],
child: const Icon(Icons.more_horiz_rounded),
),
@@ -942,6 +985,33 @@ class _ProfileScreenState extends State<ProfileScreen> {
);
}
PopupMenuItem _buildCompactItem({required String icon, required String text, required Function() onTap}) {
return PopupMenuItem(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
height: 36,
onTap: onTap,
child: SizedBox(
width: double.infinity,
child: Row(
children: [
Text(text, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),
const Spacer(),
SvgPicture.asset(icon),
],
),
),
);
}
PopupMenuItem _buildDivider() {
return PopupMenuItem(
enabled: false,
height: 1,
padding: EdgeInsets.zero,
child: Container(margin: const EdgeInsets.symmetric(horizontal: 12), height: 1, color: appTheme.dividerColor),
);
}
Row _additional(String title, String value) {
return Row(
children: [

View File

@@ -1,6 +1,6 @@
name: eatvio_app
description: A new Flutter project.
version: 1.2.7+66
version: 1.2.8+68
publish_to: 'none'
environment:
@@ -13,7 +13,11 @@ dependencies:
flutter_localizations:
sdk: flutter
flutter_secure_storage: ^9.2.4
flutter_secure_storage:
git:
url: https://github.com/sherlockvn/flutter_secure_storage.git
ref: remove-old-encrypt-algorithm
path: flutter_secure_storage
flutter_bloc: ^9.1.1
cached_network_image: ^3.4.1
flutter_svg: ^2.2.0