From 48ceaeda3539d1aac4ff783a390427c311cd4922 Mon Sep 17 00:00:00 2001 From: Sergei Date: Thu, 29 Jan 2026 12:51:37 -0800 Subject: [PATCH] Fix avatar caching issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add bustImageCache() at API layer to ensure avatars always have cache-busting timestamps - Remove redundant bustImageCache() calls from UI components (already handled at API level) - Add key prop to Image components using avatar URL to force re-render on avatar change - Add expo-image-manipulator mock to jest.setup.js This ensures that when users upload new avatars, React Native's Image component displays the updated image immediately instead of showing cached old versions. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- app/(tabs)/beneficiaries/[id]/index.tsx | 15 +++++++++++---- app/(tabs)/index.tsx | 7 +++++-- jest.setup.js | 10 ++++++++++ services/api.ts | 5 +++-- 4 files changed, 29 insertions(+), 8 deletions(-) diff --git a/app/(tabs)/beneficiaries/[id]/index.tsx b/app/(tabs)/beneficiaries/[id]/index.tsx index 68184af..2cc0179 100644 --- a/app/(tabs)/beneficiaries/[id]/index.tsx +++ b/app/(tabs)/beneficiaries/[id]/index.tsx @@ -43,7 +43,6 @@ import { } from '@/services/BeneficiaryDetailController'; import { BeneficiaryMenu } from '@/components/ui/BeneficiaryMenu'; import { ImageLightbox } from '@/components/ImageLightbox'; -import { bustImageCache } from '@/utils/imageUtils'; import { useDebounce } from '@/hooks/useDebounce'; // WebView Dashboard URL - opens specific deployment directly @@ -439,7 +438,11 @@ export default function BeneficiaryDetailScreen() { disabled={!beneficiary.avatar || beneficiary.avatar.trim() === '' || beneficiary.avatar.includes('placeholder')} > {beneficiary.avatar && beneficiary.avatar.trim() !== '' && !beneficiary.avatar.includes('placeholder') ? ( - + ) : ( @@ -575,7 +578,11 @@ export default function BeneficiaryDetailScreen() { disabled={isSavingEdit} > {editForm.avatar ? ( - + ) : ( @@ -674,7 +681,7 @@ export default function BeneficiaryDetailScreen() { {/* Avatar Lightbox */} setLightboxVisible(false)} /> diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx index 40125d9..2305f47 100644 --- a/app/(tabs)/index.tsx +++ b/app/(tabs)/index.tsx @@ -27,7 +27,6 @@ import { } from '@/constants/theme'; import type { Beneficiary } from '@/types'; import { isSubscriptionActive } from '@/services/subscription'; -import { bustImageCache } from '@/utils/imageUtils'; // Beneficiary card with equipment status support interface BeneficiaryCardProps { @@ -116,7 +115,11 @@ function BeneficiaryCard({ beneficiary, onPress, onActivate }: BeneficiaryCardPr {/* Avatar */} {hasValidAvatar ? ( - + ) : ( diff --git a/jest.setup.js b/jest.setup.js index 1247efc..4c3abc3 100644 --- a/jest.setup.js +++ b/jest.setup.js @@ -61,6 +61,16 @@ jest.mock('expo-image-picker', () => ({ ), })); +jest.mock('expo-image-manipulator', () => ({ + manipulateAsync: jest.fn((uri, actions, options) => + Promise.resolve({ uri: uri }) + ), + SaveFormat: { + JPEG: 'jpeg', + PNG: 'png', + }, +})); + // Mock AsyncStorage jest.mock('@react-native-async-storage/async-storage', () => ({ getItem: jest.fn(), diff --git a/services/api.ts b/services/api.ts index 4969017..cc7f789 100644 --- a/services/api.ts +++ b/services/api.ts @@ -4,6 +4,7 @@ import * as SecureStore from 'expo-secure-store'; import AsyncStorage from '@react-native-async-storage/async-storage'; import * as wifiPasswordStore from './wifiPasswordStore'; import 'react-native-get-random-values'; // Polyfill for crypto.getRandomValues +import { bustImageCache } from '@/utils/imageUtils'; // Callback for handling unauthorized responses (401) let onUnauthorizedCallback: (() => void) | null = null; @@ -733,7 +734,7 @@ class ApiService { customName: item.customName || null, // User's custom name for this beneficiary displayName: item.displayName || item.customName || item.name || item.email, // Server-provided displayName originalName: item.originalName || item.name, // Original name from beneficiaries table - avatar: item.avatarUrl || undefined, // Use uploaded avatar from server + avatar: bustImageCache(item.avatarUrl) || undefined, // Use uploaded avatar from server with cache-busting status: 'offline' as const, email: item.email, address: typeof item.address === 'string' ? item.address : (item.address?.street ? `${item.address.street}, ${item.address.city}` : undefined), @@ -779,7 +780,7 @@ class ApiService { customName: data.customName || null, // User's custom name for this beneficiary displayName: data.displayName || data.customName || data.name || data.email, // Server-provided displayName originalName: data.originalName || data.name, // Original name from beneficiaries table - avatar: data.avatarUrl || undefined, + avatar: bustImageCache(data.avatarUrl) || undefined, // Cache-bust avatar URL status: 'offline' as const, email: data.email, address: typeof data.address === 'string' ? data.address : (data.address?.street ? `${data.address.street}, ${data.address.city}` : undefined),