/**
 * Failed to minify the file using Terser v5.39.0. Serving the original version.
 * Original file: /npm/@itenl/react-native-code-push@0.0.6/CodePush.js
 *
 * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
 */
import { AcquisitionManager as Sdk } from 'code-push/script/acquisition-sdk';
import { Alert } from './AlertAdapter';
import requestFetchAdapter from './request-fetch-adapter';
import { AppState, Platform } from 'react-native';
import RestartManager from './RestartManager';
import log from './logging';
import hoistStatics from 'hoist-non-react-statics';

let NativeCodePush = require('react-native').NativeModules.CodePush;
const PackageMixins = require('./package-mixins')(NativeCodePush);

async function checkForUpdateByOrigin(deploymentKey = null, handleBinaryVersionMismatchCallback = null) {
  /*
   * Before we ask the server if an update exists, we
   * need to retrieve three pieces of information from the
   * native side: deployment key, app version (e.g. 1.0.1)
   * and the hash of the currently running update (if there is one).
   * This allows the client to only receive updates which are targetted
   * for their specific deployment and version and which are actually
   * different from the CodePush update they have already installed.
   */
  const nativeConfig = await getConfiguration();
  /*
   * If a deployment key was explicitly provided,
   * then let's override the one we retrieved
   * from the native-side of the app. This allows
   * dynamically "redirecting" end-users at different
   * deployments (e.g. an early access deployment for insiders).
   */
  const config = deploymentKey ? { ...nativeConfig, ...{ deploymentKey } } : nativeConfig;
  const sdk = getPromisifiedSdk(requestFetchAdapter, config);

  // Use dynamically overridden getCurrentPackage() during tests.
  const localPackage = await module.exports.getCurrentPackage();

  /*
   * If the app has a previously installed update, and that update
   * was targetted at the same app version that is currently running,
   * then we want to use its package hash to determine whether a new
   * release has been made on the server. Otherwise, we only need
   * to send the app version to the server, since we are interested
   * in any updates for current binary version, regardless of hash.
   */
  let queryPackage;
  if (localPackage) {
    queryPackage = localPackage;
  } else {
    queryPackage = { appVersion: config.appVersion };
    if (Platform.OS === 'ios' && config.packageHash) {
      queryPackage.packageHash = config.packageHash;
    }
  }

  const update = await sdk.queryUpdateWithCurrentPackage(queryPackage);

  /*
   * There are four cases where checkForUpdate will resolve to null:
   * ----------------------------------------------------------------
   * 1) The server said there isn't an update. This is the most common case.
   * 2) The server said there is an update but it requires a newer binary version.
   *    This would occur when end-users are running an older binary version than
   *    is available, and CodePush is making sure they don't get an update that
   *    potentially wouldn't be compatible with what they are running.
   * 3) The server said there is an update, but the update's hash is the same as
   *    the currently running update. This should _never_ happen, unless there is a
   *    bug in the server, but we're adding this check just to double-check that the
   *    client app is resilient to a potential issue with the update check.
   * 4) The server said there is an update, but the update's hash is the same as that
   *    of the binary's currently running version. This should only happen in Android -
   *    unlike iOS, we don't attach the binary's hash to the updateCheck request
   *    because we want to avoid having to install diff updates against the binary's
   *    version, which we can't do yet on Android.
   */
  if (!update || update.updateAppVersion || (localPackage && update.packageHash === localPackage.packageHash) || ((!localPackage || localPackage._isDebugOnly) && config.packageHash === update.packageHash)) {
    if (update && update.updateAppVersion) {
      log('An update is available but it is not targeting the binary version of your app.');
      if (handleBinaryVersionMismatchCallback && typeof handleBinaryVersionMismatchCallback === 'function') {
        handleBinaryVersionMismatchCallback(update);
      }
    }

    return null;
  } else {
    const remotePackage = { ...update, ...PackageMixins.remote(sdk.reportStatusDownload) };
    remotePackage.failedInstall = await NativeCodePush.isFailedUpdate(remotePackage.packageHash);
    remotePackage.deploymentKey = deploymentKey || nativeConfig.deploymentKey;
    return remotePackage;
  }
}

