import { NativeEventEmitter } from 'react-native'; import NativePulseUpdates from './NativePulseUpdates'; import type { PulseManifest, PulseUpdatesConfig, UpdateCheckResult, UpdateFetchResult } from './types'; const PulseUpdatesModule = NativePulseUpdates; // Track configuration and state let _isConfigured = false; // Cached state values let _isEnabled = false; let _updateId: string | null = null; let _runtimeVersion: string | null = null; let _channel: string | null = null; let _isEmbeddedLaunch = true; let _manifest: PulseManifest | null = null; let _createdAt: Date | null = null; let _localAssets: Record | null = null; /** * Configure pulse-updates with the given options. * * NOTE: This is now OPTIONAL. Configuration is automatically loaded from: * - iOS: Info.plist (keys: PulseUpdatesURL, PulseUpdatesEnabled, etc.) * - Android: AndroidManifest.xml metadata * * Only call this if you need to override the native configuration. */ export function configure(config: PulseUpdatesConfig): void { if (!PulseUpdatesModule) { console.warn('PulseUpdates native module not found'); return; } if (_isConfigured) { console.warn('PulseUpdates.configure() has already been called'); return; } PulseUpdatesModule.configure( config.enabled, config.updateUrl, config.runtimeVersion, config.checkOnLaunch, config.launchWaitMs, config.channel ?? null, config.signingKeyId ?? null, config.signingPublicKey ?? null ); _isConfigured = true; _isEnabled = config.enabled; _runtimeVersion = config.runtimeVersion; } /** * Whether configure() has been called from JS. * Note: Even if false, native config may be loaded from Info.plist/AndroidManifest.xml */ export function isConfigured(): boolean { return _isConfigured; } /** * Whether pulse-updates is enabled. */ export function getIsEnabled(): boolean { return _isEnabled; } // For backwards compatibility, export as mutable (will be updated by refreshStateAsync) export let isEnabled = false; export let updateId: string | null = null; export let runtimeVersion: string | null = null; export let channel: string | null = null; export let isEmbeddedLaunch = true; // expo-updates compatibility: same as isEmbeddedLaunch export let isUsingEmbeddedAssets = true; export let manifest: PulseManifest | null = null; export let createdAt: Date | null = null; /** * A dictionary of locally available assets for the current update. * Keys are the asset keys from the manifest, values are local file:// URLs. * This can be used by asset resolvers to use locally downloaded or embedded * assets instead of fetching from remote URLs. */ export let localAssets: Record | null = null; /** * Refresh exported values from native module. * Call this at app startup to get the current state from native. * Configuration is loaded automatically from Info.plist/AndroidManifest.xml. */ export async function refreshStateAsync(): Promise { if (__DEV__ || !PulseUpdatesModule) return; try { const state = await PulseUpdatesModule.getStateAsync(); // Native module returns state based on native config (Info.plist/AndroidManifest.xml) _isEnabled = state.isEnabled ?? false; _updateId = state.updateId ?? null; _runtimeVersion = state.runtimeVersion ?? null; _channel = state.channel ?? null; _isEmbeddedLaunch = state.isEmbeddedLaunch ?? true; // Update exported values isEnabled = _isEnabled; updateId = _updateId; runtimeVersion = _runtimeVersion; channel = _channel; isEmbeddedLaunch = _isEmbeddedLaunch; // expo-updates compatibility: same as isEmbeddedLaunch isUsingEmbeddedAssets = _isEmbeddedLaunch; if (state.manifestString) { try { _manifest = JSON.parse(state.manifestString); manifest = _manifest; } catch { _manifest = null; manifest = null; } } else { _manifest = null; manifest = null; } _createdAt = state.commitTime ? new Date(state.commitTime) : null; createdAt = _createdAt; // Read localAssets map for asset resolution _localAssets = state.localAssets ?? null; localAssets = _localAssets; } catch (error) { console.warn('[PulseUpdates] Failed to refresh state:', error); // Keep current values on error } } /** * Check the server for available updates. */ export async function checkForUpdateAsync(): Promise { if (__DEV__) { return { isAvailable: false, reason: 'Updates disabled in development mode' }; } if (!PulseUpdatesModule) { throw new Error('PulseUpdates native module not found'); } const result = await PulseUpdatesModule.checkForUpdateAsync(); if (result.manifestString) { return { isAvailable: result.isAvailable, manifest: JSON.parse(result.manifestString), isRollback: result.isRollback, failedReason: result.failedReason, }; } return { isAvailable: result.isAvailable, isRollback: result.isRollback, failedReason: result.failedReason, }; } /** * Download the most recent update from the server. */ export async function fetchUpdateAsync(): Promise { if (__DEV__) { return { isNew: false }; } if (!PulseUpdatesModule) { throw new Error('PulseUpdates native module not found'); } const result = await PulseUpdatesModule.fetchUpdateAsync(); if (result.manifestString) { return { isNew: result.isNew, manifest: JSON.parse(result.manifestString), }; } return { isNew: result.isNew, }; } /** * Reload the app using the most recently downloaded update. */ export async function reloadAsync(): Promise { if (__DEV__) { console.log('[PulseUpdates] Reload skipped in development mode'); return; } if (!PulseUpdatesModule) { throw new Error('PulseUpdates native module not found'); } await PulseUpdatesModule.reloadAsync(); } /** * Mark the current update as successfully launched. */ export async function markAppReady(): Promise { if (!_isEnabled) { return; } if (!PulseUpdatesModule) { return; } await PulseUpdatesModule.markAppReady(); } /** * Report a launch failure to trigger rollback on next start. */ export async function reportLaunchFailure(reason: string): Promise { if (!_isEnabled) { return; } if (!PulseUpdatesModule) { return; } await PulseUpdatesModule.reportLaunchFailure(reason); } /** * Get the build number from the current update's metadata. */ export function getUpdateBuild(): string | null { if (!_isEnabled || _isEmbeddedLaunch || !_manifest) { return null; } return (_manifest.metadata?.build as string) ?? null; } /** * Get display version string (e.g., "1.0.0" or "1.0.0.42"). */ export function getDisplayVersion(appVersion: string): string { const build = getUpdateBuild(); if (build) { return `${appVersion}.${build}`; } return appVersion; } /** * Get the local file path for an asset by its key. * Returns the local file:// URL if the asset is available locally, * or null if it needs to be fetched from the remote URL. */ export function getLocalAssetPath(assetKey: string): string | null { if (!_localAssets) { return null; } return _localAssets[assetKey] ?? null; } /** * Get all locally available assets. * Returns a copy of the localAssets map. */ export function getLocalAssets(): Record { return _localAssets ? { ..._localAssets } : {}; } // Event emitter for update events let eventEmitter: NativeEventEmitter | null = null; function getEventEmitter(): NativeEventEmitter { if (!eventEmitter && PulseUpdatesModule) { eventEmitter = new NativeEventEmitter(PulseUpdatesModule as any); } return eventEmitter!; } export type UpdateEventType = | 'updateAvailable' | 'updateDownloaded' | 'updateError' | 'noUpdateAvailable'; export interface UpdateEvent { type: UpdateEventType; manifest?: PulseManifest; error?: string; } /** * Add a listener for update events. */ export function addUpdateListener( listener: (event: UpdateEvent) => void ): { remove: () => void } { const emitter = getEventEmitter(); const subscription = emitter.addListener('PulseUpdatesEvent', listener); return { remove: () => subscription.remove(), }; }