Skip to content

Instantly share code, notes, and snippets.

@a7ul
Last active January 4, 2023 04:40
Show Gist options
  • Save a7ul/49d8e854fe65ce0b8856cd702fff0686 to your computer and use it in GitHub Desktop.
Save a7ul/49d8e854fe65ce0b8856cd702fff0686 to your computer and use it in GitHub Desktop.
How to load custom codepush bundle for a react native app without using offical codepush servers? This can allow us to load code push bundles from anywhere like a gcs bucket or even local http server.
// How to use it ?
// ===============
// Step 1: Create a custom codepush bundle
// ----------------------------------------
// react-native bundle --assets-dest out --bundle-output out/main.jsbundle --dev false --platform ios --entry-file index.ts
// Then just compress the contents of out dir into a single zip file. for example: out.zip
// Step 2: Create a remotePackage json object
// ------------------------------------------
// const remotePackage = {
// description: "1.21.2", // optional description
// label: "v96", // optional label that we use
// appVersion: "1.18.0",
// packageHash:
// "7b9551b9482e78f094fdb2d19505b989517760d2f98d359c63c8ddcda686dbd4",
// downloadUrl: "http://192.168.0.102:8080/out.zip"
// };
// The packageHash can be created using `openssl dgst -sha256 out.zip`
// This remotePackage object can come to the app via a http call or even via a deep link or qr code scan ?
// Step 3: Call the forceCodepushCustomBundle function with the remotePackage
// --------------------------------------------------------------------------
// <Button onPress={() => forceCodepushCustomBundle(remotePackage)}>
// Update
// </Button>
// Step 4: Run react native in release mode
// ----------------------------------------
// react-native run-ios --configuration Release
import CodePush, { RemotePackage } from "react-native-code-push";
import { AcquisitionManager } from "code-push/script/acquisition-sdk";
import semver from "semver";
type CodePushUpdateCheckFunction = typeof AcquisitionManager.prototype.queryUpdateWithCurrentPackage;
const NULL = null as any;
async function getHijackedUpdateChecker(
currentAppVersion: string,
customRemotePackage: RemotePackage
): Promise<CodePushUpdateCheckFunction> {
// get the diff between currentPackage version and the remotePackage
const currentAndRequestedDiff = semver.diff(
currentAppVersion,
customRemotePackage.appVersion
);
return function customQueryUpdateWithCurrentPackage(
_,
callbackForRemotePackage = (_err, _remotePackage) => null // eslint-disable-line @typescript-eslint/no-unused-vars
) {
try {
// -------------------------
// This function will be called by CodePush.sync to check for updates.
// This function is expected to do a network call to the codepush servers with currentAppVersion and then do a callback with the returned metadata or error
// The metadata is nothing but RemotePackage object that contains the bundle url, packageHash (SHA256 hash of the zip bundle) and other details.
// CodePush.sync after calling this function will do check if currently running packageHash equals that of returned remotePackage packageHash.
// If they dont match it will just download and update. Hence this function is also responsible for checking if an update should be allowed or not too.
// ----------------------------
const shouldUpdate = ["patch", "prepatch", "prerelease"].includes(
`${currentAndRequestedDiff}`
);
if (shouldUpdate) {
callbackForRemotePackage(NULL, customRemotePackage);
} else {
callbackForRemotePackage(NULL, NULL);
}
} catch (err) {
callbackForRemotePackage(err, NULL);
}
};
}
export async function forceCodepushCustomBundle(
customRemotePackage: RemotePackage
) {
const CodePushInternal = CodePush as any;
const currentConfig = await CodePushInternal.getConfiguration();
const AcquisitionSDK: AcquisitionManager =
CodePushInternal.AcquisitionSdk.prototype;
// Step1: Backup original functions
// --------------------------------
const _originalReportStatusDownload = AcquisitionSDK.reportStatusDownload;
const _originalReportStatusDeploy = AcquisitionSDK.reportStatusDeploy;
const _originalQueryUpdateWithCurrentPackage =
AcquisitionSDK.queryUpdateWithCurrentPackage;
try {
// Step2: Hijack codepush update check functions
// ---------------------------------------------
AcquisitionSDK.reportStatusDownload = (...args) =>
console.log("DUMMY reportStatusDownload: ", ...args);
AcquisitionSDK.reportStatusDeploy = (...args) =>
console.log("DUMMY reportStatusDeploy: ", ...args);
AcquisitionSDK.queryUpdateWithCurrentPackage = await getHijackedUpdateChecker(
currentConfig.appVersion,
customRemotePackage
);
// Step3: Hijacking complete. Now just run the CodePush sync and it will use our hijacked sdk
// ------------------------------------------------------------------------------------------
// Forcefully allow to restart and sync
CodePush.allowRestart();
await CodePush.sync({
updateDialog: { title: "Update" },
installMode: CodePush.InstallMode.IMMEDIATE
});
} catch (err) {
// TODO: Failed to update. Log or show alert here ?
console.log(err);
} finally {
// Step4: Restore codepush update metadata functions.
// ---------------------------------------------------------------------------------
AcquisitionSDK.reportStatusDownload = _originalReportStatusDownload;
AcquisitionSDK.reportStatusDeploy = _originalReportStatusDeploy;
AcquisitionSDK.queryUpdateWithCurrentPackage = _originalQueryUpdateWithCurrentPackage;
}
}
// TODO:
/*
Apart from the TODO on the code here
We need to maybe change the codepush auto update in the app to manual for staging or come up with some other solution
to basically not allow this update to be overriden after the app restarts
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment