diff --git a/ios/ExportOptions.plist b/ios/ExportOptions.plist new file mode 100644 index 0000000..39c520d --- /dev/null +++ b/ios/ExportOptions.plist @@ -0,0 +1,14 @@ + + + + + method + app-store-connect + destination + upload + signingStyle + automatic + teamID + Z4N56CL767 + + diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index e744daa..6f5dbea 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -115,7 +115,6 @@ 58C1A39422F3ADDA7073882C /* Pods-RunnerTests.release.xcconfig */, CAFF337A6DF135B15E2E5A82 /* Pods-RunnerTests.profile.xcconfig */, ); - name = Pods; path = Pods; sourceTree = ""; }; @@ -481,7 +480,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.coves.covesFlutter; + PRODUCT_BUNDLE_IDENTIFIER = social.coves; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; @@ -665,7 +664,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.coves.covesFlutter; + PRODUCT_BUNDLE_IDENTIFIER = social.coves; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -681,6 +680,9 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; + CODE_SIGN_IDENTITY = "Apple Distribution"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Distribution"; + CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = Z4N56CL767; ENABLE_BITCODE = NO; @@ -689,8 +691,9 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.coves.covesFlutter; + PRODUCT_BUNDLE_IDENTIFIER = social.coves; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = "Coves App Store"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 6cc4a53..6e10f30 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -2,6 +2,8 @@ + CADisableMinimumFrameDurationOnPhone + CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName @@ -20,10 +22,31 @@ $(FLUTTER_BUILD_NAME) CFBundleSignature ???? + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLName + social.coves.oauth + CFBundleURLSchemes + + social.coves + + + CFBundleVersion $(FLUTTER_BUILD_NUMBER) + ITSAppUsesNonExemptEncryption + LSRequiresIPhoneOS + NSCameraUsageDescription + Coves needs access to your camera to take photos for your profile and posts. + NSPhotoLibraryUsageDescription + Coves needs access to your photo library to select images for your profile and posts. + UIApplicationSupportsIndirectInputEvents + UILaunchStoryboardName LaunchScreen UIMainStoryboardFile @@ -41,26 +64,5 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - CADisableMinimumFrameDurationOnPhone - - UIApplicationSupportsIndirectInputEvents - - NSPhotoLibraryUsageDescription - Coves needs access to your photo library to select images for your profile and posts. - NSCameraUsageDescription - Coves needs access to your camera to take photos for your profile and posts. - CFBundleURLTypes - - - CFBundleTypeRole - Editor - CFBundleURLName - social.coves.oauth - CFBundleURLSchemes - - social.coves - - - diff --git a/lib/screens/community/community_feed_screen.dart b/lib/screens/community/community_feed_screen.dart index b6025c6..1dd66a6 100644 --- a/lib/screens/community/community_feed_screen.dart +++ b/lib/screens/community/community_feed_screen.dart @@ -11,7 +11,6 @@ import '../../models/post.dart'; import '../../providers/auth_provider.dart'; import '../../providers/community_subscription_provider.dart'; import '../../providers/vote_provider.dart'; -import '../../services/api_exceptions.dart'; import '../../services/coves_api_service.dart'; import '../../utils/display_utils.dart'; import '../../utils/error_messages.dart'; @@ -58,6 +57,7 @@ class _CommunityFeedScreenState extends State { CommunityView? _community; bool _isLoadingCommunity = false; String? _communityError; + bool _communityIsAuthError = false; // Feed state List _posts = []; @@ -137,6 +137,7 @@ class _CommunityFeedScreenState extends State { setState(() { _isLoadingCommunity = true; _communityError = null; + _communityIsAuthError = false; }); try { @@ -163,53 +164,14 @@ class _CommunityFeedScreenState extends State { ); } } - } on NetworkException catch (e) { - if (kDebugMode) { - debugPrint('Network error loading community: $e'); - } - if (mounted) { - setState(() { - _communityError = 'Please check your internet connection'; - _isLoadingCommunity = false; - }); - } - } on NotFoundException catch (e) { - if (kDebugMode) { - debugPrint('Community not found: $e'); - } - if (mounted) { - setState(() { - _communityError = 'Community not found'; - _isLoadingCommunity = false; - }); - } - } on ServerException catch (e) { - if (kDebugMode) { - debugPrint('Server error loading community: $e'); - } - if (mounted) { - setState(() { - _communityError = 'Server error. Please try again later'; - _isLoadingCommunity = false; - }); - } - } on ApiException catch (e) { - if (kDebugMode) { - debugPrint('API error loading community: $e'); - } - if (mounted) { - setState(() { - _communityError = e.message; - _isLoadingCommunity = false; - }); - } - } on Exception catch (e) { + } catch (e) { if (kDebugMode) { debugPrint('Error loading community: $e'); } if (mounted) { setState(() { - _communityError = ErrorMessages.getUserFriendly(e.toString()); + _communityError = ErrorMessage.community(e); + _communityIsAuthError = isAuthError(e); _isLoadingCommunity = false; }); } @@ -251,43 +213,13 @@ class _CommunityFeedScreenState extends State { _syncViewerStates(response.feed); } - } on NetworkException catch (e) { - if (kDebugMode) { - debugPrint('Network error loading feed: $e'); - } - if (mounted) { - setState(() { - _feedError = 'Please check your internet connection'; - _isLoadingFeed = false; - }); - } - } on ServerException catch (e) { - if (kDebugMode) { - debugPrint('Server error loading feed: $e'); - } - if (mounted) { - setState(() { - _feedError = 'Server error. Please try again later'; - _isLoadingFeed = false; - }); - } - } on ApiException catch (e) { - if (kDebugMode) { - debugPrint('API error loading feed: $e'); - } - if (mounted) { - setState(() { - _feedError = e.message; - _isLoadingFeed = false; - }); - } - } on Exception catch (e) { + } catch (e) { if (kDebugMode) { debugPrint('Error loading community feed: $e'); } if (mounted) { setState(() { - _feedError = ErrorMessages.getUserFriendly(e.toString()); + _feedError = ErrorMessage.loadFeed(e); _isLoadingFeed = false; }); } @@ -320,34 +252,14 @@ class _CommunityFeedScreenState extends State { _syncViewerStates(response.feed); } - } on NetworkException catch (e) { - if (kDebugMode) { - debugPrint('Network error loading more posts: $e'); - } - if (mounted) { - setState(() { - _isLoadingMore = false; - _loadMoreError = 'Please check your internet connection'; - }); - } - } on ApiException catch (e) { - if (kDebugMode) { - debugPrint('API error loading more posts: $e'); - } - if (mounted) { - setState(() { - _isLoadingMore = false; - _loadMoreError = e.message; - }); - } - } on Exception catch (e) { + } catch (e) { if (kDebugMode) { debugPrint('Error loading more posts: $e'); } if (mounted) { setState(() { _isLoadingMore = false; - _loadMoreError = ErrorMessages.getUserFriendly(e.toString()); + _loadMoreError = ErrorMessage.loadFeed(e); }); } } @@ -409,6 +321,9 @@ class _CommunityFeedScreenState extends State { title: 'Community not found', message: _communityError!, onRetry: _loadCommunity, + secondaryActionLabel: _communityIsAuthError ? 'Sign In' : null, + onSecondaryAction: + _communityIsAuthError ? () => context.push('/login') : null, ), ); } @@ -713,7 +628,6 @@ class _CommunityFeedScreenState extends State { borderRadius: BorderRadius.circular(14), child: InkWell( onTap: () async { - final wasSubscribed = isSubscribed; try { await provider.toggleSubscription( communityDid: _community!.did, @@ -724,12 +638,9 @@ class _CommunityFeedScreenState extends State { debugPrint('Error toggling subscription: $e'); } if (mounted) { - final action = wasSubscribed ? 'leave' : 'join'; ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text( - 'Failed to $action community. Please try again.', - ), + content: Text(ErrorMessage.subscription(e)), behavior: SnackBarBehavior.floating, backgroundColor: AppColors.primary, ), diff --git a/lib/screens/home/post_detail_screen.dart b/lib/screens/home/post_detail_screen.dart index 6740e4f..b227e52 100644 --- a/lib/screens/home/post_detail_screen.dart +++ b/lib/screens/home/post_detail_screen.dart @@ -539,7 +539,7 @@ class _PostDetailScreenState extends State { if (mounted) { messenger.showSnackBar( SnackBar( - content: Text('Failed to vote: $e'), + content: Text(ErrorMessage.vote(e)), behavior: SnackBarBehavior.floating, ), ); @@ -619,7 +619,7 @@ class _PostDetailScreenState extends State { if (mounted) { messenger.showSnackBar( SnackBar( - content: Text('Failed to post comment: $e'), + content: Text(ErrorMessage.comment(e)), behavior: SnackBarBehavior.floating, backgroundColor: AppColors.primary, ), @@ -657,7 +657,7 @@ class _PostDetailScreenState extends State { if (mounted) { messenger.showSnackBar( SnackBar( - content: Text('Failed to post reply: $e'), + content: Text(ErrorMessage.comment(e)), behavior: SnackBarBehavior.floating, backgroundColor: AppColors.primary, ), @@ -731,7 +731,7 @@ class _PostDetailScreenState extends State { if (error != null && comments.isEmpty) { return FullScreenError( title: 'Failed to load comments', - message: ErrorMessages.getUserFriendly(error), + message: getErrorMessage(error), onRetry: commentsProvider.retry, ); } @@ -850,7 +850,7 @@ class _PostDetailScreenState extends State { } if (error != null) { return InlineError( - message: ErrorMessages.getUserFriendly(error), + message: getErrorMessage(error), onRetry: () { commentsProvider ..clearError() diff --git a/lib/utils/error_messages.dart b/lib/utils/error_messages.dart index 60d867f..8488ce9 100644 --- a/lib/utils/error_messages.dart +++ b/lib/utils/error_messages.dart @@ -1,28 +1,155 @@ -/// Utility class for transforming technical error messages into -/// user-friendly ones -class ErrorMessages { - /// Transform technical error messages into user-friendly ones - static String getUserFriendly(String error) { - final lowerError = error.toLowerCase(); - - if (lowerError.contains('socketexception') || - lowerError.contains('network') || - lowerError.contains('connection refused')) { - return 'Please check your internet connection'; - } else if (lowerError.contains('timeoutexception') || - lowerError.contains('timeout')) { - return 'Request timed out. Please try again'; - } else if (lowerError.contains('401') || - lowerError.contains('unauthorized')) { - return 'Authentication failed. Please sign in again'; - } else if (lowerError.contains('404') || lowerError.contains('not found')) { - return 'Content not found'; - } else if (lowerError.contains('500') || - lowerError.contains('internal server')) { - return 'Server error. Please try again later'; - } - - // Fallback to generic message for unknown errors - return 'Something went wrong. Please try again'; +/// User-Friendly Error Messages +/// +/// Centralized utility for converting API exceptions to user-friendly messages. +/// Uses type-based matching against the ApiException hierarchy for reliable +/// error handling across the app. +library; + +import '../services/api_exceptions.dart'; + +/// Converts an exception to a user-friendly error message. +/// +/// Uses type-based matching for reliability: +/// - [AuthenticationException] → Sign-in prompt +/// - [NetworkException] → Connection issues +/// - [ServerException] → Server problems +/// - [NotFoundException] → Resource not found +/// - [ValidationException] → Shows the validation message +/// - [FederationException] → atProto federation issues +/// - Other [ApiException] → Uses fallback +/// - Unknown exceptions → Uses fallback +/// +/// Prefer using [ErrorMessage] context-specific methods when available: +/// ```dart +/// try { +/// await voteProvider.toggleVote(...); +/// } on Exception catch (e) { +/// if (mounted) { +/// messenger.showSnackBar( +/// SnackBar(content: Text(ErrorMessage.vote(e))), +/// ); +/// } +/// } +/// ``` +String getErrorMessage(Object error, {String? fallback}) { + final defaultFallback = fallback ?? 'Something went wrong. Please try again.'; + + if (error is AuthenticationException) { + return 'Please sign in to continue.'; + } + + if (error is NetworkException) { + return 'No connection. Please check your internet.'; + } + + if (error is ServerException) { + return 'Server error. Please try again later.'; + } + + if (error is NotFoundException) { + return 'Content not found. It may have been removed.'; + } + + if (error is ValidationException) { + // Validation errors have user-facing messages + return error.message; + } + + if (error is FederationException) { + return 'Could not reach the server. Please try again.'; + } + + if (error is ApiException) { + return defaultFallback; } + + return defaultFallback; +} + +/// Context-specific error messages for common operations. +/// +/// Provides operation-appropriate fallback messages when the exception +/// type doesn't give enough context. +abstract final class ErrorMessage { + /// Error message for vote operations + static String vote(Object error) => getErrorMessage( + error, + fallback: 'Could not update your vote. Please try again.', + ); + + /// Error message for comment operations + static String comment(Object error) => getErrorMessage( + error, + fallback: 'Could not post comment. Please try again.', + ); + + /// Error message for post creation + static String createPost(Object error) => getErrorMessage( + error, + fallback: 'Could not create post. Please try again.', + ); + + /// Error message for post deletion + static String deletePost(Object error) => getErrorMessage( + error, + fallback: 'Could not delete post. Please try again.', + ); + + /// Error message for feed loading + static String loadFeed(Object error) => getErrorMessage( + error, + fallback: 'Could not load feed. Pull to refresh.', + ); + + /// Error message for profile operations + static String profile(Object error) => getErrorMessage( + error, + fallback: 'Could not load profile. Please try again.', + ); + + /// Error message for subscription operations (join/leave community) + static String subscription(Object error) => getErrorMessage( + error, + fallback: 'Could not update subscription. Please try again.', + ); + + /// Error message for report operations + static String report(Object error) => getErrorMessage( + error, + fallback: 'Could not submit report. Please try again.', + ); + + /// Error message for save/bookmark operations + static String save(Object error) => getErrorMessage( + error, + fallback: 'Could not save. Please try again.', + ); + + /// Error message for community operations + static String community(Object error) => getErrorMessage( + error, + fallback: 'Could not load community. Please try again.', + ); } + +/// Returns true if the error is an authentication error. +/// +/// Use this to check if the error requires a sign-in action: +/// ```dart +/// } on Exception catch (e) { +/// if (mounted) { +/// ScaffoldMessenger.of(context).showSnackBar( +/// SnackBar( +/// content: Text(ErrorMessage.vote(e)), +/// action: isAuthError(e) +/// ? SnackBarAction( +/// label: 'Sign In', +/// onPressed: () => context.push('/login'), +/// ) +/// : null, +/// ), +/// ); +/// } +/// } +/// ``` +bool isAuthError(Object error) => error is AuthenticationException; diff --git a/lib/widgets/comment_card.dart b/lib/widgets/comment_card.dart index 6ced0c5..fd33acc 100644 --- a/lib/widgets/comment_card.dart +++ b/lib/widgets/comment_card.dart @@ -11,6 +11,7 @@ import '../models/post.dart'; import '../providers/auth_provider.dart'; import '../providers/vote_provider.dart'; import '../services/api_exceptions.dart'; +import '../utils/error_messages.dart'; import '../utils/date_time_utils.dart'; import 'icons/animated_heart_icon.dart'; import 'report_dialog.dart'; @@ -604,9 +605,9 @@ class _CommentCardState extends State { } if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Failed to vote. Please try again.'), - duration: Duration(seconds: 2), + SnackBar( + content: Text(ErrorMessage.vote(e)), + duration: const Duration(seconds: 2), ), ); } diff --git a/lib/widgets/post_card_actions.dart b/lib/widgets/post_card_actions.dart index 29351bd..42c4568 100644 --- a/lib/widgets/post_card_actions.dart +++ b/lib/widgets/post_card_actions.dart @@ -11,6 +11,7 @@ import '../providers/community_subscription_provider.dart'; import '../providers/vote_provider.dart'; import '../services/api_exceptions.dart'; import '../services/coves_api_service.dart'; +import '../utils/error_messages.dart'; import '../utils/date_time_utils.dart'; import 'icons/animated_heart_icon.dart'; import 'report_dialog.dart'; @@ -512,10 +513,8 @@ class _PostCardActionsState extends State { } if (context.mounted) { messenger.showSnackBar( - const SnackBar( - content: Text( - 'Could not update your vote. Please try again.', - ), + SnackBar( + content: Text(ErrorMessage.vote(e)), behavior: SnackBarBehavior.floating, ), );