async function checkForUpdate() {
  if (arguments.length == 1 && typeof arguments[0] == 'object') {
    let options = arguments[0] || {};
    const syncOptions = {
      deploymentKey: null,
      ignoreFailedUpdates: true,
      rollbackRetryOptions: null,
      handleBinaryVersionMismatchCallback: null,
      ...options
    };
    try {
      let remotePackage = await checkForUpdateByOrigin(syncOptions.deploymentKey, syncOptions.handleBinaryVersionMismatchCallback);
      if (remotePackage) remotePackage.shouldUpdateBeIgnored = await shouldUpdateBeIgnored(remotePackage, syncOptions);
      return remotePackage;
    } catch (error) {
      log(error.message);
      return null;
    }
  } else {
    return checkForUpdateByOrigin.apply([...arguments]);
  }
}

const getConfiguration = (() => {
  let config;
  return async function getConfiguration() {
    if (config) {
      return config;
    } else if (testConfig) {
      return testConfig;
    } else {
      config = await NativeCodePush.getConfiguration();
      return config;
    }
  };
})();

async function getCurrentPackage() {
  return await getUpdateMetadata(CodePush.UpdateState.LATEST);
}

async function getUpdateMetadata(updateState) {
  let updateMetadata = await NativeCodePush.getUpdateMetadata(updateState || CodePush.UpdateState.RUNNING);
  if (updateMetadata) {
    updateMetadata = { ...PackageMixins.local, ...updateMetadata };
    updateMetadata.failedInstall = await NativeCodePush.isFailedUpdate(updateMetadata.packageHash);
    updateMetadata.isFirstRun = await NativeCodePush.isFirstRun(updateMetadata.packageHash);
  }
  return updateMetadata;
}

function getPromisifiedSdk(requestFetchAdapter, config) {
  // Use dynamically overridden AcquisitionSdk during tests.
  const sdk = new module.exports.AcquisitionSdk(requestFetchAdapter, config);
  sdk.queryUpdateWithCurrentPackage = queryPackage => {
    return new Promise((resolve, reject) => {
      module.exports.AcquisitionSdk.prototype.queryUpdateWithCurrentPackage.call(sdk, queryPackage, (err, update) => {
        if (err) {
          reject(err);
        } else {
          resolve(update);
        }
      });
    });
  };

  sdk.reportStatusDeploy = (deployedPackage, status, previousLabelOrAppVersion, previousDeploymentKey) => {
    return new Promise((resolve, reject) => {
      module.exports.AcquisitionSdk.prototype.reportStatusDeploy.call(sdk, deployedPackage, status, previousLabelOrAppVersion, previousDeploymentKey, err => {
        if (err) {
          reject(err);
        } else {
          resolve();
        }
      });
    });
  };

  sdk.reportStatusDownload = downloadedPackage => {
    return new Promise((resolve, reject) => {
      module.exports.AcquisitionSdk.prototype.reportStatusDownload.call(sdk, downloadedPackage, err => {
        if (err) {
          reject(err);
        } else {
          resolve();
        }
      });
    });
  };

  return sdk;
}

// This ensures that notifyApplicationReadyInternal is only called once
// in the lifetime of this module instance.
const notifyApplicationReady = (() => {
  let notifyApplicationReadyPromise;
  return () => {
    if (!notifyApplicationReadyPromise) {
      notifyApplicationReadyPromise = notifyApplicationReadyInternal();
    }

    return notifyApplicationReadyPromise;
  };
})();

async function notifyApplicationReadyInternal() {
  await NativeCodePush.notifyApplicationReady();
  const statusReport = await NativeCodePush.getNewStatusReport();
  statusReport && tryReportStatus(statusReport); // Don't wait for this to complete.

  return statusReport;
}

async function tryReportStatus(statusReport, resumeListener) {
  const config = await getConfiguration();
  const previousLabelOrAppVersion = statusReport.previousLabelOrAppVersion;
  const previousDeploymentKey = statusReport.previousDeploymentKey || config.deploymentKey;
  try {
    if (statusReport.appVersion) {
      log(`Reporting binary update (${statusReport.appVersion})`);

      const sdk = getPromisifiedSdk(requestFetchAdapter, config);
      await sdk.reportStatusDeploy(/* deployedPackage */ null, /* status */ null, previousLabelOrAppVersion, previousDeploymentKey);
    } else {
      const label = statusReport.package.label;
      if (statusReport.status === 'DeploymentSucceeded') {
        log(`Reporting CodePush update success (${label})`);
      } else {
        log(`Reporting CodePush update rollback (${label})`);
        await NativeCodePush.setLatestRollbackInfo(statusReport.package.packageHash);
      }

      config.deploymentKey = statusReport.package.deploymentKey;
      const sdk = getPromisifiedSdk(requestFetchAdapter, config);
      await sdk.reportStatusDeploy(statusReport.package, statusReport.status, previousLabelOrAppVersion, previousDeploymentKey);
    }

    NativeCodePush.recordStatusReported(statusReport);
    resumeListener && AppState.removeEventListener('change', resumeListener);
  } catch (e) {
    log(`Report status failed: ${JSON.stringify(statusReport)}`);
    NativeCodePush.saveStatusReportForRetry(statusReport);
    // Try again when the app resumes
    if (!resumeListener) {
      resumeListener = async newState => {
        if (newState !== 'active') return;
        const refreshedStatusReport = await NativeCodePush.getNewStatusReport();
        if (refreshedStatusReport) {
          tryReportStatus(refreshedStatusReport, resumeListener);
        } else {
          AppState.removeEventListener('change', resumeListener);
        }
      };
      AppState.addEventListener('change', resumeListener);
    }
  }
}

async function shouldUpdateBeIgnored(remotePackage, syncOptions) {
  let { rollbackRetryOptions } = syncOptions;

  const isFailedPackage = remotePackage && remotePackage.failedInstall;
  if (!isFailedPackage || !syncOptions.ignoreFailedUpdates) {
    return false;
  }

  if (!rollbackRetryOptions) {
    return true;
  }

  if (typeof rollbackRetryOptions !== 'object') {
    rollbackRetryOptions = CodePush.DEFAULT_ROLLBACK_RETRY_OPTIONS;
  } else {
    rollbackRetryOptions = { ...CodePush.DEFAULT_ROLLBACK_RETRY_OPTIONS, ...rollbackRetryOptions };
  }

  if (!validateRollbackRetryOptions(rollbackRetryOptions)) {
    return true;
  }

  const latestRollbackInfo = await NativeCodePush.getLatestRollbackInfo();
  if (!validateLatestRollbackInfo(latestRollbackInfo, remotePackage.packageHash)) {
    log('The latest rollback info is not valid.');
    return true;
  }

  const { delayInHours, maxRetryAttempts } = rollbackRetryOptions;
  const hoursSinceLatestRollback = (Date.now() - latestRollbackInfo.time) / (1000 * 60 * 60);
  if (hoursSinceLatestRollback >= delayInHours && maxRetryAttempts >= latestRollbackInfo.count) {
    log('Previous rollback should be ignored due to rollback retry options.');
    return false;
  }

  return true;
}

function validateLatestRollbackInfo(latestRollbackInfo, packageHash) {
  return latestRollbackInfo && latestRollbackInfo.time && latestRollbackInfo.count && latestRollbackInfo.packageHash && latestRollbackInfo.packageHash === packageHash;
}

function validateRollbackRetryOptions(rollbackRetryOptions) {
  if (typeof rollbackRetryOptions.delayInHours !== 'number') {
    log("The 'delayInHours' rollback retry parameter must be a number.");
    return false;
  }

  if (typeof rollbackRetryOptions.maxRetryAttempts !== 'number') {
    log("The 'maxRetryAttempts' rollback retry parameter must be a number.");
    return false;
  }

  if (rollbackRetryOptions.maxRetryAttempts < 1) {
    log("The 'maxRetryAttempts' rollback retry parameter cannot be less then 1.");
    return false;
  }

  return true;
}

var testConfig;

// This function is only used for tests. Replaces the default SDK, configuration and native bridge
function setUpTestDependencies(testSdk, providedTestConfig, testNativeBridge) {
  if (testSdk) module.exports.AcquisitionSdk = testSdk;
  if (providedTestConfig) testConfig = providedTestConfig;
  if (testNativeBridge) NativeCodePush = testNativeBridge;
}

// This function allows only one syncInternal operation to proceed at any given time.
// Parallel calls to sync() while one is ongoing yields CodePush.SyncStatus.SYNC_IN_PROGRESS.
const sync = (() => {
  let syncInProgress = false;
  const setSyncCompleted = () => {
    syncInProgress = false;
  };

  return (options = {}, syncStatusChangeCallback, downloadProgressCallback, handleBinaryVersionMismatchCallback) => {
    let syncStatusCallbackWithTryCatch, downloadProgressCallbackWithTryCatch;
    if (typeof syncStatusChangeCallback === 'function') {
      syncStatusCallbackWithTryCatch = (...args) => {
        try {
          syncStatusChangeCallback(...args);
        } catch (error) {
          log(`An error has occurred : ${error.stack}`);
        }
      };
    }

    if (typeof downloadProgressCallback === 'function') {
      downloadProgressCallbackWithTryCatch = (...args) => {
        try {
          downloadProgressCallback(...args);
        } catch (error) {
          log(`An error has occurred: ${error.stack}`);
        }
      };
    }

    if (syncInProgress) {
      typeof syncStatusCallbackWithTryCatch === 'function' ? syncStatusCallbackWithTryCatch(CodePush.SyncStatus.SYNC_IN_PROGRESS) : log('Sync already in progress.');
      return Promise.resolve(CodePush.SyncStatus.SYNC_IN_PROGRESS);
    }

    syncInProgress = true;
    const syncPromise = syncInternal(options, syncStatusCallbackWithTryCatch, downloadProgressCallbackWithTryCatch, handleBinaryVersionMismatchCallback);
    syncPromise.then(setSyncCompleted).catch(setSyncCompleted);

    return syncPromise;
  };
})();

/*
 * The syncInternal method provides a simple, one-line experience for
 * incorporating the check, download and installation of an update.
 *
 * It simply composes the existing API methods together and adds additional
 * support for respecting mandatory updates, ignoring previously failed
 * releases, and displaying a standard confirmation UI to the end-user
 * when an update is available.
 */
