diff --git a/.gitea/workflows/test-flight-cd.yaml b/.gitea/workflows/test-flight-cd.yaml index 4320ecc5..ac54d483 100644 --- a/.gitea/workflows/test-flight-cd.yaml +++ b/.gitea/workflows/test-flight-cd.yaml @@ -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 }} \ No newline at end of file + API_APPSTORE_KEY: ${{ vars.API_APPSTORE_KEY }} diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 6388d648..2e0d56e4 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -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 = ( diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 9a16914c..a1ded5ee 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -21,7 +21,7 @@ CFBundleSignature ???? CFBundleVersion - 65 + 67 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/ios/fastlane/Fastfile b/ios/fastlane/Fastfile index d9653264..f7261196 100644 --- a/ios/fastlane/Fastfile +++ b/ios/fastlane/Fastfile @@ -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 diff --git a/lib/features/diary/presentation/bloc/diary_cubit.dart b/lib/features/diary/presentation/bloc/diary_cubit.dart index 1bbfbbc7..6e6f8891 100644 --- a/lib/features/diary/presentation/bloc/diary_cubit.dart +++ b/lib/features/diary/presentation/bloc/diary_cubit.dart @@ -136,15 +136,7 @@ class DiaryCubit extends Cubit { 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 { Future 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 { 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 { 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 { } } - Future 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 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(); } diff --git a/lib/features/profile/presentation/pages/profile_screen.dart b/lib/features/profile/presentation/pages/profile_screen.dart index 211543a9..f3329074 100644 --- a/lib/features/profile/presentation/pages/profile_screen.dart +++ b/lib/features/profile/presentation/pages/profile_screen.dart @@ -760,76 +760,92 @@ class _ProfileScreenState extends State { 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( - 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().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( + 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().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().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().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().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().getMyProfile(); + }, + ), + ], ], child: Hero( tag: state.profile.profilePhotoUrl, @@ -899,41 +915,68 @@ class _ProfileScreenState extends State { ), 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().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().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().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().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 { ); } + 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: [ diff --git a/pubspec.yaml b/pubspec.yaml index fefe422b..3895a06b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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