/** * Pulse Updates Asset Resolver * * Patches React Native's AssetSourceResolver to intercept asset resolution * and serve locally downloaded/embedded assets from OTA updates. * * This approach is based on expo-asset's implementation which patches * AssetSourceResolver.prototype.defaultAsset directly - more reliable than * using setCustomSourceTransformer. * * The patch is applied at module load time to ensure it's in place before * any images start loading. */ // Map of asset key -> local file URL let localAssets: Record | null = null; let isPatched = false; // Store original method let originalDefaultAsset: (() => any) | null = null; /** * Apply the AssetSourceResolver patch immediately at module load time. * This ensures the patch is in place before any images start loading. */ function applyPatch(): void { if (isPatched) return; try { const AssetSourceResolver = require('react-native/Libraries/Image/AssetSourceResolver').default; if (!AssetSourceResolver || !AssetSourceResolver.prototype.defaultAsset) { return; } // Save original method originalDefaultAsset = AssetSourceResolver.prototype.defaultAsset; // Patch the prototype AssetSourceResolver.prototype.defaultAsset = function(): any { try { // Only try to resolve from local assets if we have any if (localAssets && Object.keys(localAssets).length > 0) { const localUri = resolveLocalAsset(this.asset); if (localUri) { return this.fromSource(localUri); } } } catch (e) { // If local asset resolution fails, fall back to original behavior } // Fall back to original behavior try { return originalDefaultAsset!.call(this); } catch (e) { // If original method fails, try calling it as a regular function // This handles edge cases where 'this' context might be lost return originalDefaultAsset!.apply(this, []); } }; isPatched = true; } catch (e) { // Silently fail - will use default asset resolution } } // Apply patch immediately when module is imported applyPatch(); /** * Initialize the asset resolver with the localAssets map. * Call this after PulseUpdates.refreshStateAsync() * * @param assets Map of asset KEY (like "assets/images/icon.png") to local file:// URL */ export function initializeAssetResolver(assets: Record | null): void { localAssets = assets; // Patch is already applied at module load time, just updating the assets map } /** * Update the local assets map (call when a new update is applied) */ export function updateLocalAssets(assets: Record | null): void { localAssets = assets; } /** * Try to resolve an asset from localAssets map * Returns the local file:// URI if found, null otherwise */ function resolveLocalAsset(asset: any): string | null { if (!localAssets || !asset) { return null; } // Construct possible keys to look up const keysToTry = buildAssetKeys(asset); for (const key of keysToTry) { if (localAssets[key]) { return localAssets[key]; } } return null; } /** * Build possible asset keys from Metro asset metadata * Metro assets have: name, type, httpServerLocation, hash, scales * * Key format in localAssets: * - MD5 hash (key field from manifest) - what expo-asset/Metro uses * - SHA256 hash (hash field from manifest) - for integrity verification * - Path-based key for legacy compatibility */ function buildAssetKeys(asset: any): string[] { const keys: string[] = []; const { name, type, httpServerLocation, hash, fileHashes } = asset; // 1. Try Metro's hash directly (MD5 hash of file contents) // This is the primary lookup method matching expo-asset if (hash) { keys.push(hash); } // 2. Try fileHashes if available (Metro may provide multiple hash types) if (fileHashes) { Object.values(fileHashes).forEach((h: any) => { if (typeof h === 'string' && h !== hash) { keys.push(h); } }); } // Build path-based keys let location = httpServerLocation || ''; // Remove leading slash if (location.startsWith('/')) { location = location.slice(1); } // 2. Standard key: assets/path/name.type if (location) { keys.push(`${location}/${name}.${type}`); } // 3. With assets/ prefix if not present if (location && !location.startsWith('assets')) { keys.push(`assets/${location}/${name}.${type}`); } // 4. Without any prefix keys.push(`${name}.${type}`); // 5. Try with scale suffixes for retina assets const scales = asset.scales || [1]; for (const scale of scales) { if (scale !== 1) { const scaleSuffix = `@${scale}x`; if (location) { keys.push(`${location}/${name}${scaleSuffix}.${type}`); if (!location.startsWith('assets')) { keys.push(`assets/${location}/${name}${scaleSuffix}.${type}`); } } } } return keys; } export default { initializeAssetResolver, updateLocalAssets, };