fix(stability): remove auth/context force unwraps and add guarded null-safe paths
This commit is contained in:
parent
f41708c662
commit
947992e8b9
5 changed files with 60 additions and 15 deletions
|
|
@ -68,8 +68,12 @@ void showGlobalSnackBar(String message, {bool isError = false}) {
|
||||||
|
|
||||||
/// Show a dialog safely through the global navigator key.
|
/// Show a dialog safely through the global navigator key.
|
||||||
Future<T?> showGlobalDialog<T>({required WidgetBuilder builder}) {
|
Future<T?> showGlobalDialog<T>({required WidgetBuilder builder}) {
|
||||||
|
final context = navigatorKey.currentContext;
|
||||||
|
if (context == null) {
|
||||||
|
return Future<T?>.value(null);
|
||||||
|
}
|
||||||
return showDialog<T>(
|
return showDialog<T>(
|
||||||
context: navigatorKey.currentContext!,
|
context: context,
|
||||||
builder: builder,
|
builder: builder,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -201,8 +205,14 @@ class _AuthGateState extends State<AuthGate> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _showResetPasswordDialog() async {
|
Future<void> _showResetPasswordDialog() async {
|
||||||
|
final context = navigatorKey.currentContext;
|
||||||
|
if (context == null) {
|
||||||
|
_isInPasswordRecoveryFlow = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await showDialog<void>(
|
await showDialog<void>(
|
||||||
context: navigatorKey.currentContext!,
|
context: context,
|
||||||
barrierDismissible: false,
|
barrierDismissible: false,
|
||||||
builder: (_) => const _ResetPasswordDialog(),
|
builder: (_) => const _ResetPasswordDialog(),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -483,6 +483,11 @@ class GarageScreenState extends State<GarageScreen> {
|
||||||
.update({'collection_id': targetId})
|
.update({'collection_id': targetId})
|
||||||
.inFilter('id', _selectedIds.toList());
|
.inFilter('id', _selectedIds.toList());
|
||||||
} else {
|
} else {
|
||||||
|
final userId = supabase.auth.currentUser?.id;
|
||||||
|
if (userId == null) {
|
||||||
|
throw Exception('You must be signed in to copy cars.');
|
||||||
|
}
|
||||||
|
|
||||||
final sourceCars = _cars
|
final sourceCars = _cars
|
||||||
.where((car) => _selectedIds.contains(car['id'] as int))
|
.where((car) => _selectedIds.contains(car['id'] as int))
|
||||||
.toList(growable: false);
|
.toList(growable: false);
|
||||||
|
|
@ -492,7 +497,7 @@ class GarageScreenState extends State<GarageScreen> {
|
||||||
final imagePath = car['user_image_url'] as String?;
|
final imagePath = car['user_image_url'] as String?;
|
||||||
return <String, dynamic>{
|
return <String, dynamic>{
|
||||||
'hw_id': car['hw_id'] as String,
|
'hw_id': car['hw_id'] as String,
|
||||||
'user_id': supabase.auth.currentUser!.id,
|
'user_id': userId,
|
||||||
'collection_id': targetId,
|
'collection_id': targetId,
|
||||||
if (notes != null && notes.trim().isNotEmpty) 'notes': notes,
|
if (notes != null && notes.trim().isNotEmpty) 'notes': notes,
|
||||||
if (imagePath != null && imagePath.isNotEmpty)
|
if (imagePath != null && imagePath.isNotEmpty)
|
||||||
|
|
@ -1028,11 +1033,16 @@ class GarageScreenState extends State<GarageScreen> {
|
||||||
.update({'collection_id': targetId})
|
.update({'collection_id': targetId})
|
||||||
.eq('id', car['id']);
|
.eq('id', car['id']);
|
||||||
} else {
|
} else {
|
||||||
|
final userId = supabase.auth.currentUser?.id;
|
||||||
|
if (userId == null) {
|
||||||
|
throw Exception('You must be signed in to copy cars.');
|
||||||
|
}
|
||||||
|
|
||||||
final notes = car['notes'] as String?;
|
final notes = car['notes'] as String?;
|
||||||
final imagePath = car['user_image_url'] as String?;
|
final imagePath = car['user_image_url'] as String?;
|
||||||
await supabase.from('hotwheels').insert({
|
await supabase.from('hotwheels').insert({
|
||||||
'hw_id': car['hw_id'] as String,
|
'hw_id': car['hw_id'] as String,
|
||||||
'user_id': supabase.auth.currentUser!.id,
|
'user_id': userId,
|
||||||
'collection_id': targetId,
|
'collection_id': targetId,
|
||||||
if (notes != null && notes.trim().isNotEmpty) 'notes': notes,
|
if (notes != null && notes.trim().isNotEmpty) 'notes': notes,
|
||||||
if (imagePath != null && imagePath.isNotEmpty)
|
if (imagePath != null && imagePath.isNotEmpty)
|
||||||
|
|
|
||||||
|
|
@ -419,9 +419,14 @@ class ScanTabState extends State<ScanTab> {
|
||||||
String hwId, {
|
String hwId, {
|
||||||
String? notes,
|
String? notes,
|
||||||
}) async {
|
}) async {
|
||||||
|
final userId = supabase.auth.currentUser?.id;
|
||||||
|
if (userId == null) {
|
||||||
|
throw Exception('You must be signed in to add cars.');
|
||||||
|
}
|
||||||
|
|
||||||
await supabase.from('hotwheels').insert({
|
await supabase.from('hotwheels').insert({
|
||||||
'hw_id': hwId,
|
'hw_id': hwId,
|
||||||
'user_id': supabase.auth.currentUser!.id,
|
'user_id': userId,
|
||||||
'collection_id': collectionId,
|
'collection_id': collectionId,
|
||||||
if (notes != null && notes.trim().isNotEmpty) 'notes': notes.trim(),
|
if (notes != null && notes.trim().isNotEmpty) 'notes': notes.trim(),
|
||||||
});
|
});
|
||||||
|
|
@ -433,6 +438,11 @@ class ScanTabState extends State<ScanTab> {
|
||||||
String? series,
|
String? series,
|
||||||
int? year,
|
int? year,
|
||||||
}) async {
|
}) async {
|
||||||
|
final userId = supabase.auth.currentUser?.id;
|
||||||
|
if (userId == null) {
|
||||||
|
throw Exception('You must be signed in to create catalog entries.');
|
||||||
|
}
|
||||||
|
|
||||||
final cleanedSeries = series?.trim();
|
final cleanedSeries = series?.trim();
|
||||||
final payload = <String, dynamic>{
|
final payload = <String, dynamic>{
|
||||||
'hw_id': hwId,
|
'hw_id': hwId,
|
||||||
|
|
@ -445,12 +455,15 @@ class ScanTabState extends State<ScanTab> {
|
||||||
|
|
||||||
await supabase.from('car_votes').insert({
|
await supabase.from('car_votes').insert({
|
||||||
'hw_id': hwId,
|
'hw_id': hwId,
|
||||||
'user_id': supabase.auth.currentUser!.id,
|
'user_id': userId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _ensureValidationVote(String hwId) async {
|
Future<void> _ensureValidationVote(String hwId) async {
|
||||||
final userId = supabase.auth.currentUser!.id;
|
final userId = supabase.auth.currentUser?.id;
|
||||||
|
if (userId == null) {
|
||||||
|
throw Exception('You must be signed in to validate entries.');
|
||||||
|
}
|
||||||
final existingVote = await supabase
|
final existingVote = await supabase
|
||||||
.from('car_votes')
|
.from('car_votes')
|
||||||
.select('id')
|
.select('id')
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,14 @@ class CollectionMember {
|
||||||
class CollectionService {
|
class CollectionService {
|
||||||
CollectionService._();
|
CollectionService._();
|
||||||
|
|
||||||
|
static String _requireUserId() {
|
||||||
|
final userId = supabase.auth.currentUser?.id;
|
||||||
|
if (userId == null) {
|
||||||
|
throw Exception('You must be signed in to perform this action.');
|
||||||
|
}
|
||||||
|
return userId;
|
||||||
|
}
|
||||||
|
|
||||||
/// Ensures the current user has at least one collection membership.
|
/// Ensures the current user has at least one collection membership.
|
||||||
/// Creates a default collection on first login.
|
/// Creates a default collection on first login.
|
||||||
static Future<String?> ensureDefaultCollection() async {
|
static Future<String?> ensureDefaultCollection() async {
|
||||||
|
|
@ -80,7 +88,7 @@ class CollectionService {
|
||||||
/// Fetch all collections the current user is a member of,
|
/// Fetch all collections the current user is a member of,
|
||||||
/// including item count and member count.
|
/// including item count and member count.
|
||||||
static Future<List<Collection>> getMyCollections() async {
|
static Future<List<Collection>> getMyCollections() async {
|
||||||
final userId = supabase.auth.currentUser!.id;
|
final userId = _requireUserId();
|
||||||
|
|
||||||
// Get memberships with collection data.
|
// Get memberships with collection data.
|
||||||
final memberships = await supabase
|
final memberships = await supabase
|
||||||
|
|
@ -249,7 +257,7 @@ class CollectionService {
|
||||||
required String name,
|
required String name,
|
||||||
String? description,
|
String? description,
|
||||||
}) async {
|
}) async {
|
||||||
final userId = supabase.auth.currentUser!.id;
|
final userId = _requireUserId();
|
||||||
|
|
||||||
final row = await supabase
|
final row = await supabase
|
||||||
.from('collections')
|
.from('collections')
|
||||||
|
|
@ -287,7 +295,7 @@ class CollectionService {
|
||||||
required String name,
|
required String name,
|
||||||
String? description,
|
String? description,
|
||||||
}) async {
|
}) async {
|
||||||
final userId = supabase.auth.currentUser!.id;
|
final userId = _requireUserId();
|
||||||
|
|
||||||
final collection = await supabase
|
final collection = await supabase
|
||||||
.from('collections')
|
.from('collections')
|
||||||
|
|
@ -311,7 +319,7 @@ class CollectionService {
|
||||||
|
|
||||||
/// Delete a collection. Owner only. Cascade deletes members & items.
|
/// Delete a collection. Owner only. Cascade deletes members & items.
|
||||||
static Future<void> delete(String collectionId) async {
|
static Future<void> delete(String collectionId) async {
|
||||||
final userId = supabase.auth.currentUser!.id;
|
final userId = _requireUserId();
|
||||||
|
|
||||||
final collection = await supabase
|
final collection = await supabase
|
||||||
.from('collections')
|
.from('collections')
|
||||||
|
|
@ -362,7 +370,7 @@ class CollectionService {
|
||||||
throw Exception('Unsupported role "$role".');
|
throw Exception('Unsupported role "$role".');
|
||||||
}
|
}
|
||||||
|
|
||||||
final currentUserId = supabase.auth.currentUser!.id;
|
final currentUserId = _requireUserId();
|
||||||
|
|
||||||
final collection = await supabase
|
final collection = await supabase
|
||||||
.from('collections')
|
.from('collections')
|
||||||
|
|
@ -428,7 +436,7 @@ class CollectionService {
|
||||||
required String collectionId,
|
required String collectionId,
|
||||||
required String memberUserId,
|
required String memberUserId,
|
||||||
}) async {
|
}) async {
|
||||||
final currentUserId = supabase.auth.currentUser!.id;
|
final currentUserId = _requireUserId();
|
||||||
|
|
||||||
final collection = await supabase
|
final collection = await supabase
|
||||||
.from('collections')
|
.from('collections')
|
||||||
|
|
@ -501,7 +509,7 @@ class CollectionService {
|
||||||
|
|
||||||
/// Leave a collection (for non-owners).
|
/// Leave a collection (for non-owners).
|
||||||
static Future<void> leave(String collectionId) async {
|
static Future<void> leave(String collectionId) async {
|
||||||
final userId = supabase.auth.currentUser!.id;
|
final userId = _requireUserId();
|
||||||
|
|
||||||
final membership = await supabase
|
final membership = await supabase
|
||||||
.from('collection_members')
|
.from('collection_members')
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,11 @@ class StorageService {
|
||||||
await deleteCarImage(oldPath);
|
await deleteCarImage(oldPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
final userId = supabase.auth.currentUser!.id;
|
final user = supabase.auth.currentUser;
|
||||||
|
if (user == null) {
|
||||||
|
throw Exception('You must be signed in to upload images.');
|
||||||
|
}
|
||||||
|
final userId = user.id;
|
||||||
final path = '$userId/$entryId.jpg';
|
final path = '$userId/$entryId.jpg';
|
||||||
final compressed = await _compressImage(file);
|
final compressed = await _compressImage(file);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue