cap-codepush
Version:
CodePush Plugin for Capacitor. Working with Capacitor 7.
539 lines (489 loc) • 25.2 kB
text/typescript
import { AcquisitionStatus, NativeUpdateNotification } from "code-push/script/acquisition-sdk";
import { Callback, ErrorCallback, SuccessCallback } from "./callbackUtil";
import { CodePushUtil } from "./codePushUtil";
import { InstallMode } from "./installMode";
import { LocalPackage } from "./localPackage";
import { NativeAppInfo } from "./nativeAppInfo";
import { CodePush as NativeCodePush } from "./nativeCodePushPlugin";
import { DownloadProgress, ILocalPackage, IPackage, IRemotePackage } from "./package";
import { RemotePackage } from "./remotePackage";
import { Sdk } from "./sdk";
import { SyncOptions, UpdateDialogOptions } from "./syncOptions";
import { SyncStatus } from "./syncStatus";
import { ConfirmResult, Dialog } from "@capacitor/dialog";
interface CodePushCapacitorPlugin {
/**
* Get the current package information.
*
* @returns The currently deployed package information.
*/
getCurrentPackage(): Promise<ILocalPackage>;
/**
* Gets the pending package information, if any. A pending package is one that has been installed but the application still runs the old code.
* This happens only after a package has been installed using ON_NEXT_RESTART or ON_NEXT_RESUME mode, but the application was not restarted/resumed yet.
*/
getPendingPackage(): Promise<ILocalPackage>;
/**
* Checks with the CodePush server if an update package is available for download.
*
* @param querySuccess Callback invoked in case of a successful response from the server.
* The callback takes one RemotePackage parameter. A non-null package is a valid update.
* A null package means the application is up to date for the current native application version.
* @param queryError Optional callback invoked in case of an error.
* @param deploymentKey Optional deployment key that overrides the config.xml setting.
*/
checkForUpdate(querySuccess: SuccessCallback<IRemotePackage>, queryError?: ErrorCallback, deploymentKey?: string): void;
/**
* Notifies the plugin that the update operation succeeded and that the application is ready.
* Calling this function is required on the first run after an update. On every subsequent application run, calling this function is a noop.
* If using sync API, calling this function is not required since sync calls it internally.
*/
notifyApplicationReady(): Promise<void>;
/**
* Reloads the application. If there is a pending update package installed using ON_NEXT_RESTART or ON_NEXT_RESUME modes, the update
* will be immediately visible to the user. Otherwise, calling this function will simply reload the current version of the application.
*/
restartApplication(): Promise<void>;
/**
* Convenience method for installing updates in one method call.
* This method is provided for simplicity, and its behavior can be replicated by using window.codePush.checkForUpdate(), RemotePackage's download() and LocalPackage's install() methods.
*
* The algorithm of this method is the following:
* - Checks for an update on the CodePush server.
* - If an update is available
* - If the update is mandatory and the alertMessage is set in options, the user will be informed that the application will be updated to the latest version.
* The update package will then be downloaded and applied.
* - If the update is not mandatory and the confirmMessage is set in options, the user will be asked if they want to update to the latest version.
* If they decline, the syncCallback will be invoked with SyncStatus.UPDATE_IGNORED.
* - Otherwise, the update package will be downloaded and applied with no user interaction.
* - If no update is available on the server, or if a previously rolled back update is available and the ignoreFailedUpdates is set to true, the syncCallback will be invoked with the SyncStatus.UP_TO_DATE.
* - If an error occurs during checking for update, downloading or installing it, the syncCallback will be invoked with the SyncStatus.ERROR.
*
* @param syncOptions Optional SyncOptions parameter configuring the behavior of the sync operation.
* @param downloadProgress Optional callback invoked during the download process. It is called several times with one DownloadProgress parameter.
* @returns The status of the sync operation. The possible statuses are defined by the SyncStatus enum.
*
*/
sync(syncOptions?: SyncOptions, downloadProgress?: SuccessCallback<DownloadProgress>): Promise<SyncStatus>;
}
/**
* This is the entry point to Cordova CodePush SDK.
* It provides the following features to the app developer:
* - polling the server for new versions of the app
* - notifying the plugin that the application loaded successfully after an update
* - getting information about the currently deployed package
*/
class CodePush implements CodePushCapacitorPlugin {
/**
* The default options for the sync command.
*/
private static DefaultSyncOptions: SyncOptions;
/**
* The default UI for the update dialog, in case it is enabled.
* Please note that the update dialog is disabled by default.
*/
private static DefaultUpdateDialogOptions: UpdateDialogOptions;
/**
* Whether or not a sync is currently in progress.
*/
private static SyncInProgress: boolean;
/**
* Notifies the plugin that the update operation succeeded and that the application is ready.
* Calling this function is required on the first run after an update. On every subsequent application run, calling this function is a noop.
* If using sync API, calling this function is not required since sync calls it internally.
*/
public notifyApplicationReady(): Promise<void> {
return NativeCodePush.notifyApplicationReady();
}
/**
* Reloads the application. If there is a pending update package installed using ON_NEXT_RESTART or ON_NEXT_RESUME modes, the update
* will be immediately visible to the user. Otherwise, calling this function will simply reload the current version of the application.
*/
public restartApplication(): Promise<void> {
return NativeCodePush.restartApplication();
}
/**
* Reports an application status back to the server.
* !!! This function is called from the native side, please make changes accordingly. !!!
*/
public reportStatus(status: number, label: string, appVersion: string, deploymentKey: string, lastVersionLabelOrAppVersion?: string, lastVersionDeploymentKey?: string) {
if (((!label && appVersion === lastVersionLabelOrAppVersion) || label === lastVersionLabelOrAppVersion)
&& deploymentKey === lastVersionDeploymentKey) {
// No-op since the new appVersion and label is exactly the same as the previous
// (the app might have been updated via a direct or HockeyApp deployment).
return;
}
var createPackageForReporting = (label: string, appVersion: string): IPackage => {
return {
/* The SDK only reports the label and appVersion.
The rest of the properties are added for type safety. */
label, appVersion, deploymentKey,
description: null, isMandatory: false,
packageHash: null, packageSize: null,
failedInstall: false
};
};
var reportDone = (error: Error) => {
var reportArgs = {
status,
label,
appVersion,
deploymentKey,
lastVersionLabelOrAppVersion,
lastVersionDeploymentKey
};
if (error) {
CodePushUtil.logError(`An error occurred while reporting status: ${JSON.stringify(reportArgs)}`, error);
NativeCodePush.reportFailed({ statusReport: reportArgs });
} else {
CodePushUtil.logMessage(`Reported status: ${JSON.stringify(reportArgs)}`);
NativeCodePush.reportSucceeded({ statusReport: reportArgs });
}
};
switch (status) {
case ReportStatus.STORE_VERSION:
Sdk.reportStatusDeploy(null, AcquisitionStatus.DeploymentSucceeded, deploymentKey, lastVersionLabelOrAppVersion, lastVersionDeploymentKey, reportDone);
break;
case ReportStatus.UPDATE_CONFIRMED:
Sdk.reportStatusDeploy(createPackageForReporting(label, appVersion), AcquisitionStatus.DeploymentSucceeded, deploymentKey, lastVersionLabelOrAppVersion, lastVersionDeploymentKey, reportDone);
break;
case ReportStatus.UPDATE_ROLLED_BACK:
Sdk.reportStatusDeploy(createPackageForReporting(label, appVersion), AcquisitionStatus.DeploymentFailed, deploymentKey, lastVersionLabelOrAppVersion, lastVersionDeploymentKey, reportDone);
break;
}
}
/**
* Get the current package information.
*
* @returns The currently deployed package information.
*/
public async getCurrentPackage(): Promise<ILocalPackage> {
const pendingUpdate = await NativeAppInfo.isPendingUpdate();
var packageInfoFile = pendingUpdate ? LocalPackage.OldPackageInfoFile : LocalPackage.PackageInfoFile;
return new Promise<ILocalPackage>((resolve, reject) => {
LocalPackage.getPackageInfoOrNull(packageInfoFile, resolve as any, reject);
});
}
/**
* Gets the pending package information, if any. A pending package is one that has been installed but the application still runs the old code.
* This happens only after a package has been installed using ON_NEXT_RESTART or ON_NEXT_RESUME mode, but the application was not restarted/resumed yet.
*/
public async getPendingPackage(): Promise<ILocalPackage> {
const pendingUpdate = await NativeAppInfo.isPendingUpdate();
if (!pendingUpdate) return null;
return new Promise<ILocalPackage>((resolve, reject) => {
LocalPackage.getPackageInfoOrNull(LocalPackage.PackageInfoFile, resolve as any, reject);
});
}
/**
* Checks with the CodePush server if an update package is available for download.
*
* @param querySuccess Callback invoked in case of a successful response from the server.
* The callback takes one RemotePackage parameter. A non-null package is a valid update.
* A null package means the application is up to date for the current native application version.
* @param queryError Optional callback invoked in case of an error.
* @param deploymentKey Optional deployment key that overrides the config.xml setting.
*/
public checkForUpdate(querySuccess: SuccessCallback<IRemotePackage>, queryError?: ErrorCallback, deploymentKey?: string): void {
try {
const callback: Callback<RemotePackage | NativeUpdateNotification> = async (error: Error, remotePackageOrUpdateNotification: IRemotePackage | NativeUpdateNotification) => {
if (error) {
CodePushUtil.invokeErrorCallback(error, queryError);
} else {
const appUpToDate = () => {
CodePushUtil.logMessage("App is up to date.");
querySuccess && querySuccess(null);
};
if (remotePackageOrUpdateNotification) {
if ((<NativeUpdateNotification>remotePackageOrUpdateNotification).updateAppVersion) {
/* There is an update available for a different version. In the current version of the plugin, we treat that as no update. */
CodePushUtil.logMessage("An update is available, but it is targeting a newer binary version than you are currently running.");
appUpToDate();
} else {
/* There is an update available for the current version. */
var remotePackage: RemotePackage = <RemotePackage>remotePackageOrUpdateNotification;
const installFailed = await NativeAppInfo.isFailedUpdate(remotePackage.packageHash);
var result: RemotePackage = new RemotePackage();
result.appVersion = remotePackage.appVersion;
result.deploymentKey = deploymentKey; // server does not send back the deployment key
result.description = remotePackage.description;
result.downloadUrl = remotePackage.downloadUrl;
result.isMandatory = remotePackage.isMandatory;
result.label = remotePackage.label;
result.packageHash = remotePackage.packageHash;
result.packageSize = remotePackage.packageSize;
result.failedInstall = installFailed;
CodePushUtil.logMessage("An update is available. " + JSON.stringify(result));
querySuccess && querySuccess(result);
}
} else {
appUpToDate();
}
}
};
const queryUpdate = async () => {
try {
const acquisitionManager = await Sdk.getAcquisitionManager(deploymentKey);
const localPackage = await LocalPackage.getCurrentOrDefaultPackage();
try {
const currentBinaryVersion = await NativeAppInfo.getApplicationVersion();
localPackage.appVersion = currentBinaryVersion;
} catch (e) {
/* Nothing to do */
/* TODO : Why ? */
}
CodePushUtil.logMessage("Checking for update.");
acquisitionManager.queryUpdateWithCurrentPackage(localPackage, callback);
} catch (e) {
CodePushUtil.invokeErrorCallback(e, queryError);
}
};
if (deploymentKey) {
queryUpdate();
} else {
NativeAppInfo.getDeploymentKey()
.then(
(defaultDeploymentKey) => {
deploymentKey = defaultDeploymentKey;
queryUpdate();
},
(deploymentKeyError) => {
CodePushUtil.invokeErrorCallback(deploymentKeyError, queryError);
}
);
}
} catch (e) {
CodePushUtil.invokeErrorCallback(new Error("An error occurred while querying for updates." + CodePushUtil.getErrorMessage(e)), queryError);
}
}
/**
* Convenience method for installing updates in one method call.
* This method is provided for simplicity, and its behavior can be replicated by using window.codePush.checkForUpdate(), RemotePackage's download() and LocalPackage's install() methods.
* If another sync is already running, it yields SyncStatus.IN_PROGRESS.
*
* The algorithm of this method is the following:
* - Checks for an update on the CodePush server.
* - If an update is available
* - If the update is mandatory and the alertMessage is set in options, the user will be informed that the application will be updated to the latest version.
* The update package will then be downloaded and applied.
* - If the update is not mandatory and the confirmMessage is set in options, the user will be asked if they want to update to the latest version.
* If they decline, the syncCallback will be invoked with SyncStatus.UPDATE_IGNORED.
* - Otherwise, the update package will be downloaded and applied with no user interaction.
* - If no update is available on the server, the syncCallback will be invoked with the SyncStatus.UP_TO_DATE.
* - If an error occurs during checking for update, downloading or installing it, the syncCallback will be invoked with the SyncStatus.ERROR.
*
* @param syncOptions Optional SyncOptions parameter configuring the behavior of the sync operation.
* @param downloadProgress Optional callback invoked during the download process. It is called several times with one DownloadProgress parameter.
*/
public async sync(syncOptions?: SyncOptions, downloadProgress?: SuccessCallback<DownloadProgress>): Promise<SyncStatus> {
return await new Promise(
(resolve, reject) => {
/* Check if a sync is already in progress */
if (CodePush.SyncInProgress) {
/* A sync is already in progress */
CodePushUtil.logMessage("Sync already in progress.");
resolve(SyncStatus.IN_PROGRESS);
}
/* Create a callback that resets the SyncInProgress flag when the sync is complete
* If the sync status is a result status, then the sync must be complete and the flag must be updated
* Otherwise, do not change the flag and trigger the syncCallback as usual
*/
const syncCallbackAndUpdateSyncInProgress: Callback<SyncStatus> = (err: Error | null, result: SyncStatus | null): void => {
if (err) {
syncOptions.onSyncError && syncOptions.onSyncError(err);
CodePush.SyncInProgress = false;
reject(err);
} else {
/* Call the user's callback */
syncOptions.onSyncStatusChanged && syncOptions.onSyncStatusChanged(result);
/* Check if the sync operation is over */
switch (result) {
case SyncStatus.ERROR:
case SyncStatus.UP_TO_DATE:
case SyncStatus.UPDATE_IGNORED:
case SyncStatus.UPDATE_INSTALLED:
/* The sync has completed */
CodePush.SyncInProgress = false;
resolve(result);
break;
default:
/* The sync is not yet complete, so do nothing */
break;
}
}
};
/* Begin the sync */
CodePush.SyncInProgress = true;
this.syncInternal(
syncCallbackAndUpdateSyncInProgress,
syncOptions,
downloadProgress,
);
}
);
}
/**
* Convenience method for installing updates in one method call.
* This method is provided for simplicity, and its behavior can be replicated by using window.codePush.checkForUpdate(), RemotePackage's download() and LocalPackage's install() methods.
*
* A helper function for the sync function. It does not check if another sync is ongoing.
*
* @param syncCallback Optional callback to be called with the status of the sync operation.
* The callback will be called only once, and the possible statuses are defined by the SyncStatus enum.
* @param syncOptions Optional SyncOptions parameter configuring the behavior of the sync operation.
* @param downloadProgress Optional callback invoked during the download process. It is called several times with one DownloadProgress parameter.
*
*/
private syncInternal(syncCallback?: Callback<any>, syncOptions?: SyncOptions, downloadProgress?: SuccessCallback<DownloadProgress>): void {
/* No options were specified, use default */
const defaultSyncOptions = this.getDefaultSyncOptions();
if (!syncOptions) {
syncOptions = defaultSyncOptions;
} else {
/* Some options were specified */
/* Handle dialog options */
const defaultDialogOptions = this.getDefaultUpdateDialogOptions();
if (syncOptions.updateDialog) {
if (typeof syncOptions.updateDialog !== typeof ({})) {
/* updateDialog set to true condition, use default options */
syncOptions.updateDialog = defaultDialogOptions;
} else {
/* some options were specified, merge with default */
CodePushUtil.copyUnassignedMembers(defaultDialogOptions, syncOptions.updateDialog);
}
}
/* Handle other options. Dialog options will not be overwritten. */
CodePushUtil.copyUnassignedMembers(defaultSyncOptions, syncOptions);
}
this.notifyApplicationReady();
const onError = (error: Error) => {
CodePushUtil.logError("An error occurred during sync.", error);
syncCallback && syncCallback(error, SyncStatus.ERROR);
};
const onInstallSuccess = (appliedWhen: InstallMode) => {
switch (appliedWhen) {
case InstallMode.ON_NEXT_RESTART:
CodePushUtil.logMessage("Update is installed and will be run on the next app restart.");
break;
case InstallMode.ON_NEXT_RESUME:
if (syncOptions.minimumBackgroundDuration > 0) {
CodePushUtil.logMessage(`Update is installed and will be run after the app has been in the background for at least ${syncOptions.minimumBackgroundDuration} seconds.`);
} else {
CodePushUtil.logMessage("Update is installed and will be run when the app next resumes.");
}
break;
}
syncCallback && syncCallback(null, SyncStatus.UPDATE_INSTALLED);
};
const onDownloadSuccess = (localPackage: ILocalPackage) => {
syncCallback && syncCallback(null, SyncStatus.INSTALLING_UPDATE);
localPackage.install(syncOptions).then(onInstallSuccess, onError);
};
const downloadAndInstallUpdate = (remotePackage: RemotePackage) => {
syncCallback && syncCallback(null, SyncStatus.DOWNLOADING_PACKAGE);
remotePackage.download(downloadProgress).then(onDownloadSuccess, onError);
};
const onUpdate = async (remotePackage: RemotePackage) => {
if (remotePackage === null) {
/* Then the app is up to date */
syncCallback && syncCallback(null, SyncStatus.UP_TO_DATE);
} else {
if (remotePackage.failedInstall && syncOptions.ignoreFailedUpdates) {
CodePushUtil.logMessage("An update is available, but it is being ignored due to have been previously rolled back.");
syncCallback && syncCallback(null, SyncStatus.UPDATE_IGNORED);
} else {
if (syncOptions.updateDialog) {
CodePushUtil.logMessage("Awaiting user action.");
syncCallback && syncCallback(null, SyncStatus.AWAITING_USER_ACTION);
const dlgOpts: UpdateDialogOptions = <UpdateDialogOptions>syncOptions.updateDialog;
if (remotePackage.isMandatory) {
/* Alert user */
const message = dlgOpts.appendReleaseDescription ?
dlgOpts.mandatoryUpdateMessage + dlgOpts.descriptionPrefix + remotePackage.description :
dlgOpts.mandatoryUpdateMessage;
await Dialog.alert(
{
message,
title: dlgOpts.updateTitle,
buttonTitle: dlgOpts.mandatoryContinueButtonLabel
}
);
downloadAndInstallUpdate(remotePackage);
} else {
/* Confirm update with user */
const message = dlgOpts.appendReleaseDescription ?
dlgOpts.optionalUpdateMessage + dlgOpts.descriptionPrefix + remotePackage.description
: dlgOpts.optionalUpdateMessage;
const confirmResult: ConfirmResult = await Dialog.confirm({
message,
title: dlgOpts.updateTitle,
okButtonTitle: dlgOpts.optionalInstallButtonLabel,
cancelButtonTitle: dlgOpts.optionalIgnoreButtonLabel
});
if (confirmResult.value === true) {
/* Install */
downloadAndInstallUpdate(remotePackage);
} else {
/* Cancel */
CodePushUtil.logMessage("User cancelled the update.");
syncCallback && syncCallback(null, SyncStatus.UPDATE_IGNORED);
}
}
} else {
/* No user interaction */
downloadAndInstallUpdate(remotePackage);
}
}
}
};
syncCallback && syncCallback(null, SyncStatus.CHECKING_FOR_UPDATE);
this.checkForUpdate(onUpdate, onError, syncOptions.deploymentKey);
}
/**
* Returns the default options for the CodePush sync operation.
* If the options are not defined yet, the static DefaultSyncOptions member will be instantiated.
*/
private getDefaultSyncOptions(): SyncOptions {
if (!CodePush.DefaultSyncOptions) {
CodePush.DefaultSyncOptions = {
ignoreFailedUpdates: true,
installMode: InstallMode.ON_NEXT_RESTART,
minimumBackgroundDuration: 0,
mandatoryInstallMode: InstallMode.IMMEDIATE,
updateDialog: false,
deploymentKey: undefined
};
}
return CodePush.DefaultSyncOptions;
}
/**
* Returns the default options for the update dialog.
* Please note that the dialog is disabled by default.
*/
private getDefaultUpdateDialogOptions(): UpdateDialogOptions {
if (!CodePush.DefaultUpdateDialogOptions) {
CodePush.DefaultUpdateDialogOptions = {
updateTitle: "Update available",
mandatoryUpdateMessage: "An update is available that must be installed.",
mandatoryContinueButtonLabel: "Continue",
optionalUpdateMessage: "An update is available. Would you like to install it?",
optionalInstallButtonLabel: "Install",
optionalIgnoreButtonLabel: "Ignore",
appendReleaseDescription: false,
descriptionPrefix: " Description: "
};
}
return CodePush.DefaultUpdateDialogOptions;
}
}
/**
* Defines the application statuses reported from the native layer.
* !!! This enum is defined in native code as well, please make changes accordingly. !!!
*/
enum ReportStatus {
STORE_VERSION = 0,
UPDATE_CONFIRMED = 1,
UPDATE_ROLLED_BACK = 2
}
export const codePush = new CodePush();
(window as any).codePush = codePush;