async function syncInternal(options = {}, syncStatusChangeCallback, downloadProgressCallback, handleBinaryVersionMismatchCallback) {
  let resolvedInstallMode;
  const syncOptions = {
    deploymentKey: null,
    ignoreFailedUpdates: true,
    rollbackRetryOptions: null,
    installMode: CodePush.InstallMode.ON_NEXT_RESTART,
    mandatoryInstallMode: CodePush.InstallMode.IMMEDIATE,
    minimumBackgroundDuration: 0,
    updateDialog: null,
    ...options
  };

  syncStatusChangeCallback =
    typeof syncStatusChangeCallback === 'function'
      ? syncStatusChangeCallback
      : syncStatus => {
          switch (syncStatus) {
            case CodePush.SyncStatus.CHECKING_FOR_UPDATE:
              log('Checking for update.');
              break;
            case CodePush.SyncStatus.AWAITING_USER_ACTION:
              log('Awaiting user action.');
              break;
            case CodePush.SyncStatus.DOWNLOADING_PACKAGE:
              log('Downloading package.');
              break;
            case CodePush.SyncStatus.INSTALLING_UPDATE:
              log('Installing update.');
              break;
            case CodePush.SyncStatus.UP_TO_DATE:
              log('App is up to date.');
              break;
            case CodePush.SyncStatus.UPDATE_IGNORED:
              log('User cancelled the update.');
              break;
            case CodePush.SyncStatus.UPDATE_INSTALLED:
              if (resolvedInstallMode == CodePush.InstallMode.ON_NEXT_RESTART) {
                log('Update is installed and will be run on the next app restart.');
              } else if (resolvedInstallMode == CodePush.InstallMode.ON_NEXT_RESUME) {
                if (syncOptions.minimumBackgroundDuration > 0) {
                  log(`Update is installed and will be run after the app has been in the background for at least ${syncOptions.minimumBackgroundDuration} seconds.`);
                } else {
                  log('Update is installed and will be run when the app next resumes.');
                }
              }
              break;
            case CodePush.SyncStatus.UNKNOWN_ERROR:
              log('An unknown error occurred.');
              break;
          }
        };

  try {
    await CodePush.notifyApplicationReady();

    syncStatusChangeCallback(CodePush.SyncStatus.CHECKING_FOR_UPDATE);
    const remotePackage = await checkForUpdate(syncOptions.deploymentKey, handleBinaryVersionMismatchCallback);

    const doDownloadAndInstall = async () => {
      syncStatusChangeCallback(CodePush.SyncStatus.DOWNLOADING_PACKAGE);
      const localPackage = await remotePackage.download(downloadProgressCallback);

      // Determine the correct install mode based on whether the update is mandatory or not.
      resolvedInstallMode = localPackage.isMandatory ? syncOptions.mandatoryInstallMode : syncOptions.installMode;

      syncStatusChangeCallback(CodePush.SyncStatus.INSTALLING_UPDATE);
      await localPackage.install(resolvedInstallMode, syncOptions.minimumBackgroundDuration, () => {
        syncStatusChangeCallback(CodePush.SyncStatus.UPDATE_INSTALLED);
      });

      return CodePush.SyncStatus.UPDATE_INSTALLED;
    };

    const updateShouldBeIgnored = await shouldUpdateBeIgnored(remotePackage, syncOptions);

    if (!remotePackage || updateShouldBeIgnored) {
      if (updateShouldBeIgnored) {
        log('An update is available, but it is being ignored due to having been previously rolled back.');
      }

      const currentPackage = await CodePush.getCurrentPackage();
      if (currentPackage && currentPackage.isPending) {
        syncStatusChangeCallback(CodePush.SyncStatus.UPDATE_INSTALLED);
        return CodePush.SyncStatus.UPDATE_INSTALLED;
      } else {
        syncStatusChangeCallback(CodePush.SyncStatus.UP_TO_DATE);
        return CodePush.SyncStatus.UP_TO_DATE;
      }
    } else if (syncOptions.updateDialog) {
      // updateDialog supports any truthy value (e.g. true, "goo", 12),
      // but we should treat a non-object value as just the default dialog
      if (typeof syncOptions.updateDialog !== 'object') {
        syncOptions.updateDialog = CodePush.DEFAULT_UPDATE_DIALOG;
      } else {
        syncOptions.updateDialog = { ...CodePush.DEFAULT_UPDATE_DIALOG, ...syncOptions.updateDialog };
      }

      return await new Promise((resolve, reject) => {
        let message = null;
        let installButtonText = null;

        const dialogButtons = [];

        if (remotePackage.isMandatory) {
          message = syncOptions.updateDialog.mandatoryUpdateMessage;
          installButtonText = syncOptions.updateDialog.mandatoryContinueButtonLabel;
        } else {
          message = syncOptions.updateDialog.optionalUpdateMessage;
          installButtonText = syncOptions.updateDialog.optionalInstallButtonLabel;
          // Since this is an optional update, add a button
          // to allow the end-user to ignore it
          dialogButtons.push({
            text: syncOptions.updateDialog.optionalIgnoreButtonLabel,
            onPress: () => {
              syncStatusChangeCallback(CodePush.SyncStatus.UPDATE_IGNORED);
              resolve(CodePush.SyncStatus.UPDATE_IGNORED);
            }
          });
        }

        // Since the install button should be placed to the
        // right of any other button, add it last
        dialogButtons.push({
          text: installButtonText,
          onPress: () => {
            doDownloadAndInstall().then(resolve, reject);
          }
        });

        // If the update has a description, and the developer
        // explicitly chose to display it, then set that as the message
        if (syncOptions.updateDialog.appendReleaseDescription && remotePackage.description) {
          message += `${syncOptions.updateDialog.descriptionPrefix} ${remotePackage.description}`;
        }

        syncStatusChangeCallback(CodePush.SyncStatus.AWAITING_USER_ACTION);
        Alert.alert(syncOptions.updateDialog.title, message, dialogButtons);
      });
    } else {
      return await doDownloadAndInstall();
    }
  } catch (error) {
    syncStatusChangeCallback(CodePush.SyncStatus.UNKNOWN_ERROR);
    log(error.message);
    throw error;
  }
}

const getShouldUpdateBeIgnored = (() => {
  return async (options = {}, syncStatusChangeCallback, handleBinaryVersionMismatchCallback) => {
    const syncOptions = {
      deploymentKey: null,
      ignoreFailedUpdates: true,
      rollbackRetryOptions: null,
      installMode: CodePush.InstallMode.ON_NEXT_RESTART,
      mandatoryInstallMode: CodePush.InstallMode.IMMEDIATE,
      minimumBackgroundDuration: 0,
      updateDialog: null,
      ...options
    };

    syncStatusChangeCallback =
      typeof syncStatusChangeCallback === 'function'
        ? syncStatusChangeCallback
        : syncStatus => {
            switch (syncStatus) {
              case CodePush.SyncStatus.CHECKING_FOR_UPDATE:
                log('Checking for update.');
                break;
              case CodePush.SyncStatus.AWAITING_USER_ACTION:
                log('Awaiting user action.');
                break;
              case CodePush.SyncStatus.DOWNLOADING_PACKAGE:
                log('Downloading package.');
                break;
              case CodePush.SyncStatus.INSTALLING_UPDATE:
                log('Installing update.');
                break;
              case CodePush.SyncStatus.UP_TO_DATE:
                log('App is up to date.');
                break;
              case CodePush.SyncStatus.UPDATE_IGNORED:
                log('User cancelled the update.');
                break;
              case CodePush.SyncStatus.UPDATE_INSTALLED:
                if (resolvedInstallMode == CodePush.InstallMode.ON_NEXT_RESTART) {
                  log('Update is installed and will be run on the next app restart.');
                } else if (resolvedInstallMode == CodePush.InstallMode.ON_NEXT_RESUME) {
                  if (syncOptions.minimumBackgroundDuration > 0) {
                    log(`Update is installed and will be run after the app has been in the background for at least ${syncOptions.minimumBackgroundDuration} seconds.`);
                  } else {
                    log('Update is installed and will be run when the app next resumes.');
                  }
                }
                break;
              case CodePush.SyncStatus.UNKNOWN_ERROR:
                log('An unknown error occurred.');
                break;
            }
          };

    try {
      await CodePush.notifyApplicationReady();
      syncStatusChangeCallback(CodePush.SyncStatus.CHECKING_FOR_UPDATE);

      const remotePackage = await checkForUpdate(syncOptions.deploymentKey, handleBinaryVersionMismatchCallback);

      const updateShouldBeIgnored = await shouldUpdateBeIgnored(remotePackage, syncOptions);

      return {
        status: true,
        remotePackage,
        updateShouldBeIgnored
      };
    } catch (error) {
      syncStatusChangeCallback(CodePush.SyncStatus.UNKNOWN_ERROR);
      log(error.message);
      return null;
    }
  };
})();

let CodePush;

function codePushify(options = {}) {
  let React;
  let ReactNative = require('react-native');

  try {
    React = require('react');
  } catch (e) {}
  if (!React) {
    try {
      React = ReactNative.React;
    } catch (e) {}
    if (!React) {
      throw new Error("Unable to find the 'React' module.");
    }
  }

  if (!React.Component) {
    throw new Error(
      `Unable to find the "Component" class, please either:
1. Upgrade to a newer version of React Native that supports it, or
2. Call the codePush.sync API in your component instead of using the @codePush decorator`
    );
  }

  var decorator = RootComponent => {
    const extended = class CodePushComponent extends React.Component {
      componentDidMount() {
        if (options.checkFrequency === CodePush.CheckFrequency.MANUAL) {
          CodePush.notifyAppReady();
        } else {
          let rootComponentInstance = this.refs.rootComponent;

          let syncStatusCallback;
          if (rootComponentInstance && rootComponentInstance.codePushStatusDidChange) {
            syncStatusCallback = rootComponentInstance.codePushStatusDidChange;
            if (rootComponentInstance instanceof React.Component) {
              syncStatusCallback = syncStatusCallback.bind(rootComponentInstance);
            }
          }

          let downloadProgressCallback;
          if (rootComponentInstance && rootComponentInstance.codePushDownloadDidProgress) {
            downloadProgressCallback = rootComponentInstance.codePushDownloadDidProgress;
            if (rootComponentInstance instanceof React.Component) {
              downloadProgressCallback = downloadProgressCallback.bind(rootComponentInstance);
            }
          }

          let handleBinaryVersionMismatchCallback;
          if (rootComponentInstance && rootComponentInstance.codePushOnBinaryVersionMismatch) {
            handleBinaryVersionMismatchCallback = rootComponentInstance.codePushOnBinaryVersionMismatch;
            if (rootComponentInstance instanceof React.Component) {
              handleBinaryVersionMismatchCallback = handleBinaryVersionMismatchCallback.bind(rootComponentInstance);
            }
          }

          CodePush.sync(options, syncStatusCallback, downloadProgressCallback, handleBinaryVersionMismatchCallback);
          if (options.checkFrequency === CodePush.CheckFrequency.ON_APP_RESUME) {
            ReactNative.AppState.addEventListener('change', newState => {
              newState === 'active' && CodePush.sync(options, syncStatusCallback, downloadProgressCallback);
            });
          }
        }
      }

      render() {
        const props = { ...this.props };

        // we can set ref property on class components only (not stateless)
        // check it by render method
        if (RootComponent.prototype.render) {
          props.ref = 'rootComponent';
        }

        return <RootComponent {...props} />;
      }
    };

    return hoistStatics(extended, RootComponent);
  };

  if (typeof options === 'function') {
    // Infer that the root component was directly passed to us.
    return decorator(options);
  } else {
    return decorator;
  }
}

