diff --git a/.gitea/workflows/test-flight-cd.yaml b/.gitea/workflows/test-flight-cd.yaml index c1244bd9..0bb7c723 100644 --- a/.gitea/workflows/test-flight-cd.yaml +++ b/.gitea/workflows/test-flight-cd.yaml @@ -42,9 +42,16 @@ jobs: env: KEYCHAIN_PASSWORD: ${{ vars.IOS_RUNNER_CI_PASSWORD }} - - name: Flutter build + - name: Setup API key run: | echo "${API_APPSTORE_KEY}" | base64 --decode > ios/AuthKey.p8 + chmod 600 ios/AuthKey.p8 + ls -la ios/AuthKey.p8 + env: + API_APPSTORE_KEY: ${{ vars.API_APPSTORE_KEY }} + + - name: Flutter build + run: | flutter clean flutter pub get flutter pub upgrade diff --git a/ios/fastlane/Fastfile b/ios/fastlane/Fastfile index d9653264..a43260fe 100644 --- a/ios/fastlane/Fastfile +++ b/ios/fastlane/Fastfile @@ -18,8 +18,16 @@ default_platform(:ios) platform :ios do desc "Push a new beta build to TestFlight" lane :beta do + UI.message "Checking API key file..." + if File.exist?("./AuthKey.p8") + UI.message "API key file exists" + else + UI.user_error!("API key file not found at ./AuthKey.p8") + end + match(type: "appstore", readonly: true) increment_build_number(xcodeproj: "Runner.xcodeproj") + build_app( workspace: "Runner.xcworkspace", scheme: "Runner", @@ -27,7 +35,29 @@ 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: "./AuthKey.p8", + duration: 1200, + in_house: false + ) + + begin + upload_to_testflight( + api_key_path: "./AuthKey.p8", + 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/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: [