import { Inter_900Black } from '@expo-google-fonts/inter';
import Constants from 'expo-constants';
import { ExpoUpdatesManifest } from 'expo-manifests';
import { requireNativeModule } from 'expo-modules-core';
import { StatusBar } from 'expo-status-bar';
import * as Updates from 'expo-updates';
import { UpdatesLogEntry } from 'expo-updates';
import React from 'react';
import { ActivityIndicator, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
const ExpoUpdatesE2ETest = requireNativeModule('ExpoUpdatesE2ETest');
require('./includedAssets/test.png');
require('./includedAssets/lock-filled.svg');
// eslint-disable-next-line no-unused-expressions
Inter_900Black;
// keep the line below for replacement in generate-test-update-bundles
// REPLACE_WITH_CRASHING_CODE
function TestValue(props: { testID: string; value: string }) {
return (
{props.testID}
{props.value || 'null'}
);
}
function TestButton(props: { testID: string; onPress: () => void }) {
return (
[styles.button, pressed && styles.buttonPressed]}
onPress={props.onPress}>
{props.testID}
);
}
export default function App() {
const [numAssetFiles, setNumAssetFiles] = React.useState(0);
const [logs, setLogs] = React.useState([]);
const [numActive, setNumActive] = React.useState(0);
const [extraParamsString, setExtraParamsString] = React.useState('');
const [isRollback, setIsRollback] = React.useState(false);
const [isReloading, setIsReloading] = React.useState(false);
const [startTime, setStartTime] = React.useState(null);
const [didCheckAndDownloadHappenInParallel, setDidCheckAndDownloadHappenInParallel] =
React.useState(false);
const [wasIsStartupProcedureRunningEverTrue, setWasIsStartupProcedureRunningEverTrue] =
React.useState(false);
const {
isStartupProcedureRunning,
currentlyRunning,
availableUpdate,
downloadedUpdate,
isUpdateAvailable,
isUpdatePending,
checkError,
isChecking,
isDownloading,
isRestarting,
restartCount,
downloadProgress,
} = Updates.useUpdates();
React.useEffect(() => {
setStartTime(Date.now());
}, []);
React.useEffect(() => {
if (isStartupProcedureRunning) {
setWasIsStartupProcedureRunningEverTrue(true);
}
}, [isStartupProcedureRunning]);
// Get rollback state with this, until useUpdates() supports rollbacks
React.useEffect(() => {
const handleAsync = async () => {
setIsRollback(availableUpdate?.type === Updates.UpdateInfoType.ROLLBACK);
};
if (isUpdateAvailable) {
handleAsync();
}
}, [isUpdateAvailable]);
// Record if checking an downloading happen in parallel (they shouldn't)
React.useEffect(() => {
if (isChecking && isDownloading) {
setDidCheckAndDownloadHappenInParallel(true);
}
}, [isChecking, isDownloading]);
const runBlockAsync = (block: () => Promise) => async () => {
setNumActive((n) => n + 1);
try {
await block();
} catch (e) {
console.warn(e);
} finally {
setNumActive((n) => n - 1);
}
};
const handleSetExtraParams = runBlockAsync(async () => {
await Updates.setExtraParamAsync('testsetnull', 'testvalue');
await Updates.setExtraParamAsync('testsetnull', null);
await Updates.setExtraParamAsync('testparam', 'testvalue');
const params = await Updates.getExtraParamsAsync();
setExtraParamsString(JSON.stringify(params, null, 2));
});
const handleSetUpdateURLAndRequestHeadersOverride = runBlockAsync(async () => {
Updates.setUpdateURLAndRequestHeadersOverride({
updateUrl: `${Constants.expoConfig?.updates?.url}-override`,
requestHeaders: {},
});
});
const handleToggleUpdateRequestHeadersOverride = runBlockAsync(async () => {
if (currentlyRunning.channel !== 'preview') {
Updates.setUpdateRequestHeadersOverride({
'expo-channel-name': 'preview',
});
} else {
Updates.setUpdateRequestHeadersOverride(null);
}
});
const handleReadAssetFiles = runBlockAsync(async () => {
const numFiles = await ExpoUpdatesE2ETest.readInternalAssetsFolderAsync();
setNumAssetFiles(numFiles);
});
const handleClearAssetFiles = runBlockAsync(async () => {
await ExpoUpdatesE2ETest.clearInternalAssetsFolderAsync();
const numFiles = await ExpoUpdatesE2ETest.readInternalAssetsFolderAsync();
setNumAssetFiles(numFiles);
});
const handleReadLogEntries = runBlockAsync(async () => {
const logEntries = await Updates.readLogEntriesAsync(60000);
setLogs(logEntries);
const server_port = process.env.EXPO_PUBLIC_UPDATES_SERVER_PORT;
try {
await fetch(`http://localhost:${server_port}/upload-log-entries`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(logEntries),
});
} catch (e) {
console.log('Server does not support the log entry endpoints');
}
});
const handleClearLogEntries = runBlockAsync(async () => {
await Updates.clearLogEntriesAsync();
});
const handleReload = async () => {
setIsReloading(true);
// this is done after a timeout so that the button press finishes for detox
setTimeout(async () => {
try {
await Updates.reloadAsync();
setIsReloading(false);
} catch (e) {
console.warn(e);
}
}, 2000);
};
const handleCheckForUpdate = runBlockAsync(async () => {
await Updates.checkForUpdateAsync();
});
const handleDownloadUpdate = runBlockAsync(async () => {
await Updates.fetchUpdateAsync();
});
const handleCheckAndDownloadAtSameTime = runBlockAsync(async () => {
await Promise.all([
Updates.checkForUpdateAsync(),
Updates.fetchUpdateAsync(),
Updates.checkForUpdateAsync(),
Updates.fetchUpdateAsync(),
Updates.checkForUpdateAsync(),
Updates.fetchUpdateAsync(),
Updates.checkForUpdateAsync(),
]);
});
const logsToString = (logs: UpdatesLogEntry[]) =>
JSON.stringify(
logs.map((log) => {
return {
message: log.message,
time: new Date(log.timestamp).toISOString(),
code: log.code,
};
})
);
return (
Log messages
{logsToString(logs)}
Updates expoConfig
{JSON.stringify((Updates.manifest as ExpoUpdatesManifest)?.extra?.expoClient || {})}
Constants expoConfig
{JSON.stringify(Constants.expoConfig)}
{numActive > 0 ? : null}
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
marginTop: 100,
marginBottom: 100,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
button: {
alignItems: 'center',
justifyContent: 'center',
margin: 5,
paddingVertical: 2,
paddingHorizontal: 10,
borderRadius: 4,
elevation: 3,
backgroundColor: '#4630EB',
},
buttonPressed: {
backgroundColor: '#FFFFFF',
},
buttonText: {
color: 'white',
fontSize: 10,
},
labelText: {
fontSize: 10,
},
logEntriesContainer: {
margin: 10,
height: 20,
paddingVertical: 5,
paddingHorizontal: 10,
width: '90%',
minWidth: '90%',
borderColor: '#4630EB',
borderWidth: 1,
borderRadius: 4,
},
logEntriesText: {
fontSize: 6,
},
});