From 35b9d94873c5d8e1c33414a8dba18cf785981254 Mon Sep 17 00:00:00 2001 From: Syed Salman Reza <71028588+syed-reza98@users.noreply.github.com> Date: Thu, 7 May 2026 13:58:44 +0600 Subject: [PATCH 1/4] Update GitHub Actions versions and permissions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Upgrade workflow actions and add security permission: grant permissions.security-events write and bump action versions across jobs — actions/checkout to v4.2.2, actions/setup-java to v4.4.0, subosito/flutter-action to v2.16.0 (Flutter 3.41.9), codecov/codecov-action to v4.6.0, actions/upload-artifact to v4.4.3, and github/codeql-action to v3.27.6. These updates keep CI tooling current and ensure compatibility/stability across test, Android/iOS build, and CodeQL analysis jobs. --- .github/workflows/test-and-build.yml | 37 ++++++++++++++-------------- 1 file changed, 19 insertions(+), 18 deletions(-) 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 From 5ce26e173eb98089ef2bd3688224ae7d76357a36 Mon Sep 17 00:00:00 2001 From: Syed Salman Reza <71028588+syed-reza98@users.noreply.github.com> Date: Thu, 7 May 2026 14:03:44 +0600 Subject: [PATCH 2/4] Include pet details in CommentModel serialization Add a nested 'pets' object to CommentModel.toMap so serialized comments include the pet's name and profile image URL (mapped from petName and petProfileImageUrl). This allows downstream consumers to access basic pet info alongside the comment payload. --- lib/models/post_model.dart | 4 ++++ 1 file changed, 4 insertions(+) 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, + }, }; } From 35315de74744798a3fbfd8ef72bd59477bc0655c Mon Sep 17 00:00:00 2001 From: Syed Salman Reza <71028588+syed-reza98@users.noreply.github.com> Date: Thu, 7 May 2026 14:14:57 +0600 Subject: [PATCH 3/4] 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 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. --- .gitignore | 2 ++ lib/controllers/connectivity_controller.dart | 19 ++++-------- lib/repositories/auth_repository.dart | 16 ++++++---- lib/repositories/offline_feed_repository.dart | 14 ++++----- .../offline_marketplace_repository.dart | 10 +++---- lib/utils/health_improvements.dart | 10 ++++--- supabase/functions/delete-self/index.ts | 30 +++++++++++++++++++ 7 files changed, 67 insertions(+), 34 deletions(-) create mode 100644 supabase/functions/delete-self/index.ts 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/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/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 }) +}) From 57e42d59feab4cf817ab6aec47c781f31f637425 Mon Sep 17 00:00:00 2001 From: Syed Salman Reza Date: Thu, 7 May 2026 17:39:15 +0600 Subject: [PATCH 4/4] Update problems report; rename flutter_bootstrap Update android/build/reports/problems/problems-report.html to replace the embedded diagnostics JSON (adds deprecation warnings about declaring 'is-' Boolean properties and updates problem counts/documentation links). Also rename web/flutter_bootstrap.js to web/flutter_bootstrap.js.bak (file rename, no content changes). --- android/build/reports/problems/problems-report.html | 2 +- web/{flutter_bootstrap.js => flutter_bootstrap.js.bak} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename web/{flutter_bootstrap.js => flutter_bootstrap.js.bak} (100%) 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/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