Skip to content

Commit 35315de

Browse files
committed
Add delete-self; fix offline cache & connectivity
Introduce a Supabase Edge Function (supabase/functions/delete-self) and call it from AuthRepository to perform a signup rollback (delete auth user) when profile creation fails; log rollback errors and still sign out. Fix offline repositories to use proper Map<String, dynamic> casts when reading cached JSON and to persist JSON maps (via toJson) when caching models. Simplify connectivity providers by returning the original status stream and using the provider's asData value for isOnline/isOffline to avoid extra stream wrapping. Add a 1-hour grace parameter (configurable) to getOverdueDoses in health_improvements. Also add .claude/settings.local.json to .gitignore.
1 parent 5ce26e1 commit 35315de

7 files changed

Lines changed: 67 additions & 34 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,5 @@ dist/petsphere-e6d8abd0.ipa
5252
/android/gradle.properties
5353
/scratch
5454
/.kiro
55+
.claude/settings.local.json
56+

lib/controllers/connectivity_controller.dart

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,25 +13,18 @@ final connectivityServiceProvider = Provider<ConnectivityService>((ref) {
1313
/// Stream of connectivity status changes (Broadcast stream for multiple listeners)
1414
final connectivityStatusProvider = StreamProvider<ConnectivityStatus>((ref) {
1515
final service = ref.watch(connectivityServiceProvider);
16-
return service.statusStream.asBroadcastStream();
16+
return service.statusStream;
1717
});
1818

1919
/// Convenience: whether device is currently online
20+
/// Optimized using .select to only rebuild when the status changes to/from online
2021
final isOnlineProvider = Provider<bool>((ref) {
21-
final stream = ref.watch(connectivityStatusProvider);
22-
return stream.whenData((status) => status == ConnectivityStatus.online)
23-
.maybeWhen(
24-
data: (isOnline) => isOnline,
25-
orElse: () => false,
26-
);
22+
final status = ref.watch(connectivityStatusProvider).asData?.value ?? ConnectivityStatus.unknown;
23+
return status == ConnectivityStatus.online;
2724
});
2825

2926
/// Convenience: whether device is currently offline
3027
final isOfflineProvider = Provider<bool>((ref) {
31-
final stream = ref.watch(connectivityStatusProvider);
32-
return stream.whenData((status) => status == ConnectivityStatus.offline)
33-
.maybeWhen(
34-
data: (isOffline) => isOffline,
35-
orElse: () => false,
36-
);
28+
final status = ref.watch(connectivityStatusProvider).asData?.value ?? ConnectivityStatus.unknown;
29+
return status == ConnectivityStatus.offline;
3730
});

lib/repositories/auth_repository.dart

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import 'dart:io';
2+
import 'dart:developer';
23
import 'package:supabase_flutter/supabase_flutter.dart';
4+
35
import '../models/user_model.dart';
46
import '../utils/supabase_config.dart';
57

@@ -41,11 +43,15 @@ class AuthRepository {
4143
'name': name,
4244
});
4345
} catch (e) {
44-
// Clean up by signing out the user since profile creation failed.
45-
// NOTE: Ideally, we would delete the auth user here to allow re-signup with the same email.
46-
// However, deleting a user requires admin privileges (Service Role key), which should
47-
// NOT be embedded in the client app.
48-
// TODO: Implement a Supabase Edge Function 'delete-self' or similar to handle this rollback.
46+
// Clean up by deleting the auth user and signing out since profile creation failed.
47+
// This allows the user to try again with the same email.
48+
try {
49+
await supabase.functions.invoke('delete-self');
50+
} catch (rollbackError) {
51+
// Log rollback error but proceed with throwing the original error
52+
log('Rollback failed', error: rollbackError);
53+
}
54+
4955
await supabase.auth.signOut();
5056
throw Exception(
5157
'Failed to create your profile. Please try signing up again. If the problem persists, contact support.');

lib/repositories/offline_feed_repository.dart

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ class OfflineFeedRepository {
3535
if (_connectivity.isOffline) {
3636
final cached = _cache.getCachedFeedPosts();
3737
if (cached != null && cached.isNotEmpty) {
38-
return cached.map((json) => PostModel.fromJson(json)).toList();
38+
return cached.map((json) => PostModel.fromJson(json as Map<String, dynamic>)).toList();
3939
}
4040
// If offline and no cache, throw error
4141
throw Exception('No cached posts available and device is offline');
@@ -45,21 +45,21 @@ class OfflineFeedRepository {
4545
if (_cache.isFeedPostsFresh(_postsCacheTTL)) {
4646
final cached = _cache.getCachedFeedPosts();
4747
if (cached != null) {
48-
return cached.map((json) => PostModel.fromJson(json)).toList();
48+
return cached.map((json) => PostModel.fromJson(json as Map<String, dynamic>)).toList();
4949
}
5050
}
5151

5252
// Cache is stale or missing, fetch from network
5353
try {
5454
final posts = await _feedRepository.fetchPosts();
55-
// Note: We cache the model objects; they'll be JSON-serialized by saveJson
56-
await _cache.cacheFeedPosts(posts);
55+
// Convert models to JSON maps for persistence
56+
await _cache.cacheFeedPosts(posts.map((p) => p.toJson()).toList());
5757
return posts;
5858
} catch (e) {
5959
// Network error - try returning cached data if available
6060
final cached = _cache.getCachedFeedPosts();
6161
if (cached != null && cached.isNotEmpty) {
62-
return cached.map((json) => PostModel.fromJson(json)).toList();
62+
return cached.map((json) => PostModel.fromJson(json as Map<String, dynamic>)).toList();
6363
}
6464
rethrow;
6565
}
@@ -76,7 +76,7 @@ class OfflineFeedRepository {
7676
if (cached != null) {
7777
for (final json in cached) {
7878
if (json['id'] == postId) {
79-
return PostModel.fromJson(json);
79+
return PostModel.fromJson(json as Map<String, dynamic>);
8080
}
8181
}
8282
}
@@ -91,7 +91,7 @@ class OfflineFeedRepository {
9191
if (cached != null) {
9292
for (final json in cached) {
9393
if (json['id'] == postId) {
94-
return PostModel.fromJson(json);
94+
return PostModel.fromJson(json as Map<String, dynamic>);
9595
}
9696
}
9797
}

lib/repositories/offline_marketplace_repository.dart

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ class OfflineMarketplaceRepository {
3535
if (_connectivity.isOffline) {
3636
final cached = _cache.getCachedProducts();
3737
if (cached != null && cached.isNotEmpty) {
38-
return cached.map((json) => ProductModel.fromJson(json)).toList();
38+
return cached.map((json) => ProductModel.fromJson(json as Map<String, dynamic>)).toList();
3939
}
4040
throw Exception('No cached products available and device is offline');
4141
}
@@ -44,7 +44,7 @@ class OfflineMarketplaceRepository {
4444
if (_cache.isProductsFresh(_productsCacheTTL)) {
4545
final cached = _cache.getCachedProducts();
4646
if (cached != null) {
47-
return cached.map((json) => ProductModel.fromJson(json)).toList();
47+
return cached.map((json) => ProductModel.fromJson(json as Map<String, dynamic>)).toList();
4848
}
4949
}
5050

@@ -62,7 +62,7 @@ class OfflineMarketplaceRepository {
6262
// Network error - try cache as fallback
6363
final cached = _cache.getCachedProducts();
6464
if (cached != null && cached.isNotEmpty) {
65-
return cached.map((json) => ProductModel.fromJson(json)).toList();
65+
return cached.map((json) => ProductModel.fromJson(json as Map<String, dynamic>)).toList();
6666
}
6767
rethrow;
6868
}
@@ -79,7 +79,7 @@ class OfflineMarketplaceRepository {
7979
if (cached != null) {
8080
for (final json in cached) {
8181
if (json['id'] == id) {
82-
return ProductModel.fromJson(json);
82+
return ProductModel.fromJson(json as Map<String, dynamic>);
8383
}
8484
}
8585
}
@@ -94,7 +94,7 @@ class OfflineMarketplaceRepository {
9494
if (cached != null) {
9595
for (final json in cached) {
9696
if (json['id'] == id) {
97-
return ProductModel.fromJson(json);
97+
return ProductModel.fromJson(json as Map<String, dynamic>);
9898
}
9999
}
100100
}

lib/utils/health_improvements.dart

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -118,13 +118,15 @@ bool areMedicationDosesLow(
118118
return coveredDays.length < daysThreshold;
119119
}
120120

121-
/// Get overdue medication doses
121+
/// Get overdue medication doses (past scheduled time + 1 hour grace period)
122122
List<MedicationDose> getOverdueDoses(
123-
List<MedicationDose> doses,
124-
) {
123+
List<MedicationDose> doses, {
124+
Duration grace = const Duration(hours: 1),
125+
}) {
125126
final now = DateTime.now();
126127
return doses.where((d) {
127-
return d.givenAt == null && d.scheduledFor.isBefore(now);
128+
// Only overdue if not given AND now is past (scheduled time + grace)
129+
return d.givenAt == null && now.isAfter(d.scheduledFor.add(grace));
128130
}).toList();
129131
}
130132

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { serve } from "https://deno.land/std@0.168.0/http/server.ts"
2+
import { createClient } from "https://esm.sh/@supabase/supabase-js@2"
3+
4+
serve(async (req) => {
5+
const supabaseClient = createClient(
6+
Deno.env.get('SUPABASE_URL') ?? '',
7+
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? '',
8+
{ auth: { persistSession: false } }
9+
)
10+
11+
// Get the JWT from the request
12+
const authHeader = req.headers.get('Authorization')
13+
if (!authHeader) {
14+
return new Response(JSON.stringify({ error: 'No authorization header' }), { status: 401 })
15+
}
16+
17+
// Get the user from the JWT
18+
const { data: { user }, error: authError } = await supabaseClient.auth.getUser(authHeader.replace('Bearer ', ''))
19+
if (authError || !user) {
20+
return new Response(JSON.stringify({ error: 'Invalid token' }), { status: 401 })
21+
}
22+
23+
// Delete the user using service role
24+
const { error: deleteError } = await supabaseClient.auth.admin.deleteUser(user.id)
25+
if (deleteError) {
26+
return new Response(JSON.stringify({ error: deleteError.message }), { status: 500 })
27+
}
28+
29+
return new Response(JSON.stringify({ message: 'User deleted successfully' }), { status: 200 })
30+
})

0 commit comments

Comments
 (0)