Fix avatar caching issues
- 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 <noreply@anthropic.com>
This commit is contained in:
parent
70f9a91be1
commit
48ceaeda35
@ -43,7 +43,6 @@ import {
|
|||||||
} from '@/services/BeneficiaryDetailController';
|
} from '@/services/BeneficiaryDetailController';
|
||||||
import { BeneficiaryMenu } from '@/components/ui/BeneficiaryMenu';
|
import { BeneficiaryMenu } from '@/components/ui/BeneficiaryMenu';
|
||||||
import { ImageLightbox } from '@/components/ImageLightbox';
|
import { ImageLightbox } from '@/components/ImageLightbox';
|
||||||
import { bustImageCache } from '@/utils/imageUtils';
|
|
||||||
import { useDebounce } from '@/hooks/useDebounce';
|
import { useDebounce } from '@/hooks/useDebounce';
|
||||||
|
|
||||||
// WebView Dashboard URL - opens specific deployment directly
|
// 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')}
|
disabled={!beneficiary.avatar || beneficiary.avatar.trim() === '' || beneficiary.avatar.includes('placeholder')}
|
||||||
>
|
>
|
||||||
{beneficiary.avatar && beneficiary.avatar.trim() !== '' && !beneficiary.avatar.includes('placeholder') ? (
|
{beneficiary.avatar && beneficiary.avatar.trim() !== '' && !beneficiary.avatar.includes('placeholder') ? (
|
||||||
<Image source={{ uri: bustImageCache(beneficiary.avatar) || undefined }} style={styles.headerAvatarImage} />
|
<Image
|
||||||
|
key={beneficiary.avatar}
|
||||||
|
source={{ uri: beneficiary.avatar }}
|
||||||
|
style={styles.headerAvatarImage}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<View style={styles.headerAvatar}>
|
<View style={styles.headerAvatar}>
|
||||||
<Text style={styles.headerAvatarText}>
|
<Text style={styles.headerAvatarText}>
|
||||||
@ -575,7 +578,11 @@ export default function BeneficiaryDetailScreen() {
|
|||||||
disabled={isSavingEdit}
|
disabled={isSavingEdit}
|
||||||
>
|
>
|
||||||
{editForm.avatar ? (
|
{editForm.avatar ? (
|
||||||
<Image source={{ uri: bustImageCache(editForm.avatar) || undefined }} style={styles.avatarPickerImage} />
|
<Image
|
||||||
|
key={editForm.avatar}
|
||||||
|
source={{ uri: editForm.avatar }}
|
||||||
|
style={styles.avatarPickerImage}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<View style={styles.avatarPickerPlaceholder}>
|
<View style={styles.avatarPickerPlaceholder}>
|
||||||
<Ionicons name="camera" size={32} color={AppColors.textMuted} />
|
<Ionicons name="camera" size={32} color={AppColors.textMuted} />
|
||||||
@ -674,7 +681,7 @@ export default function BeneficiaryDetailScreen() {
|
|||||||
{/* Avatar Lightbox */}
|
{/* Avatar Lightbox */}
|
||||||
<ImageLightbox
|
<ImageLightbox
|
||||||
visible={lightboxVisible}
|
visible={lightboxVisible}
|
||||||
imageUri={bustImageCache(beneficiary?.avatar)}
|
imageUri={beneficiary?.avatar || null}
|
||||||
onClose={() => setLightboxVisible(false)}
|
onClose={() => setLightboxVisible(false)}
|
||||||
/>
|
/>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
|
|||||||
@ -27,7 +27,6 @@ import {
|
|||||||
} from '@/constants/theme';
|
} from '@/constants/theme';
|
||||||
import type { Beneficiary } from '@/types';
|
import type { Beneficiary } from '@/types';
|
||||||
import { isSubscriptionActive } from '@/services/subscription';
|
import { isSubscriptionActive } from '@/services/subscription';
|
||||||
import { bustImageCache } from '@/utils/imageUtils';
|
|
||||||
|
|
||||||
// Beneficiary card with equipment status support
|
// Beneficiary card with equipment status support
|
||||||
interface BeneficiaryCardProps {
|
interface BeneficiaryCardProps {
|
||||||
@ -116,7 +115,11 @@ function BeneficiaryCard({ beneficiary, onPress, onActivate }: BeneficiaryCardPr
|
|||||||
{/* Avatar */}
|
{/* Avatar */}
|
||||||
<View style={styles.avatarWrapper}>
|
<View style={styles.avatarWrapper}>
|
||||||
{hasValidAvatar ? (
|
{hasValidAvatar ? (
|
||||||
<Image source={{ uri: bustImageCache(beneficiary.avatar) || undefined }} style={styles.avatarImage} />
|
<Image
|
||||||
|
key={beneficiary.avatar}
|
||||||
|
source={{ uri: beneficiary.avatar }}
|
||||||
|
style={styles.avatarImage}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<View style={[styles.avatar, hasNoSubscription && styles.avatarNoSubscription]}>
|
<View style={[styles.avatar, hasNoSubscription && styles.avatarNoSubscription]}>
|
||||||
<Text style={[styles.avatarText, hasNoSubscription && styles.avatarTextNoSubscription]}>
|
<Text style={[styles.avatarText, hasNoSubscription && styles.avatarTextNoSubscription]}>
|
||||||
|
|||||||
@ -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
|
// Mock AsyncStorage
|
||||||
jest.mock('@react-native-async-storage/async-storage', () => ({
|
jest.mock('@react-native-async-storage/async-storage', () => ({
|
||||||
getItem: jest.fn(),
|
getItem: jest.fn(),
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import * as SecureStore from 'expo-secure-store';
|
|||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
import * as wifiPasswordStore from './wifiPasswordStore';
|
import * as wifiPasswordStore from './wifiPasswordStore';
|
||||||
import 'react-native-get-random-values'; // Polyfill for crypto.getRandomValues
|
import 'react-native-get-random-values'; // Polyfill for crypto.getRandomValues
|
||||||
|
import { bustImageCache } from '@/utils/imageUtils';
|
||||||
|
|
||||||
// Callback for handling unauthorized responses (401)
|
// Callback for handling unauthorized responses (401)
|
||||||
let onUnauthorizedCallback: (() => void) | null = null;
|
let onUnauthorizedCallback: (() => void) | null = null;
|
||||||
@ -733,7 +734,7 @@ class ApiService {
|
|||||||
customName: item.customName || null, // User's custom name for this beneficiary
|
customName: item.customName || null, // User's custom name for this beneficiary
|
||||||
displayName: item.displayName || item.customName || item.name || item.email, // Server-provided displayName
|
displayName: item.displayName || item.customName || item.name || item.email, // Server-provided displayName
|
||||||
originalName: item.originalName || item.name, // Original name from beneficiaries table
|
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,
|
status: 'offline' as const,
|
||||||
email: item.email,
|
email: item.email,
|
||||||
address: typeof item.address === 'string' ? item.address : (item.address?.street ? `${item.address.street}, ${item.address.city}` : undefined),
|
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
|
customName: data.customName || null, // User's custom name for this beneficiary
|
||||||
displayName: data.displayName || data.customName || data.name || data.email, // Server-provided displayName
|
displayName: data.displayName || data.customName || data.name || data.email, // Server-provided displayName
|
||||||
originalName: data.originalName || data.name, // Original name from beneficiaries table
|
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,
|
status: 'offline' as const,
|
||||||
email: data.email,
|
email: data.email,
|
||||||
address: typeof data.address === 'string' ? data.address : (data.address?.street ? `${data.address.street}, ${data.address.city}` : undefined),
|
address: typeof data.address === 'string' ? data.address : (data.address?.street ? `${data.address.street}, ${data.address.city}` : undefined),
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user