@mgscreativa - Hey man! been digging this past week trying to get codepush to work within my Expo managed project.
I have codepush setup as well through app center, and can also create and deploy codepush bundles to appcenter as well.
The steps i use to create a codepush update & test it:
package.json scripts:
"update:staging:ios": "appcenter codepush release-react -a org/App -d Staging -e index.tsx --plist-file ios/App/Info.plist",
"update:staging:android": "appcenter codepush release-react -a org/App-1 -d Staging -e index.tsx",
"update:staging:all": "yarn run update:staging:ios && yarn run update:staging:android",
Steps in order:
- two new eas builds under the
preview-staging profile for both ios and android
npx expo prebuild to generate the ios and android folders needed locally for the codepush update
yarn update:staging:all to which i get successfull codepush bundles and releases to appcenter
Android is not working at all for me when it comes to codepush updates - when trying through a manual approach, it would see that there is an update for the installed version, download it, then when trying to install it i would run into this error:
codepush error update is invalid - a JS bundle file name "null"
When it comes to iOS on both the manual approach & the silent approach through only the codepush decorator, it would download, install, and restart the app. After it restarted it would succesfully use the codepush bundle, but upon force closing the app and then re opening, it would not use the bundle anymore and be on the original js bundle included in the adhoc release.
Im using Expo sdk 49 with Expo Router with the expo-dev-client setup.
I have internal adhoc testing setup as well where I can utilize the preview-staging build profiles that has also been working as expected, I have been able to continuously utilize eas build commands to build new builds for testing through preview-staging with everything working.
My current setup in eas.json.
{
"cli": {
"version": ">= 3.13.2"
},
"build": {
// * Base build profiles that can be extended
// this will run a dev staging build, and have QR code to scan for device
"dev-staging": {
"distribution": "internal",
"developmentClient": true,
"channel": "dev-staging", // this is used for EAS Updates to target specific builds
"env": {
"APP_ENV": "STAGING",
"ANDROID_GOOGLE_SERVICES": "./google-services/google-services.json",
"IOS_GOOGLE_SERVICES": "./google-services/GoogleService-Info.plist"
}
},
// this will run a dev production build, and have QR code to scan for device
"dev-production": {
"distribution": "internal",
"developmentClient": true,
"channel": "dev-production",
"env": {
"APP_ENV": "PRODUCTION",
"ANDROID_GOOGLE_SERVICES": "./google-services/google-services.json",
"IOS_GOOGLE_SERVICES": "./google-services/GoogleService-Info.plist"
}
},
// ...
// * Build profiles that extend the base build profiles for simulator
// this will run a dev simulator build, and can download & install on simulator through the CLI
"dev-simulator": {
"extends": "dev-staging",
"channel": "dev-simulator",
"ios": {
"simulator": true
}
},
"prod-simulator": {
"extends": "dev-production",
"ios": {
"simulator": true
}
},
// ...
// * Build profiles used for preview builds (adhoc internal testing)
// channels can be used for EAS Updates to target specific builds
"preview-staging": {
"extends": "dev-staging",
"channel": "preview-staging",
"developmentClient": false
},
"preview-production": {
"extends": "dev-production",
"channel": "preview-production",
"developmentClient": false
},
// ...
// * Build profiles used for release builds (app stores & test flight)
// channels can be used for EAS Updates to target specific builds
"release-beta": {
"extends": "preview-staging",
"channel": "release-beta",
"distribution": "store"
},
"release": {
"extends": "preview-production",
"channel": "release",
"distribution": "store"
}
},
"submit": {
"release-beta": {
"android": {
"track": "internal",
"releaseStatus": "draft"
}
},
"release": {
"android": {
"track": "production",
"releaseStatus": "draft"
}
}
}
}
I have an index.tsx file right now that looks like this:
import '@expo/metro-runtime';
import { registerRootComponent } from 'expo';
import { ExpoRoot } from 'expo-router';
import deepLinkHandlers from 'features/DeepLinking';
import { nativeSentryWrap, sentryInit } from 'sentry/config';
import codePushWrap from 'updates/config';
/*
Here we setup all notification handlers that are needed outside the scope of React
- this are handlers that mostly handle notifications when the app is in background/quit state
- very inconsistent on iOS and hard to debug
*/
deepLinkHandlers.backgroundHandlers.setBackgroundMessageHandler();
sentryInit();
/**
* updated entry point to be able to utilize `sentry` and fix an issue with `expo-router`
*
* @see https://docs.expo.dev/router/reference/troubleshooting/
*/
export const App = () => {
const ctx = require.context('./app');
return <ExpoRoot context={ctx} />;
};
const CodePushedApp = codePushWrap(nativeSentryWrap(App));
registerRootComponent(CodePushedApp);
codePushWrap is just a simple wrapper method that uses codePush
- this method worked on iOS like the manual check did below and the same issue as well
- Android would never restart after an app resume, so i never saw anything in terms of the codepush actually downloading and installing, etc
import { CodePushOptions } from 'react-native-code-push';
import codePush from './codePush';
/**
* Options for the `codePush` wrapper
* - these options are configured to allow the the app `on start up` to check for updates
* - if we have an update, it will install and restart immediately, making it seamless to the user
*
* - this is helpful, as if the app hits this `checkFrequency` - we then know its safe to restart the app as the user is not in the middle of something
*/
const codePushOptions: CodePushOptions = {
checkFrequency: codePush.CheckFrequency.ON_APP_RESUME,
installMode: codePush.InstallMode.ON_NEXT_SUSPEND,
};
/**
* Wraps the root component with CodePush with the given options
* @see codePushOptions
* @param RootComponent - App component that is exported from 'expo-router/_app'
*/
function codePushWrap(RootComponent: React.ComponentType<JSX.IntrinsicAttributes>) {
return codePush(codePushOptions)(RootComponent);
}
export default codePushWrap;
My manual approach looks like this:
- this handles each step so i can show UI gracefully to the user
- This manual approach worked for iOS with the codePush.restartApp. But upon user closing app, it reverted and didnt use the codepush bundle (on the codepush website, it shows 1 install, 1 download, 0 rollbacks as well)
- Android is where i got that error above.
/* eslint-disable @typescript-eslint/no-use-before-define */
import { Alert, Platform } from 'react-native';
import { RemotePackage } from 'react-native-code-push';
import { useSettingsStore } from 'stores';
import Toast from 'layouts/ToastLayout/Toast';
import Wait from 'utils/Wait';
import codePush from './codePush';
const { updateAppStateSlice } = useSettingsStore.getState();
export enum UpdateCheck {
Interactive = 'interactive',
Silent = 'silent',
}
async function checkForUpdates(type: UpdateCheck) {
/**
* Interactive updates are used to check for updates when the user manually checks for updates
* - this is helpful to show the user that there are updates available for the app, and give them the option to install the update
*/
if (type === UpdateCheck.Interactive) {
try {
updateAppStateSlice({ checkingForUpdates: true });
await interactiveCheck();
} catch (error) {
handleCodePushError(error);
} finally {
// clean up any alerts that may have been left behind
Toast.Burnt.dismissAllAlerts();
updateAppStateSlice({ checkingForUpdates: false });
}
}
/**
* Silent updates are used to check for updates when the app comes back into the foreground
* - this is helpful to add an indicator to the user that there are updates available for the app, without interrupting their current flow
*/
if (type === UpdateCheck.Silent) {
try {
const update = await codePush.checkForUpdate();
const hasUpdate = !!update;
if (hasUpdate) {
console.log(`[CodePush] Update Metadata: ${JSON.stringify(update)}`);
} else {
console.log(`[CodePush] No update metadata found`);
}
updateAppStateSlice({ update });
} catch (error) {
// if there is an error here, we don't need to show the user anything since this is a silent check
console.warn(`Error silently checking for updates: ${error}`);
}
}
}
export default checkForUpdates;
/**
* Checks for updates on the `codepush` server with `alerts` shown to the user
* - this includes loading indicators and alerts to install the update, etc
* @see UpdateCheck.Interactive
*/
async function interactiveCheck() {
showBurntLoading('Checking for updates', 'Please wait...');
const { appState } = useSettingsStore.getState();
const update = appState.update || (await codePush.checkForUpdate());
await waitDismissBurntAlerts();
if (!update) {
updateAppStateSlice({ update: null });
Alert.alert(
'No Update Available',
`There is no update available at this time. Please check the ${Platform.select({
ios: 'App',
android: 'Play',
})} Store periodically for new releases.`
);
return;
}
if (update.isMandatory) {
Alert.alert(
'Update Available',
'This update is mandatory and will restart the app and apply the update',
[
{
text: 'Install',
onPress: async () => {
await installUpdate(update);
},
isPreferred: true,
},
],
{ cancelable: false }
);
return;
}
Alert.alert(
'Update Available',
'Installing the update will restart the app and apply the update',
[
{
text: 'Install',
onPress: async () => {
await installUpdate(update);
},
},
{
style: 'destructive',
text: 'Cancel',
onPress: () => {
updateAppStateSlice({ update });
},
},
]
);
}
/**
* Installs the update with `UI` shown to the user to indicate each step
*/
async function installUpdate(update: RemotePackage) {
try {
showBurntLoading('Downloading update', 'Please wait...');
const download = await update.download();
Alert.alert(
'Update Downloaded',
`JSON.stringify(download): ${JSON.stringify(download, null, 2)}`
);
await waitDismissBurntAlerts();
showBurntLoading('Installing update', 'Please wait...');
await download.install(codePush.InstallMode.ON_NEXT_RESTART);
await codePush.notifyAppReady();
await waitDismissBurntAlerts();
Toast.Burnt.alert({
preset: 'done',
title: 'Update installed',
message: 'The app will now restart to apply the update',
duration: 2, // matches the wait time below to give the user time to read the alert
shouldDismissByTap: false,
});
await Wait(2000);
Toast.Burnt.dismissAllAlerts();
await Wait(300);
codePush.restartApp();
} catch (error) {
Toast.Burnt.dismissAllAlerts();
handleCodePushError(error);
}
}
/**
* Waits for the `Burnt` alerts to dismiss
* - first `wait` adds a `500ms` delay to allow the `Burnt` loading indicator more time to show
* - second `wait` adds a `300ms` delay to allow slight delay before next alert is shown
*/
async function waitDismissBurntAlerts() {
await Wait(500);
Toast.Burnt.dismissAllAlerts();
await Wait(300);
}
/**
* Wrapper around `Burnt` loading indicator
* - this config is used for each loading indicator shown to the user
* - `autoHide` is set to `false` to allow the loading indicator to stay on screen until we dismiss it
* - `shouldDismissByTap` is set to `false` to allow the loading indicator to stay on screen
*/
function showBurntLoading(title: string, message: string) {
Toast.Burnt.alert({
preset: 'spinner',
title,
message,
duration: 5,
autoHide: false,
shouldDismissByTap: false,
});
}
/**
* Handles errors from `codepush` and shows an alert to the user
* - will check to see if their is an `error.statusCode` and `error.message` to show to the user
* @param error - error from `codepush`
*/
function handleCodePushError(error: any) {
console.warn(`Error checking for updates: ${error}`);
if (error instanceof Error && 'statusCode' in error && 'message' in error) {
Alert.alert(
'Error Checking for Updates',
`There was an error checking for updates. Please try again later. ${
error?.statusCode ? `Error Code: ${error.statusCode}` : ''
}${error?.message ? `Error Message: ${error.message}` : ''}`
);
} else {
Alert.alert(
'Error Checking for Updates',
`There was an error checking for updates. Please try again later. Error ${JSON.stringify(
error,
null,
2
)}`
);
}
}
@mgscreativa - Hey man! been digging this past week trying to get codepush to work within my Expo managed project.
I have codepush setup as well through app center, and can also create and deploy codepush bundles to appcenter as well.
The steps i use to create a codepush update & test it:
package.json scripts:
Steps in order:
preview-stagingprofile for bothiosandandroidnpx expo prebuildto generate the ios and android folders needed locally for the codepush updateyarn update:staging:allto which i get successfull codepush bundles and releases to appcenterAndroid is not working at all for me when it comes to codepush updates - when trying through a manual approach, it would see that there is an update for the installed version, download it, then when trying to install it i would run into this error:
When it comes to iOS on both the manual approach & the silent approach through only the codepush decorator, it would download, install, and restart the app. After it restarted it would succesfully use the codepush bundle, but upon force closing the app and then re opening, it would not use the bundle anymore and be on the original js bundle included in the adhoc release.
Im using Expo sdk 49 with Expo Router with the
expo-dev-clientsetup.I have internal
adhoctesting setup as well where I can utilize thepreview-stagingbuild profiles that has also been working as expected, I have been able to continuously utilizeeas buildcommands to build new builds for testing throughpreview-stagingwith everything working.My current setup in eas.json.
I have an index.tsx file right now that looks like this:
codePushWrap is just a simple wrapper method that uses
codePushMy manual approach looks like this: