diff --git a/.github/workflows/test-and-build.yml b/.github/workflows/test-and-build.yml
index 89f172e..2d3a6df 100644
--- a/.github/workflows/test-and-build.yml
+++ b/.github/workflows/test-and-build.yml
@@ -17,22 +17,23 @@ jobs:
contents: read
checks: write
pull-requests: write
+ security-events: write
steps:
- name: Checkout Code
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+ uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.2.2
- name: Setup Java
- uses: actions/setup-java@cd89f11832ad0855263829ad3623d1d31a929307 # v3.13.0
+ uses: actions/setup-java@c1e323688fd81a25caa38c78aa6df2d33d3e20d9 # v4.4.0
with:
distribution: 'zulu'
java-version: '17'
cache: 'gradle'
- name: Setup Flutter
- uses: subosito/flutter-action@44ae346459a5c099ba318147f2f297c0d74a0db2 # v2.12.0
+ uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2.16.0
with:
- flutter-version: '3.24.3'
+ flutter-version: '3.41.9'
channel: 'stable'
cache: true
@@ -49,14 +50,14 @@ jobs:
run: flutter test --coverage
- name: Upload coverage to Codecov
- uses: codecov/codecov-action@eaaf4bedf32dbdc6b720b63067d99c4d77d6047d # v3.1.4
+ uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4.6.0
with:
token: ${{ secrets.CODECOV_TOKEN }}
file: ./coverage/lcov.info
fail_ci_if_error: false
- name: Archive coverage reports
- uses: actions/upload-artifact@c7d193f32ed247398f1ef51e04acc37d996a730c # v4.1.0
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.4.3
with:
name: code-coverage
path: coverage/
@@ -71,19 +72,19 @@ jobs:
steps:
- name: Checkout Code
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+ uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.2.2
- name: Setup Java
- uses: actions/setup-java@cd89f11832ad0855263829ad3623d1d31a929307 # v3.13.0
+ uses: actions/setup-java@c1e323688fd81a25caa38c78aa6df2d33d3e20d9 # v4.4.0
with:
distribution: 'zulu'
java-version: '17'
cache: 'gradle'
- name: Setup Flutter
- uses: subosito/flutter-action@44ae346459a5c099ba318147f2f297c0d74a0db2 # v2.12.0
+ uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2.16.0
with:
- flutter-version: '3.24.3'
+ flutter-version: '3.41.9'
channel: 'stable'
cache: true
@@ -94,7 +95,7 @@ jobs:
run: flutter build apk --debug --dart-define=SUPABASE_URL=${{ secrets.SUPABASE_URL }} --dart-define=SUPABASE_ANON_KEY=${{ secrets.SUPABASE_ANON_KEY }}
- name: Upload APK
- uses: actions/upload-artifact@c7d193f32ed247398f1ef51e04acc37d996a730c # v4.1.0
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.4.3
with:
name: release-apk
path: build/app/outputs/flutter-apk/app-debug.apk
@@ -109,12 +110,12 @@ jobs:
steps:
- name: Checkout Code
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+ uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.2.2
- name: Setup Flutter
- uses: subosito/flutter-action@44ae346459a5c099ba318147f2f297c0d74a0db2 # v2.12.0
+ uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2.16.0
with:
- flutter-version: '3.24.3'
+ flutter-version: '3.41.9'
channel: 'stable'
cache: true
@@ -125,7 +126,7 @@ jobs:
run: flutter build ios --no-codesign --debug --dart-define=SUPABASE_URL=${{ secrets.SUPABASE_URL }} --dart-define=SUPABASE_ANON_KEY=${{ secrets.SUPABASE_ANON_KEY }}
- name: Upload Runner
- uses: actions/upload-artifact@c7d193f32ed247398f1ef51e04acc37d996a730c # v4.1.0
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.4.3
with:
name: ios-build
path: build/ios/iphoneos/Runner.app
@@ -139,12 +140,12 @@ jobs:
steps:
- name: Checkout Code
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+ uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.2.2
- name: Initialize CodeQL
- uses: github/codeql-action/init@ccf91b7d19760778f69e6b54a32057635678b871 # v3.25.3
+ uses: github/codeql-action/init@0daab03d71ff584ef619d027a3fd9146679c5d84 # v3.27.6
with:
languages: javascript, python
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@ccf91b7d19760778f69e6b54a32057635678b871 # v3.25.3
+ uses: github/codeql-action/analyze@0daab03d71ff584ef619d027a3fd9146679c5d84 # v3.27.6
diff --git a/.gitignore b/.gitignore
index 5f72c48..b8b3a43 100755
--- a/.gitignore
+++ b/.gitignore
@@ -52,3 +52,5 @@ dist/petsphere-e6d8abd0.ipa
/android/gradle.properties
/scratch
/.kiro
+.claude/settings.local.json
+
diff --git a/android/build/reports/problems/problems-report.html b/android/build/reports/problems/problems-report.html
index 7c50449..f281836 100644
--- a/android/build/reports/problems/problems-report.html
+++ b/android/build/reports/problems/problems-report.html
@@ -650,7 +650,7 @@
diff --git a/lib/controllers/connectivity_controller.dart b/lib/controllers/connectivity_controller.dart
index f2160ef..0b7d1da 100644
--- a/lib/controllers/connectivity_controller.dart
+++ b/lib/controllers/connectivity_controller.dart
@@ -13,25 +13,18 @@ final connectivityServiceProvider = Provider((ref) {
/// Stream of connectivity status changes (Broadcast stream for multiple listeners)
final connectivityStatusProvider = StreamProvider((ref) {
final service = ref.watch(connectivityServiceProvider);
- return service.statusStream.asBroadcastStream();
+ return service.statusStream;
});
/// Convenience: whether device is currently online
+/// Optimized using .select to only rebuild when the status changes to/from online
final isOnlineProvider = Provider((ref) {
- final stream = ref.watch(connectivityStatusProvider);
- return stream.whenData((status) => status == ConnectivityStatus.online)
- .maybeWhen(
- data: (isOnline) => isOnline,
- orElse: () => false,
- );
+ final status = ref.watch(connectivityStatusProvider).asData?.value ?? ConnectivityStatus.unknown;
+ return status == ConnectivityStatus.online;
});
/// Convenience: whether device is currently offline
final isOfflineProvider = Provider((ref) {
- final stream = ref.watch(connectivityStatusProvider);
- return stream.whenData((status) => status == ConnectivityStatus.offline)
- .maybeWhen(
- data: (isOffline) => isOffline,
- orElse: () => false,
- );
+ final status = ref.watch(connectivityStatusProvider).asData?.value ?? ConnectivityStatus.unknown;
+ return status == ConnectivityStatus.offline;
});
diff --git a/lib/models/post_model.dart b/lib/models/post_model.dart
index c651160..2f690d0 100755
--- a/lib/models/post_model.dart
+++ b/lib/models/post_model.dart
@@ -36,6 +36,10 @@ class CommentModel {
'pet_id': petId,
'text': text,
'created_at': createdAt.toIso8601String(),
+ 'pets': {
+ 'name': petName,
+ 'profile_image_url': petProfileImageUrl,
+ },
};
}
diff --git a/lib/repositories/auth_repository.dart b/lib/repositories/auth_repository.dart
index 9dbbaf5..94b4509 100644
--- a/lib/repositories/auth_repository.dart
+++ b/lib/repositories/auth_repository.dart
@@ -1,5 +1,7 @@
import 'dart:io';
+import 'dart:developer';
import 'package:supabase_flutter/supabase_flutter.dart';
+
import '../models/user_model.dart';
import '../utils/supabase_config.dart';
@@ -41,11 +43,15 @@ class AuthRepository {
'name': name,
});
} catch (e) {
- // Clean up by signing out the user since profile creation failed.
- // NOTE: Ideally, we would delete the auth user here to allow re-signup with the same email.
- // However, deleting a user requires admin privileges (Service Role key), which should
- // NOT be embedded in the client app.
- // TODO: Implement a Supabase Edge Function 'delete-self' or similar to handle this rollback.
+ // Clean up by deleting the auth user and signing out since profile creation failed.
+ // This allows the user to try again with the same email.
+ try {
+ await supabase.functions.invoke('delete-self');
+ } catch (rollbackError) {
+ // Log rollback error but proceed with throwing the original error
+ log('Rollback failed', error: rollbackError);
+ }
+
await supabase.auth.signOut();
throw Exception(
'Failed to create your profile. Please try signing up again. If the problem persists, contact support.');
diff --git a/lib/repositories/offline_feed_repository.dart b/lib/repositories/offline_feed_repository.dart
index aaf4218..3f733bc 100644
--- a/lib/repositories/offline_feed_repository.dart
+++ b/lib/repositories/offline_feed_repository.dart
@@ -35,7 +35,7 @@ class OfflineFeedRepository {
if (_connectivity.isOffline) {
final cached = _cache.getCachedFeedPosts();
if (cached != null && cached.isNotEmpty) {
- return cached.map((json) => PostModel.fromJson(json)).toList();
+ return cached.map((json) => PostModel.fromJson(json as Map)).toList();
}
// If offline and no cache, throw error
throw Exception('No cached posts available and device is offline');
@@ -45,21 +45,21 @@ class OfflineFeedRepository {
if (_cache.isFeedPostsFresh(_postsCacheTTL)) {
final cached = _cache.getCachedFeedPosts();
if (cached != null) {
- return cached.map((json) => PostModel.fromJson(json)).toList();
+ return cached.map((json) => PostModel.fromJson(json as Map)).toList();
}
}
// Cache is stale or missing, fetch from network
try {
final posts = await _feedRepository.fetchPosts();
- // Note: We cache the model objects; they'll be JSON-serialized by saveJson
- await _cache.cacheFeedPosts(posts);
+ // Convert models to JSON maps for persistence
+ await _cache.cacheFeedPosts(posts.map((p) => p.toJson()).toList());
return posts;
} catch (e) {
// Network error - try returning cached data if available
final cached = _cache.getCachedFeedPosts();
if (cached != null && cached.isNotEmpty) {
- return cached.map((json) => PostModel.fromJson(json)).toList();
+ return cached.map((json) => PostModel.fromJson(json as Map)).toList();
}
rethrow;
}
@@ -76,7 +76,7 @@ class OfflineFeedRepository {
if (cached != null) {
for (final json in cached) {
if (json['id'] == postId) {
- return PostModel.fromJson(json);
+ return PostModel.fromJson(json as Map);
}
}
}
@@ -91,7 +91,7 @@ class OfflineFeedRepository {
if (cached != null) {
for (final json in cached) {
if (json['id'] == postId) {
- return PostModel.fromJson(json);
+ return PostModel.fromJson(json as Map);
}
}
}
diff --git a/lib/repositories/offline_marketplace_repository.dart b/lib/repositories/offline_marketplace_repository.dart
index f09c0ef..25cc57b 100644
--- a/lib/repositories/offline_marketplace_repository.dart
+++ b/lib/repositories/offline_marketplace_repository.dart
@@ -35,7 +35,7 @@ class OfflineMarketplaceRepository {
if (_connectivity.isOffline) {
final cached = _cache.getCachedProducts();
if (cached != null && cached.isNotEmpty) {
- return cached.map((json) => ProductModel.fromJson(json)).toList();
+ return cached.map((json) => ProductModel.fromJson(json as Map)).toList();
}
throw Exception('No cached products available and device is offline');
}
@@ -44,7 +44,7 @@ class OfflineMarketplaceRepository {
if (_cache.isProductsFresh(_productsCacheTTL)) {
final cached = _cache.getCachedProducts();
if (cached != null) {
- return cached.map((json) => ProductModel.fromJson(json)).toList();
+ return cached.map((json) => ProductModel.fromJson(json as Map)).toList();
}
}
@@ -62,7 +62,7 @@ class OfflineMarketplaceRepository {
// Network error - try cache as fallback
final cached = _cache.getCachedProducts();
if (cached != null && cached.isNotEmpty) {
- return cached.map((json) => ProductModel.fromJson(json)).toList();
+ return cached.map((json) => ProductModel.fromJson(json as Map)).toList();
}
rethrow;
}
@@ -79,7 +79,7 @@ class OfflineMarketplaceRepository {
if (cached != null) {
for (final json in cached) {
if (json['id'] == id) {
- return ProductModel.fromJson(json);
+ return ProductModel.fromJson(json as Map);
}
}
}
@@ -94,7 +94,7 @@ class OfflineMarketplaceRepository {
if (cached != null) {
for (final json in cached) {
if (json['id'] == id) {
- return ProductModel.fromJson(json);
+ return ProductModel.fromJson(json as Map);
}
}
}
diff --git a/lib/utils/health_improvements.dart b/lib/utils/health_improvements.dart
index bc95fe2..36cffbf 100644
--- a/lib/utils/health_improvements.dart
+++ b/lib/utils/health_improvements.dart
@@ -118,13 +118,15 @@ bool areMedicationDosesLow(
return coveredDays.length < daysThreshold;
}
-/// Get overdue medication doses
+/// Get overdue medication doses (past scheduled time + 1 hour grace period)
List getOverdueDoses(
- List doses,
-) {
+ List doses, {
+ Duration grace = const Duration(hours: 1),
+}) {
final now = DateTime.now();
return doses.where((d) {
- return d.givenAt == null && d.scheduledFor.isBefore(now);
+ // Only overdue if not given AND now is past (scheduled time + grace)
+ return d.givenAt == null && now.isAfter(d.scheduledFor.add(grace));
}).toList();
}
diff --git a/supabase/functions/delete-self/index.ts b/supabase/functions/delete-self/index.ts
new file mode 100644
index 0000000..8c63855
--- /dev/null
+++ b/supabase/functions/delete-self/index.ts
@@ -0,0 +1,30 @@
+import { serve } from "https://deno.land/std@0.168.0/http/server.ts"
+import { createClient } from "https://esm.sh/@supabase/supabase-js@2"
+
+serve(async (req) => {
+ const supabaseClient = createClient(
+ Deno.env.get('SUPABASE_URL') ?? '',
+ Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? '',
+ { auth: { persistSession: false } }
+ )
+
+ // Get the JWT from the request
+ const authHeader = req.headers.get('Authorization')
+ if (!authHeader) {
+ return new Response(JSON.stringify({ error: 'No authorization header' }), { status: 401 })
+ }
+
+ // Get the user from the JWT
+ const { data: { user }, error: authError } = await supabaseClient.auth.getUser(authHeader.replace('Bearer ', ''))
+ if (authError || !user) {
+ return new Response(JSON.stringify({ error: 'Invalid token' }), { status: 401 })
+ }
+
+ // Delete the user using service role
+ const { error: deleteError } = await supabaseClient.auth.admin.deleteUser(user.id)
+ if (deleteError) {
+ return new Response(JSON.stringify({ error: deleteError.message }), { status: 500 })
+ }
+
+ return new Response(JSON.stringify({ message: 'User deleted successfully' }), { status: 200 })
+})
diff --git a/web/flutter_bootstrap.js b/web/flutter_bootstrap.js.bak
similarity index 100%
rename from web/flutter_bootstrap.js
rename to web/flutter_bootstrap.js.bak