diff --git a/app/(tabs)/beneficiaries/[id]/index.tsx b/app/(tabs)/beneficiaries/[id]/index.tsx
index b83daee..dea4e44 100644
--- a/app/(tabs)/beneficiaries/[id]/index.tsx
+++ b/app/(tabs)/beneficiaries/[id]/index.tsx
@@ -43,6 +43,7 @@ import {
} from '@/services/BeneficiaryDetailController';
import { BeneficiaryMenu } from '@/components/ui/BeneficiaryMenu';
import { ImageLightbox } from '@/components/ImageLightbox';
+import { bustImageCache } from '@/utils/imageUtils';
// WebView Dashboard URL - opens specific deployment directly
const getDashboardUrl = (deploymentId?: number) => {
@@ -298,7 +299,8 @@ export default function BeneficiaryDetailScreen() {
setIsEditModalVisible(false);
toast.success('Saved', isCustodian ? 'Profile updated successfully' : 'Nickname saved');
- loadBeneficiary(false);
+ // Reload to get updated data with fresh avatar URL (cache-busting timestamp will be applied)
+ await loadBeneficiary(false);
} catch (err) {
toast.error('Error', 'Failed to save changes.');
} finally {
@@ -389,7 +391,7 @@ export default function BeneficiaryDetailScreen() {
disabled={!beneficiary.avatar || beneficiary.avatar.trim() === '' || beneficiary.avatar.includes('placeholder')}
>
{beneficiary.avatar && beneficiary.avatar.trim() !== '' && !beneficiary.avatar.includes('placeholder') ? (
-
+
) : (
@@ -526,7 +528,7 @@ export default function BeneficiaryDetailScreen() {
disabled={isSavingEdit}
>
{editForm.avatar ? (
-
+
) : (
@@ -625,7 +627,7 @@ export default function BeneficiaryDetailScreen() {
{/* Avatar Lightbox */}
setLightboxVisible(false)}
/>
diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx
index 956aed2..40125d9 100644
--- a/app/(tabs)/index.tsx
+++ b/app/(tabs)/index.tsx
@@ -27,6 +27,7 @@ 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 {
@@ -115,7 +116,7 @@ function BeneficiaryCard({ beneficiary, onPress, onActivate }: BeneficiaryCardPr
{/* Avatar */}
{hasValidAvatar ? (
-
+
) : (
diff --git a/utils/__tests__/imageUtils.test.ts b/utils/__tests__/imageUtils.test.ts
new file mode 100644
index 0000000..11a0eac
--- /dev/null
+++ b/utils/__tests__/imageUtils.test.ts
@@ -0,0 +1,65 @@
+import { bustImageCache } from '../imageUtils';
+
+describe('bustImageCache', () => {
+ it('should add cache-busting parameter to HTTP URLs', () => {
+ const url = 'https://example.com/avatar.jpg';
+ const result = bustImageCache(url, 1234567890);
+ expect(result).toBe('https://example.com/avatar.jpg?t=1234567890');
+ });
+
+ it('should add cache-busting parameter to URLs with existing query params', () => {
+ const url = 'https://example.com/avatar.jpg?size=large';
+ const result = bustImageCache(url, 1234567890);
+ expect(result).toBe('https://example.com/avatar.jpg?size=large&t=1234567890');
+ });
+
+ it('should not add cache-busting to file:// URIs', () => {
+ const uri = 'file:///path/to/local/image.jpg';
+ const result = bustImageCache(uri);
+ expect(result).toBe(uri);
+ });
+
+ it('should not add cache-busting to data URIs', () => {
+ const dataUri = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA';
+ const result = bustImageCache(dataUri);
+ expect(result).toBe(dataUri);
+ });
+
+ it('should not add cache-busting to placeholder images', () => {
+ const placeholderUrl = 'https://example.com/placeholder.png';
+ const result = bustImageCache(placeholderUrl);
+ expect(result).toBe(placeholderUrl);
+ });
+
+ it('should return null for null input', () => {
+ const result = bustImageCache(null);
+ expect(result).toBeNull();
+ });
+
+ it('should return null for undefined input', () => {
+ const result = bustImageCache(undefined);
+ expect(result).toBeNull();
+ });
+
+ it('should return null for empty string', () => {
+ const result = bustImageCache('');
+ expect(result).toBeNull();
+ });
+
+ it('should use current timestamp when not provided', () => {
+ const url = 'https://example.com/avatar.jpg';
+ const beforeTimestamp = Date.now();
+ const result = bustImageCache(url);
+ const afterTimestamp = Date.now();
+
+ // Extract timestamp from result
+ const match = result?.match(/t=(\d+)/);
+ expect(match).toBeTruthy();
+
+ if (match) {
+ const timestamp = parseInt(match[1], 10);
+ expect(timestamp).toBeGreaterThanOrEqual(beforeTimestamp);
+ expect(timestamp).toBeLessThanOrEqual(afterTimestamp);
+ }
+ });
+});
diff --git a/utils/imageUtils.ts b/utils/imageUtils.ts
index e8b4b63..b801866 100644
--- a/utils/imageUtils.ts
+++ b/utils/imageUtils.ts
@@ -51,3 +51,36 @@ export async function optimizeImage(
return uri;
}
}
+
+/**
+ * Add cache-busting query parameter to image URL
+ * Prevents browsers and React Native Image from caching old versions
+ *
+ * @param uri - Image URI (can be http, https, or file://)
+ * @param timestamp - Optional timestamp (defaults to current time)
+ * @returns URI with cache-busting parameter
+ */
+export function bustImageCache(uri: string | null | undefined, timestamp?: number): string | null {
+ if (!uri || uri.trim() === '') {
+ return null;
+ }
+
+ // Don't add cache buster to local file URIs (file://)
+ if (uri.startsWith('file://')) {
+ return uri;
+ }
+
+ // Don't add cache buster to data URIs
+ if (uri.startsWith('data:')) {
+ return uri;
+ }
+
+ // Don't add cache buster to placeholder images
+ if (uri.includes('placeholder')) {
+ return uri;
+ }
+
+ const cacheBuster = timestamp || Date.now();
+ const separator = uri.includes('?') ? '&' : '?';
+ return `${uri}${separator}t=${cacheBuster}`;
+}