Closed mvolonnino closed 10 months ago
@mgscreativa - i know this is a lot of information but wasnt sure if you had any insights for me or not, as I see your example utilizes expo and expo router too!
Banging my head against a wall right now trying to figure it out, so anything could be helpful 🙌
@mgscreativa - I should note, after iOS silent codepush successful update and app suspend that did apply the codepush bundle - then after force closing the app to see if it would still be applied and wasnt, when I try my manual approach (through a button in a drawer component), it hits the no update available showing to me that the app does have the update downloaded already?
Hi! that's really strange because upon update, the bundle gets overwritten. The best approach I suggest is that you download and compile test apps with my project and try to replicate the issue, If you can't then there's an issue with your code.
Another thing I notice is that if you see ios an android folders in your project, then it's not an expo managed project. This example is an Expo managed app that uses a dev build to work
Another thing I notice is in your codepush build command, I think you missed --use-hermes
and --target-binary-version
, please check https://github.com/mgscreativa/react-native-code-push-expo-plugin-managed-workflow#send-a-bundle-update-release
@mgscreativa - so with the prebuild command I am doing and the --plist-file ios/App/Info.plist
attached to the script, it grabs the correct target binary automatically and creates the bundles pointed towards what is in the app.json
.
I will definitely try adding the --use-hermes
as well.
Another thing I notice is that if you see ios an android folders in your project, then it's not an expo managed project. This example is an Expo managed app that uses a dev build to work
In terms of this, how do you go about creating a build that does not have the devClient: true
, as if that is the case, to my knowledge - codepush bundle is not used and instead the dev clients bundle is always used with hot reload?
Currently I am using eas build --profile preview-staging --platform all
which creates a build that does not utilize the devClient
to be able to then test a codepush bundle on a 'release' mode of the app (with android its just the apk i download on my android device which is the standalone app, and iOS utilizing the adhoc
internal distro for my own device to also get the standalone app to test as well.
Am I off basis here in my thinking?
I guess a better way to state this, I would not really need codepush OTA on dev client builds as that is where I currently do development, I would need them for the production/beta builds that are distributed through the app stores & TestFlight (for my QA testers) - which in my thought process would be the same as a standalone
app which is what my preview-
profiles in the eas.json
creates for me.
Okay so adding the --use-hermes
does not help either, still on iOS, it does download and install correctly to which after that first reload, shows the codepush bundle, but then after the next restart, just reverts back to the original bundle
Upon further testing, the bundle does correctly install like ive said, I have methods setup to attach on the codepush label in the version im running to be able to tell which codepush update the app is running. That all works just fine on iOS, once I close out the app, the I have this method to run:
import { useEffect } from 'react';
import { Alert, AppState } from 'react-native';
import moment from 'moment-timezone';
import { useSettingsStore } from 'stores';
import codePush from 'updates/codePush';
import checkForUpdates, { UpdateCheck } from '../checkForUpdates';
const { updateAppStateSlice } = useSettingsStore.getState();
/**
* Checks to see if the last check time is more than an hour ago
* - rate limit of 8 request per 5 mins
* @see https://learn.microsoft.com/en-us/appcenter/distribution/codepush/
*/
const hasEnoughHoursPast = (lastCheckTime: number | null, numHr: number) => {
if (!lastCheckTime) return true;
return moment().diff(moment(lastCheckTime), 'hours') >= numHr;
};
/**
* Hook that checks for updates on app state change
* - this is used to check for updates when the app is in `active` state
* - defaults to `UpdateCheck.Silent` which should only show an indicator that an update is available to not interrupt the user (we do not want to interrupt the user if they are in the middle of something, i.e registering for a program, etc)
*
* @default UpdateCheck.Silent
*/
const useCheckForUpdates = (type: UpdateCheck = UpdateCheck.Silent) => {
useEffect(() => {
const listener = AppState.addEventListener('change', nextAppState => {
const { lastSilentUpdateCheckTime } = useSettingsStore.getState().appState;
const shouldCheckForUpdates =
nextAppState === 'active' && hasEnoughHoursPast(lastSilentUpdateCheckTime, 1);
if (shouldCheckForUpdates) {
console.log(`[useCheckForUpdates][CodePush] Checking for updates`);
(async () => {
await checkForUpdates(type);
updateAppStateSlice({ lastSilentUpdateCheckTime: moment().valueOf() });
})();
}
});
return () => {
listener.remove();
};
}, [type]);
/**
* This checks to see if the app is now running on a new version that was just installed from codepush
* - will update the `codePushLabel` in the app state to reflect the new version, and clear the `update` from the app state since we are now running on the latest update from codepush or version from the app store
*/
useEffect(() => {
(async () => {
const LocalPackage = await codePush.getUpdateMetadata();
Alert.alert('LocalPackage', JSON.stringify(LocalPackage));
if (LocalPackage?.isFirstRun) {
console.log(
`[useCheckForUpdates][CodePush] Update Metadata: ${JSON.stringify(LocalPackage)}`
);
const { label } = LocalPackage;
updateAppStateSlice({ codePushLabel: label });
} else if (!LocalPackage) {
console.log(
`[useCheckForUpdates][CodePush] No update metadata found, running on latest released version`
);
updateAppStateSlice({ codePushLabel: null });
}
})();
}, []);
};
export default useCheckForUpdates;
And the LocalPackage within the second useEffect does show in the Alert.alert, which shows some keys points that all look correct to me:
{
isPending: false,
packageSize: 2337248,
appVersion: '2.2.9',
packageHash: 'fakehash',
downloadUrl: 'fakeurl',
failedInstall: false,
isFirstRun: false,
}
But like I said, its not running the codepush bundle, its the original bundle
I don't really know why that happens to you, I have it working in several production apps Expo managed, and locally compiled with EAS. This demo is based on this PR https://github.com/microsoft/react-native-code-push/pull/2415 from @deggertsen, you can take a look at the PR and update my test project files just to check.
Another check you can do is to use this repo and compile it and send it to test flight to see if the modified bundle hits the app after restart. If it does, then there's an issue with your project.
Damn that is wild, I wonder what are your steps for building a standalone app? From your commands it looks like you do it locally - have you ever tried using eas build
for it to be built in their cloud workers?
Trying to not have to do local builds and need to involve xcode & android studio with the new workflow
Local builds are not hard at all, you just need to setup the dev environment for iOS and android and EAS does the rest.
Didn't tried EAS cloud build, but believe me, local builds are the same and the error you account for is not related to the build process, because it will fail otherwise
@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-staging
profile for bothios
andandroid
npx expo prebuild
to generate the ios and android folders needed locally for the codepush updateyarn update:staging:all
to 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-client
setup.I have internal
adhoc
testing setup as well where I can utilize thepreview-staging
build profiles that has also been working as expected, I have been able to continuously utilizeeas build
commands to build new builds for testing throughpreview-staging
with 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
codePush
/**
codePush
wrapperon start up
to check for updatescheckFrequency
- 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, };/**
export default codePushWrap;
/ 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) { /**
/**
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;
/**
codepush
server withalerts
shown to the user@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); } }
/**
Burnt
alerts to dismisswait
adds a500ms
delay to allow theBurnt
loading indicator more time to showwait
adds a300ms
delay to allow slight delay before next alert is shown */ async function waitDismissBurntAlerts() { await Wait(500); Toast.Burnt.dismissAllAlerts(); await Wait(300); }/**
Burnt
loading indicatorautoHide
is set tofalse
to allow the loading indicator to stay on screen until we dismiss itshouldDismissByTap
is set tofalse
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, }); }/**
codepush
and shows an alert to the usererror.statusCode
anderror.message
to show to the usercodepush
*/ 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 )}
); } }