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,
),
);