mirror of
https://gitcode.com/gh_mirrors/re/react-native-pushy.git
synced 2025-09-18 22:20:39 +08:00
356 lines
8.8 KiB
TypeScript
356 lines
8.8 KiB
TypeScript
import { CheckResult, PushyOptions, ProgressData, EventType } from './type';
|
|
import { assertRelease, log } from './utils';
|
|
import {
|
|
EmitterSubscription,
|
|
PermissionsAndroid,
|
|
Platform,
|
|
} from 'react-native';
|
|
import {
|
|
PushyModule,
|
|
buildTime,
|
|
cInfo,
|
|
pushyNativeEventEmitter,
|
|
currentVersion,
|
|
packageVersion,
|
|
rolledBackVersion,
|
|
setLocalHashInfo,
|
|
isRolledBack,
|
|
} from './core';
|
|
|
|
const defaultServer = {
|
|
main: 'https://update.react-native.cn/api',
|
|
backups: ['https://update.reactnative.cn/api'],
|
|
queryUrl:
|
|
'https://raw.githubusercontent.com/reactnativecn/react-native-pushy/master/endpoints.json',
|
|
};
|
|
|
|
const empty = {};
|
|
const noop = () => {};
|
|
|
|
export class Pushy {
|
|
options: PushyOptions = {
|
|
appKey: '',
|
|
server: defaultServer,
|
|
autoMarkSuccess: true,
|
|
useAlert: true,
|
|
strategy: 'both',
|
|
logger: noop,
|
|
};
|
|
|
|
lastChecking: number;
|
|
lastResult: CheckResult;
|
|
|
|
progressHandlers: Record<string, EmitterSubscription> = {};
|
|
downloadedHash: string;
|
|
|
|
marked = false;
|
|
applyingUpdate = false;
|
|
version = cInfo.pushy;
|
|
|
|
constructor(options: PushyOptions) {
|
|
if (!options.appKey) {
|
|
throw new Error('appKey is required');
|
|
}
|
|
this.setOptions(options);
|
|
}
|
|
|
|
setOptions = (options: Partial<PushyOptions>) => {
|
|
for (const [key, value] of Object.entries(options)) {
|
|
if (value !== undefined) {
|
|
this.options[key] = value;
|
|
if (key === 'logger') {
|
|
if (isRolledBack) {
|
|
this.report({
|
|
type: 'rollback',
|
|
data: {
|
|
rolledBackVersion,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
report = ({
|
|
type,
|
|
message = '',
|
|
data = {},
|
|
}: {
|
|
type: EventType;
|
|
message?: string;
|
|
data?: Record<string, string | number>;
|
|
}) => {
|
|
log(type + ' ' + message);
|
|
const { logger = noop, appKey } = this.options;
|
|
logger({
|
|
type,
|
|
data: {
|
|
appKey,
|
|
currentVersion,
|
|
cInfo,
|
|
packageVersion,
|
|
buildTime,
|
|
message,
|
|
...data,
|
|
},
|
|
});
|
|
};
|
|
|
|
getCheckUrl = (endpoint: string = this.options.server!.main) => {
|
|
return `${endpoint}/checkUpdate/${this.options.appKey}`;
|
|
};
|
|
assertHash = (hash: string) => {
|
|
if (!this.downloadedHash) {
|
|
return;
|
|
}
|
|
if (hash !== this.downloadedHash) {
|
|
log(`use downloaded hash ${this.downloadedHash} first`);
|
|
return;
|
|
}
|
|
return true;
|
|
};
|
|
markSuccess = () => {
|
|
assertRelease();
|
|
if (this.marked) {
|
|
return;
|
|
}
|
|
this.marked = true;
|
|
PushyModule.markSuccess();
|
|
this.report({ type: 'markSuccess' });
|
|
};
|
|
switchVersion = (hash: string) => {
|
|
assertRelease();
|
|
if (this.assertHash(hash) && !this.applyingUpdate) {
|
|
log('switchVersion: ' + hash);
|
|
this.applyingUpdate = true;
|
|
PushyModule.reloadUpdate({ hash });
|
|
}
|
|
};
|
|
|
|
switchVersionLater = (hash: string) => {
|
|
assertRelease();
|
|
if (this.assertHash(hash)) {
|
|
log('switchVersionLater: ' + hash);
|
|
PushyModule.setNeedUpdate({ hash });
|
|
}
|
|
};
|
|
checkUpdate = async () => {
|
|
assertRelease();
|
|
const now = Date.now();
|
|
if (
|
|
this.lastResult &&
|
|
this.lastChecking &&
|
|
now - this.lastChecking < 1000 * 5
|
|
) {
|
|
return this.lastResult;
|
|
}
|
|
this.lastChecking = now;
|
|
this.report({ type: 'checking' });
|
|
const fetchPayload = {
|
|
method: 'POST',
|
|
headers: {
|
|
Accept: 'application/json',
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
packageVersion,
|
|
hash: currentVersion,
|
|
buildTime,
|
|
cInfo,
|
|
}),
|
|
};
|
|
let resp;
|
|
try {
|
|
resp = await fetch(this.getCheckUrl(), fetchPayload);
|
|
} catch (e) {
|
|
this.report({
|
|
type: 'errorChecking',
|
|
message: 'Can not connect to update server. Trying backup endpoints.',
|
|
});
|
|
const backupEndpoints = await this.getBackupEndpoints();
|
|
if (backupEndpoints) {
|
|
try {
|
|
resp = await Promise.race(
|
|
backupEndpoints.map((endpoint) =>
|
|
fetch(this.getCheckUrl(endpoint), fetchPayload),
|
|
),
|
|
);
|
|
} catch {}
|
|
}
|
|
}
|
|
if (!resp) {
|
|
this.report({
|
|
type: 'errorChecking',
|
|
message: 'Can not connect to update server. Please check your network.',
|
|
});
|
|
return this.lastResult || empty;
|
|
}
|
|
const result: CheckResult = await resp.json();
|
|
|
|
this.lastResult = result;
|
|
|
|
if (resp.status !== 200) {
|
|
this.report({
|
|
type: 'errorChecking',
|
|
message: result.message,
|
|
});
|
|
}
|
|
|
|
return result;
|
|
};
|
|
getBackupEndpoints = async () => {
|
|
const { server } = this.options;
|
|
if (!server) {
|
|
return [];
|
|
}
|
|
if (server.queryUrl) {
|
|
try {
|
|
const resp = await fetch(server.queryUrl);
|
|
const remoteEndpoints = await resp.json();
|
|
log('fetch endpoints:', remoteEndpoints);
|
|
if (Array.isArray(remoteEndpoints)) {
|
|
server.backups = Array.from(
|
|
new Set([...(server.backups || []), ...remoteEndpoints]),
|
|
);
|
|
}
|
|
} catch (e) {
|
|
log('failed to fetch endpoints from: ', server.queryUrl);
|
|
}
|
|
}
|
|
return server.backups;
|
|
};
|
|
downloadUpdate = async (
|
|
info: CheckResult,
|
|
onDownloadProgress?: (data: ProgressData) => void,
|
|
) => {
|
|
assertRelease();
|
|
const { hash, diffUrl, pdiffUrl, updateUrl, name, description, metaInfo } =
|
|
info;
|
|
if (!info.update || !hash) {
|
|
return;
|
|
}
|
|
if (rolledBackVersion === hash) {
|
|
log(`rolledback hash ${rolledBackVersion}, ignored`);
|
|
return;
|
|
}
|
|
if (this.downloadedHash === hash) {
|
|
log(`duplicated downloaded hash ${this.downloadedHash}, ignored`);
|
|
return this.downloadedHash;
|
|
}
|
|
if (this.progressHandlers[hash]) {
|
|
return;
|
|
}
|
|
if (onDownloadProgress) {
|
|
this.progressHandlers[hash] = pushyNativeEventEmitter.addListener(
|
|
'RCTPushyDownloadProgress',
|
|
(progressData) => {
|
|
if (progressData.hash === hash) {
|
|
onDownloadProgress(progressData);
|
|
}
|
|
},
|
|
);
|
|
}
|
|
let succeeded = false;
|
|
this.report({ type: 'downloading' });
|
|
if (diffUrl) {
|
|
log('downloading diff');
|
|
try {
|
|
await PushyModule.downloadPatchFromPpk({
|
|
updateUrl: diffUrl,
|
|
hash,
|
|
originHash: currentVersion,
|
|
});
|
|
succeeded = true;
|
|
} catch (e) {
|
|
log(`diff error: ${e.message}, try pdiff`);
|
|
}
|
|
}
|
|
if (!succeeded && pdiffUrl) {
|
|
log('downloading pdiff');
|
|
try {
|
|
await PushyModule.downloadPatchFromPackage({
|
|
updateUrl: pdiffUrl,
|
|
hash,
|
|
});
|
|
succeeded = true;
|
|
} catch (e) {
|
|
log(`pdiff error: ${e.message}, try full patch`);
|
|
}
|
|
}
|
|
if (!succeeded && updateUrl) {
|
|
log('downloading full patch');
|
|
try {
|
|
await PushyModule.downloadFullUpdate({
|
|
updateUrl: updateUrl,
|
|
hash,
|
|
});
|
|
succeeded = true;
|
|
} catch (e) {
|
|
log(`full patch error: ${e.message}`);
|
|
}
|
|
}
|
|
if (this.progressHandlers[hash]) {
|
|
this.progressHandlers[hash].remove();
|
|
delete this.progressHandlers[hash];
|
|
}
|
|
if (!succeeded) {
|
|
return this.report({
|
|
type: 'errorUpdate',
|
|
data: { newVersion: hash },
|
|
});
|
|
}
|
|
log('downloaded hash:', hash);
|
|
setLocalHashInfo(hash, {
|
|
name,
|
|
description,
|
|
metaInfo,
|
|
});
|
|
this.downloadedHash = hash;
|
|
return hash;
|
|
};
|
|
downloadAndInstallApk = async (
|
|
url: string,
|
|
onDownloadProgress?: (data: ProgressData) => void,
|
|
) => {
|
|
if (Platform.OS !== 'android') {
|
|
return;
|
|
}
|
|
this.report({ type: 'downloadingApk' });
|
|
if (Platform.Version <= 23) {
|
|
try {
|
|
const granted = await PermissionsAndroid.request(
|
|
PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE,
|
|
);
|
|
if (granted !== PermissionsAndroid.RESULTS.GRANTED) {
|
|
return this.report({ type: 'rejectStoragePermission' });
|
|
}
|
|
} catch (err) {
|
|
return this.report({ type: 'errorStoragePermission' });
|
|
}
|
|
}
|
|
const progressKey = 'downloadingApk';
|
|
if (onDownloadProgress) {
|
|
this.progressHandlers[progressKey] = pushyNativeEventEmitter.addListener(
|
|
'RCTPushyDownloadProgress',
|
|
(progressData: ProgressData) => {
|
|
if (progressData.hash === progressKey) {
|
|
onDownloadProgress(progressData);
|
|
}
|
|
},
|
|
);
|
|
}
|
|
await PushyModule.downloadAndInstallApk({
|
|
url,
|
|
target: 'update.apk',
|
|
hash: progressKey,
|
|
}).catch(() => {
|
|
this.report({ type: 'errowDownloadAndInstallApk' });
|
|
});
|
|
if (this.progressHandlers[progressKey]) {
|
|
this.progressHandlers[progressKey].remove();
|
|
delete this.progressHandlers[progressKey];
|
|
}
|
|
};
|
|
}
|