@hot-updater/react-native
Version:
React Native OTA solution for self-hosted
321 lines (315 loc) • 11.4 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
var _exportNames = {
HotUpdater: true,
extractSignatureFailure: true,
isSignatureVerificationError: true
};
exports.HotUpdater = void 0;
Object.defineProperty(exports, "extractSignatureFailure", {
enumerable: true,
get: function () {
return _types.extractSignatureFailure;
}
});
Object.defineProperty(exports, "isSignatureVerificationError", {
enumerable: true,
get: function () {
return _types.isSignatureVerificationError;
}
});
var _checkForUpdate = require("./checkForUpdate.js");
var _DefaultResolver = require("./DefaultResolver.js");
var _native = require("./native.js");
var _store = require("./store.js");
Object.keys(_store).forEach(function (key) {
if (key === "default" || key === "__esModule") return;
if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return;
if (key in exports && exports[key] === _store[key]) return;
Object.defineProperty(exports, key, {
enumerable: true,
get: function () {
return _store[key];
}
});
});
var _wrap = require("./wrap.js");
var _types = require("./types.js");
(0, _native.addListener)("onProgress", ({
progress
}) => {
_store.hotUpdaterStore.setState({
progress
});
});
/**
* Register getBaseURL to global objects for use without imports.
* This is needed for Expo DOM components and Babel plugin generated code.
*/
const registerGlobalGetBaseURL = () => {
const fn = _native.getBaseURL;
// Register to globalThis (modern, cross-platform)
if (typeof globalThis !== "undefined") {
if (!globalThis.HotUpdaterGetBaseURL) {
globalThis.HotUpdaterGetBaseURL = fn;
}
}
// Register to global (React Native, Node.js)
if (typeof global !== "undefined") {
if (!global.HotUpdaterGetBaseURL) {
global.HotUpdaterGetBaseURL = fn;
}
}
};
// Call registration immediately on module load
registerGlobalGetBaseURL();
/**
* Creates a HotUpdater client instance with all update management methods.
* This function is called once on module initialization to create a singleton instance.
*/
function createHotUpdaterClient() {
// Global configuration stored from wrap
const globalConfig = {
resolver: null
};
const ensureGlobalResolver = methodName => {
if (!globalConfig.resolver) {
throw new Error(`[HotUpdater] ${methodName} requires HotUpdater.wrap() to be used.\n\n` + `To fix this issue, wrap your root component with HotUpdater.wrap():\n\n` + `Option 1: With automatic updates\n` + ` export default HotUpdater.wrap({\n` + ` baseURL: "<your-update-server-url>",\n` + ` updateStrategy: "appVersion",\n` + ` updateMode: "auto"\n` + ` })(App);\n\n` + `Option 2: Manual updates only (custom flow)\n` + ` export default HotUpdater.wrap({\n` + ` baseURL: "<your-update-server-url>",\n` + ` updateMode: "manual"\n` + ` })(App);\n\n` + `For more information, visit: https://hot-updater.dev/docs/react-native-api/wrap`);
}
return globalConfig.resolver;
};
return {
/**
* `HotUpdater.wrap` checks for updates at the entry point, and if there is a bundle to update, it downloads the bundle and applies the update strategy.
*
* @param {object} options - Configuration options
* @param {string} options.source - Update server URL
* @param {object} [options.requestHeaders] - Request headers
* @param {React.ComponentType} [options.fallbackComponent] - Component to display during updates
* @param {boolean} [options.reloadOnForceUpdate=true] - Whether to automatically reload the app on force updates
* @param {Function} [options.onUpdateProcessCompleted] - Callback after update process completes
* @param {Function} [options.onProgress] - Callback to track bundle download progress
* @returns {Function} Higher-order component that wraps the app component
*
* @example
* ```tsx
* export default HotUpdater.wrap({
* baseURL: "<your-update-server-url>",
* updateStrategy: "appVersion",
* updateMode: "auto",
* requestHeaders: {
* "Authorization": "Bearer <your-access-token>",
* },
* })(App);
* ```
*/
wrap: options => {
let normalizedOptions;
if ("baseURL" in options && options.baseURL) {
const {
baseURL,
...rest
} = options;
normalizedOptions = {
...rest,
resolver: (0, _DefaultResolver.createDefaultResolver)(baseURL)
};
} else if ("resolver" in options && options.resolver) {
normalizedOptions = options;
} else {
throw new Error(`[HotUpdater] Either baseURL or resolver must be provided.\n\n` + `Option 1: Using baseURL (recommended for most cases)\n` + ` export default HotUpdater.wrap({\n` + ` baseURL: "<your-update-server-url>",\n` + ` updateStrategy: "appVersion",\n` + ` updateMode: "auto"\n` + ` })(App);\n\n` + `Option 2: Using custom resolver (advanced)\n` + ` export default HotUpdater.wrap({\n` + ` resolver: {\n` + ` checkUpdate: async (params) => { /* custom logic */ },\n` + ` notifyAppReady: async (params) => { /* custom logic */ }\n` + ` },\n` + ` updateMode: "manual"\n` + ` })(App);\n\n` + `For more information, visit: https://hot-updater.dev/docs/react-native-api/wrap`);
}
globalConfig.resolver = normalizedOptions.resolver;
globalConfig.requestHeaders = options.requestHeaders;
globalConfig.requestTimeout = options.requestTimeout;
return (0, _wrap.wrap)(normalizedOptions);
},
/**
* Reloads the app.
*/
reload: _native.reload,
/**
* Returns whether an update has finished downloading in this app session.
*
* When it returns true, calling `HotUpdater.reload()` (or restarting the app)
* will apply the downloaded update bundle.
*
* - Derived from `progress` reaching 1.0
* - Resets to false when a new download starts (progress < 1)
*
* @returns {boolean} True if a downloaded update is ready to apply
* @example
* ```ts
* if (HotUpdater.isUpdateDownloaded()) {
* await HotUpdater.reload();
* }
* ```
*/
isUpdateDownloaded: () => _store.hotUpdaterStore.getSnapshot().isUpdateDownloaded,
/**
* Fetches the current app version.
*/
getAppVersion: _native.getAppVersion,
/**
* Fetches the current bundle ID of the app.
*/
getBundleId: _native.getBundleId,
/**
* Retrieves the initial bundle ID based on the build time of the native app.
*/
getMinBundleId: _native.getMinBundleId,
/**
* Fetches the current channel of the app.
*
* If no channel is specified, the app is assigned to the 'production' channel.
*
* @returns {string} The current release channel of the app
* @default "production"
* @example
* ```ts
* const channel = HotUpdater.getChannel();
* console.log(`Current channel: ${channel}`);
* ```
*/
getChannel: _native.getChannel,
/**
* Adds a listener to HotUpdater events.
*
* @param {keyof HotUpdaterEvent} eventName - The name of the event to listen for
* @param {(event: HotUpdaterEvent[T]) => void} listener - The callback function to handle the event
* @returns {() => void} A cleanup function that removes the event listener
*
* @example
* ```ts
* const unsubscribe = HotUpdater.addListener("onProgress", ({ progress }) => {
* console.log(`Update progress: ${progress * 100}%`);
* });
*
* // Unsubscribe when no longer needed
* unsubscribe();
* ```
*/
addListener: _native.addListener,
/**
* Manually checks for updates.
*
* @param {Object} config - Update check configuration
* @param {string} config.source - Update server URL
* @param {Record<string, string>} [config.requestHeaders] - Request headers
*
* @returns {Promise<UpdateInfo | null>} Update information or null if up to date
*
* @example
* ```ts
* const updateInfo = await HotUpdater.checkForUpdate({
* source: "<your-update-server-url>",
* requestHeaders: {
* Authorization: "Bearer <your-access-token>",
* },
* });
*
* if (!updateInfo) {
* console.log("App is up to date");
* return;
* }
*
* await HotUpdater.updateBundle(updateInfo.id, updateInfo.fileUrl);
* if (updateInfo.shouldForceUpdate) {
* await HotUpdater.reload();
* }
* ```
*/
checkForUpdate: config => {
const resolver = ensureGlobalResolver("checkForUpdate");
const mergedConfig = {
...config,
resolver,
requestHeaders: {
...globalConfig.requestHeaders,
...config.requestHeaders
},
requestTimeout: config.requestTimeout ?? globalConfig.requestTimeout
};
return (0, _checkForUpdate.checkForUpdate)(mergedConfig);
},
/**
* Updates the bundle of the app.
*
* @param {UpdateBundleParams} params - Parameters object required for bundle update
* @param {string} params.bundleId - The bundle ID of the app
* @param {string|null} params.fileUrl - The URL of the zip file
*
* @returns {Promise<boolean>} Whether the update was successful
*
* @example
* ```ts
* const updateInfo = await HotUpdater.checkForUpdate({
* source: "<your-update-server-url>",
* requestHeaders: {
* Authorization: "Bearer <your-access-token>",
* },
* });
*
* if (!updateInfo) {
* return {
* status: "UP_TO_DATE",
* };
* }
*
* await HotUpdater.updateBundle({
* bundleId: updateInfo.id,
* fileUrl: updateInfo.fileUrl
* });
* if (updateInfo.shouldForceUpdate) {
* await HotUpdater.reload();
* }
* ```
*/
updateBundle: params => {
ensureGlobalResolver("updateBundle");
return (0, _native.updateBundle)(params);
},
/**
* Fetches the fingerprint of the app.
*
* @returns {string} The fingerprint of the app
*
* @example
* ```ts
* const fingerprint = HotUpdater.getFingerprintHash();
* console.log(`Fingerprint: ${fingerprint}`);
* ```
*/
getFingerprintHash: _native.getFingerprintHash,
/**
* Gets the list of bundle IDs that have been marked as crashed.
* These bundles will be rejected if attempted to install again.
*
* @returns {string[]} Array of crashed bundle IDs
*
* @example
* ```ts
* const crashedBundles = HotUpdater.getCrashHistory();
* console.log("Crashed bundles:", crashedBundles);
* ```
*/
getCrashHistory: _native.getCrashHistory,
/**
* Clears the crashed bundle history, allowing previously crashed bundles
* to be installed again.
*
* @returns {boolean} true if clearing was successful
*
* @example
* ```ts
* // Clear crash history to allow retrying a previously failed bundle
* HotUpdater.clearCrashHistory();
* ```
*/
clearCrashHistory: _native.clearCrashHistory
};
}
const HotUpdater = exports.HotUpdater = createHotUpdaterClient();
//# sourceMappingURL=index.js.map