// If the "NativeCodePush" variable isn't defined, then
// the app didn't properly install the native module,
// and therefore, it doesn't make sense initializing
// the JS interface when it wouldn't work anyways.
if (NativeCodePush) {
  CodePush = codePushify;
  Object.assign(CodePush, {
    AcquisitionSdk: Sdk,
    checkForUpdate,
    getConfiguration,
    getCurrentPackage,
    getUpdateMetadata,
    getShouldUpdateBeIgnored,
    log,
    notifyAppReady: notifyApplicationReady,
    notifyApplicationReady,
    restartApp: RestartManager.restartApp,
    setUpTestDependencies,
    sync,
    disallowRestart: RestartManager.disallow,
    allowRestart: RestartManager.allow,
    clearUpdates: NativeCodePush.clearUpdates,
    getLatestRollbackInfo: NativeCodePush.getLatestRollbackInfo,
    InstallMode: {
      IMMEDIATE: NativeCodePush.codePushInstallModeImmediate, // Restart the app immediately
      ON_NEXT_RESTART: NativeCodePush.codePushInstallModeOnNextRestart, // Don't artificially restart the app. Allow the update to be "picked up" on the next app restart
      ON_NEXT_RESUME: NativeCodePush.codePushInstallModeOnNextResume, // Restart the app the next time it is resumed from the background
      ON_NEXT_SUSPEND: NativeCodePush.codePushInstallModeOnNextSuspend // Restart the app _while_ it is in the background,
      // but only after it has been in the background for "minimumBackgroundDuration" seconds (0 by default),
      // so that user context isn't lost unless the app suspension is long enough to not matter
    },
    SyncStatus: {
      UP_TO_DATE: 0, // The running app is up-to-date
      UPDATE_INSTALLED: 1, // The app had an optional/mandatory update that was successfully downloaded and is about to be installed.
      UPDATE_IGNORED: 2, // The app had an optional update and the end-user chose to ignore it
      UNKNOWN_ERROR: 3,
      SYNC_IN_PROGRESS: 4, // There is an ongoing "sync" operation in progress.
      CHECKING_FOR_UPDATE: 5,
      AWAITING_USER_ACTION: 6,
      DOWNLOADING_PACKAGE: 7,
      INSTALLING_UPDATE: 8
    },
    CheckFrequency: {
      ON_APP_START: 0,
      ON_APP_RESUME: 1,
      MANUAL: 2
    },
    UpdateState: {
      RUNNING: NativeCodePush.codePushUpdateStateRunning,
      PENDING: NativeCodePush.codePushUpdateStatePending,
      LATEST: NativeCodePush.codePushUpdateStateLatest
    },
    DeploymentStatus: {
      FAILED: 'DeploymentFailed',
      SUCCEEDED: 'DeploymentSucceeded'
    },
    DEFAULT_UPDATE_DIALOG: {
      appendReleaseDescription: false,
      descriptionPrefix: ' Description: ',
      mandatoryContinueButtonLabel: 'Continue',
      mandatoryUpdateMessage: 'An update is available that must be installed.',
      optionalIgnoreButtonLabel: 'Ignore',
      optionalInstallButtonLabel: 'Install',
      optionalUpdateMessage: 'An update is available. Would you like to install it?',
      title: 'Update available'
    },
    DEFAULT_ROLLBACK_RETRY_OPTIONS: {
      delayInHours: 24,
      maxRetryAttempts: 1
    }
  });
} else {
  log("The CodePush module doesn't appear to be properly installed. Please double-check that everything is setup correctly.");
}

module.exports = CodePush;
