1
0
mirror of https://gitcode.com/gh_mirrors/re/react-native-pushy.git synced 2025-09-16 10:21:37 +08:00
Code Issues Packages Projects Releases Wiki Activity GitHub Gitee
Files
react-native-update/src/provider.tsx
2025-04-11 14:48:18 +08:00

384 lines
10 KiB
TypeScript

import React, {
ReactNode,
useCallback,
useEffect,
useRef,
useState,
} from 'react';
import {
Alert,
NativeEventSubscription,
AppState,
Platform,
Linking,
} from 'react-native';
import { Pushy, Cresc } from './client';
import { currentVersion, packageVersion, getCurrentVersionInfo } from './core';
import { CheckResult, ProgressData, UpdateTestPayload } from './type';
import { UpdateContext } from './context';
import { URL } from 'react-native-url-polyfill';
import { isInRollout } from './isInRollout';
import { log } from './utils';
export const UpdateProvider = ({
client,
children,
}: {
client: Pushy | Cresc;
children: ReactNode;
}) => {
client = useRef(client).current;
const { options } = client;
const stateListener = useRef<NativeEventSubscription>();
const [updateInfo, setUpdateInfo] = useState<CheckResult>();
const updateInfoRef = useRef(updateInfo);
const [progress, setProgress] = useState<ProgressData>();
const [lastError, setLastError] = useState<Error>();
const lastChecking = useRef(0);
const throwErrorIfEnabled = useCallback(
(e: Error) => {
if (options.throwError) {
throw e;
}
},
[options.throwError],
);
const dismissError = useCallback(() => {
setLastError(undefined);
}, []);
const alertUpdate = useCallback(
(...args: Parameters<typeof Alert.alert>) => {
if (
options.updateStrategy === 'alwaysAlert' ||
options.updateStrategy === 'alertUpdateAndIgnoreError'
) {
Alert.alert(...args);
}
},
[options.updateStrategy],
);
const alertError = useCallback(
(...args: Parameters<typeof Alert.alert>) => {
if (options.updateStrategy === 'alwaysAlert') {
Alert.alert(...args);
}
},
[options.updateStrategy],
);
const switchVersion = useCallback(
async (info: CheckResult | undefined = updateInfoRef.current) => {
if (info && info.hash) {
return client.switchVersion(info.hash);
}
},
[client],
);
const switchVersionLater = useCallback(
async (info: CheckResult | undefined = updateInfoRef.current) => {
if (info && info.hash) {
return client.switchVersionLater(info.hash);
}
},
[client],
);
const downloadUpdate = useCallback(
async (info: CheckResult | undefined = updateInfoRef.current) => {
if (!info || !info.update) {
return false;
}
try {
const hash = await client.downloadUpdate(info, setProgress);
if (!hash) {
return false;
}
stateListener.current && stateListener.current.remove();
if (
options.afterDownloadUpdate &&
(await options.afterDownloadUpdate(info)) === false
) {
log('afterDownloadUpdate returned false, skipping');
return false;
}
if (options.updateStrategy === 'silentAndNow') {
client.switchVersion(hash);
return true;
} else if (options.updateStrategy === 'silentAndLater') {
client.switchVersionLater(hash);
return true;
}
alertUpdate('提示', '下载完毕,是否立即更新?', [
{
text: '下次再说',
style: 'cancel',
onPress: () => {
client.switchVersionLater(hash);
},
},
{
text: '立即更新',
style: 'default',
onPress: () => {
client.switchVersion(hash);
},
},
]);
return true;
} catch (e: any) {
setLastError(e);
alertError('更新失败', e.message);
throwErrorIfEnabled(e);
return false;
}
},
[client, options, alertUpdate, alertError, throwErrorIfEnabled],
);
const downloadAndInstallApk = useCallback(
async (downloadUrl: string) => {
if (Platform.OS === 'android' && downloadUrl) {
await client.downloadAndInstallApk(downloadUrl, setProgress);
}
},
[client],
);
const checkUpdate = useCallback(
async ({ extra }: { extra?: Record<string, any> } | undefined = {}) => {
const now = Date.now();
if (lastChecking.current && now - lastChecking.current < 1000) {
return;
}
lastChecking.current = now;
let info: CheckResult;
try {
info = await client.checkUpdate(extra);
} catch (e: any) {
setLastError(e);
alertError('更新检查失败', e.message);
throwErrorIfEnabled(e);
return;
}
if (!info) {
return;
}
const rollout = info.config?.rollout?.[packageVersion];
if (info.update && rollout) {
if (!isInRollout(rollout)) {
log(`not in ${rollout}% rollout, ignored`);
return;
}
log(`in ${rollout}% rollout, continue`);
}
info.description = info.description ?? '';
updateInfoRef.current = info;
setUpdateInfo(info);
if (info.expired) {
if (
options.onPackageExpired &&
(await options.onPackageExpired(info)) === false
) {
log('onPackageExpired returned false, skipping');
return;
}
const { downloadUrl } = info;
if (downloadUrl && Pushy.apkStatus === null) {
if (options.updateStrategy === 'silentAndNow') {
if (Platform.OS === 'android' && downloadUrl.endsWith('.apk')) {
downloadAndInstallApk(downloadUrl);
} else {
Linking.openURL(downloadUrl);
}
return info;
}
alertUpdate('提示', '您的应用版本已更新,点击更新下载安装新版本', [
{
text: '更新',
onPress: () => {
if (Platform.OS === 'android' && downloadUrl.endsWith('.apk')) {
downloadAndInstallApk(downloadUrl);
} else {
Linking.openURL(downloadUrl);
}
},
},
]);
}
} else if (info.update) {
if (
options.updateStrategy === 'silentAndNow' ||
options.updateStrategy === 'silentAndLater'
) {
downloadUpdate(info);
return info;
}
alertUpdate(
'提示',
'检查到新的版本' + info.name + ',是否下载?\n' + info.description,
[
{ text: '取消', style: 'cancel' },
{
text: '确定',
style: 'default',
onPress: () => {
downloadUpdate();
},
},
],
);
}
return info;
},
[
client,
alertError,
throwErrorIfEnabled,
options.updateStrategy,
alertUpdate,
downloadAndInstallApk,
downloadUpdate,
],
);
const markSuccess = client.markSuccess;
useEffect(() => {
if (!client.assertDebug()) {
return;
}
const { checkStrategy, dismissErrorAfter, autoMarkSuccess } = options;
if (autoMarkSuccess) {
setTimeout(() => {
markSuccess();
}, 1000);
}
if (checkStrategy === 'both' || checkStrategy === 'onAppResume') {
stateListener.current = AppState.addEventListener(
'change',
nextAppState => {
if (nextAppState === 'active') {
checkUpdate();
}
},
);
}
if (checkStrategy === 'both' || checkStrategy === 'onAppStart') {
checkUpdate();
}
let dismissErrorTimer: ReturnType<typeof setTimeout>;
if (typeof dismissErrorAfter === 'number' && dismissErrorAfter > 0) {
dismissErrorTimer = setTimeout(() => {
dismissError();
}, dismissErrorAfter);
}
return () => {
stateListener.current && stateListener.current.remove();
clearTimeout(dismissErrorTimer);
};
}, [checkUpdate, options, dismissError, markSuccess, client]);
const parseTestPayload = useCallback(
(payload: UpdateTestPayload) => {
if (payload && payload.type && payload.type.startsWith('__rnPushy')) {
const logger = options.logger || (() => {});
options.logger = ({ type, data }) => {
logger({ type, data });
Alert.alert(type, JSON.stringify(data));
};
if (payload.type === '__rnPushyVersionHash') {
checkUpdate({ extra: { toHash: payload.data } }).then(() => {
if (updateInfoRef.current && updateInfoRef.current.upToDate) {
Alert.alert(
'Info',
'No update found, please wait 10s for the server to generate the patch package',
);
}
options.logger = logger;
});
}
return true;
}
return false;
},
[checkUpdate, options],
);
const parseTestQrCode = useCallback(
(code: string | UpdateTestPayload) => {
try {
const payload = typeof code === 'string' ? JSON.parse(code) : code;
return parseTestPayload(payload);
} catch {
return false;
}
},
[parseTestPayload],
);
const restartApp = useCallback(async () => {
return client.restartApp();
}, [client]);
useEffect(() => {
const parseLinking = (url: string | null) => {
if (!url) {
return;
}
const params = new URL(url).searchParams;
const payload = {
type: params.get('type'),
data: params.get('data'),
};
parseTestPayload(payload);
};
Linking.getInitialURL().then(parseLinking);
const linkingHandler = ({ url }: { url: string }) => {
parseLinking(url);
};
const linkingListener = Linking.addEventListener('url', linkingHandler);
return () => {
if ('removeEventListener' in Linking) {
(Linking as any).removeEventListener('url', linkingHandler);
} else {
linkingListener.remove();
}
};
}, [parseTestPayload]);
return (
<UpdateContext.Provider
value={{
checkUpdate,
switchVersion,
switchVersionLater,
dismissError,
updateInfo,
lastError,
markSuccess,
client,
downloadUpdate,
packageVersion,
currentHash: currentVersion,
progress,
downloadAndInstallApk,
getCurrentVersionInfo,
parseTestQrCode,
restartApp,
}}>
{children}
</UpdateContext.Provider>
);
};
/** @deprecated Please use `UpdateProvider` instead */
export const PushyProvider = UpdateProvider;