react-native-flix-codepush
Version:
A modern CodePush implementation for React Native applications
257 lines (222 loc) • 7.51 kB
text/typescript
import { useCallback, useEffect, useRef, useState } from 'react';
import { AppState, type AppStateStatus } from 'react-native';
import { CodePush } from '../core/CodePush';
import {
CodePushSyncStatus,
type CodePushInstallMode,
type CodePushLocalPackage,
type CodePushRemotePackage,
type CodePushSyncOptions,
type UseCodePushResult,
} from '../types';
export function useCodePush(options: CodePushSyncOptions = {}): UseCodePushResult {
const codePush = CodePush.getInstance();
const isMountedRef = useRef(true);
const [syncStatus, setSyncStatus] = useState<CodePushSyncStatus>(CodePushSyncStatus.UP_TO_DATE);
const [currentPackage, setCurrentPackage] = useState<CodePushLocalPackage | null>(null);
const [pendingUpdate, setPendingUpdate] = useState<CodePushRemotePackage | null>(null);
const [isCheckingForUpdate, setIsCheckingForUpdate] = useState<boolean>(false);
const [isDownloading, setIsDownloading] = useState<boolean>(false);
const [isInstalling, setIsInstalling] = useState<boolean>(false);
const [downloadProgress, setDownloadProgress] = useState<{ receivedBytes: number; totalBytes: number }>({
receivedBytes: 0,
totalBytes: 0,
});
const handleStatusChange = useCallback((status: CodePushSyncStatus) => {
if (isMountedRef.current) {
setSyncStatus(status);
}
}, []);
const handleDownloadProgress = useCallback((progress: { receivedBytes: number; totalBytes: number }) => {
if (isMountedRef.current) {
setDownloadProgress(progress);
}
}, []);
useEffect(() => {
const loadCurrentPackage = async () => {
try {
const pkg = await codePush.getCurrentPackage();
if (pkg && isMountedRef.current) {
setCurrentPackage(pkg);
}
} catch (error) {
console.error('Failed to load current package:', error);
}
};
loadCurrentPackage();
return () => {
isMountedRef.current = false;
};
}, [codePush]);
const checkForUpdate = useCallback(async (deploymentKey?: string): Promise<CodePushRemotePackage | null> => {
if (isCheckingForUpdate) {
return null;
}
try {
setIsCheckingForUpdate(true);
setSyncStatus(CodePushSyncStatus.CHECKING_FOR_UPDATE);
const update = await codePush.checkForUpdate(deploymentKey);
if (isMountedRef.current) {
if (update) {
setPendingUpdate(update);
} else {
setSyncStatus(CodePushSyncStatus.UP_TO_DATE);
}
}
return update;
} catch (error) {
console.error('Failed to check for update:', error);
if (isMountedRef.current) {
setSyncStatus(CodePushSyncStatus.UNKNOWN_ERROR);
}
return null;
} finally {
if (isMountedRef.current) {
setIsCheckingForUpdate(false);
}
}
}, [isCheckingForUpdate, codePush]);
useEffect(() => {
const handleAppStateChange = async (nextAppState: AppStateStatus) => {
if (nextAppState === 'active') {
const config = await codePush.getConfiguration();
if (config.checkFrequency === 2) { // ON_APP_RESUME
checkForUpdate();
}
}
};
const subscription = AppState.addEventListener('change', handleAppStateChange);
const checkOnStart = async () => {
const config = await codePush.getConfiguration();
if (config.checkFrequency === 1) { // ON_APP_START
checkForUpdate();
}
};
checkOnStart();
return () => {
subscription.remove();
};
}, [codePush, checkForUpdate]);
const downloadUpdate = useCallback(async (remotePackage: CodePushRemotePackage): Promise<CodePushLocalPackage> => {
try {
setIsDownloading(true);
setSyncStatus(CodePushSyncStatus.DOWNLOADING_PACKAGE);
const localPackage = await codePush.downloadUpdate(remotePackage, handleDownloadProgress);
return localPackage;
} catch (error) {
console.error('Failed to download update:', error);
setSyncStatus(CodePushSyncStatus.UNKNOWN_ERROR);
throw error;
} finally {
if (isMountedRef.current) {
setIsDownloading(false);
}
}
}, [codePush, handleDownloadProgress]);
const installUpdate = useCallback(async (packageHash: string, installMode?: CodePushInstallMode): Promise<void> => {
try {
setIsInstalling(true);
setSyncStatus(CodePushSyncStatus.INSTALLING_UPDATE);
await codePush.installUpdate(packageHash, installMode);
if (isMountedRef.current) {
setSyncStatus(CodePushSyncStatus.UPDATE_INSTALLED);
}
} catch (error) {
console.error('Failed to install update:', error);
if (isMountedRef.current) {
setSyncStatus(CodePushSyncStatus.UNKNOWN_ERROR);
}
throw error;
} finally {
if (isMountedRef.current) {
setIsInstalling(false);
}
}
}, [codePush]);
const sync = useCallback(async (syncOptions?: CodePushSyncOptions): Promise<CodePushSyncStatus> => {
try {
if (isMountedRef.current) {
setSyncStatus(CodePushSyncStatus.SYNC_IN_PROGRESS);
}
const mergedOptions = { ...options, ...syncOptions };
const status = await codePush.sync(
mergedOptions,
handleStatusChange,
handleDownloadProgress
);
if (isMountedRef.current) {
setSyncStatus(status);
}
if (status === CodePushSyncStatus.UPDATE_INSTALLED && isMountedRef.current) {
const pkg = await codePush.getCurrentPackage();
if (pkg && isMountedRef.current) {
setCurrentPackage(pkg);
}
}
return status;
} catch (error) {
console.error('Sync failed:', error);
if (isMountedRef.current) {
setSyncStatus(CodePushSyncStatus.UNKNOWN_ERROR);
}
throw error;
}
}, [options, codePush, handleStatusChange, handleDownloadProgress]);
const getCurrentPackage = useCallback(async (): Promise<CodePushLocalPackage | null> => {
try {
const pkg = await codePush.getCurrentPackage();
if (pkg && isMountedRef.current) {
setCurrentPackage(pkg);
}
return pkg;
} catch (error) {
console.error('Failed to get current package:', error);
return null;
}
}, [codePush]);
const restartApp = useCallback(async (): Promise<void> => {
return codePush.restartApplication(true);
}, [codePush]);
const clearUpdates = useCallback(async (): Promise<void> => {
try {
await codePush.clearUpdates();
if (isMountedRef.current) {
setCurrentPackage(null);
setPendingUpdate(null);
setSyncStatus(CodePushSyncStatus.UP_TO_DATE);
}
} catch (error) {
console.error('Failed to clear updates:', error);
throw error;
}
}, [codePush]);
const getUpdateMetadata = useCallback(async (): Promise<CodePushLocalPackage | null> => {
return codePush.getCurrentPackage();
}, [codePush]);
const cancelSync = useCallback((): void => {
codePush.cancelSync();
if (isMountedRef.current) {
setIsDownloading(false);
setIsInstalling(false);
setSyncStatus(CodePushSyncStatus.UP_TO_DATE);
}
}, [codePush]);
return {
checkForUpdate,
sync,
getCurrentPackage,
restartApp,
clearUpdates,
getUpdateMetadata,
downloadUpdate,
installUpdate,
syncStatus,
currentPackage,
pendingUpdate,
isCheckingForUpdate,
isDownloading,
isInstalling,
cancelSync,
downloadProgress,
};
}