mirror of
				https://gitcode.com/gh_mirrors/re/react-native-pushy.git
				synced 2025-10-31 21:33:12 +08:00 
			
		
		
		
	v10
This commit is contained in:
		
							
								
								
									
										29
									
								
								.eslintrc
									
									
									
									
									
								
							
							
						
						
									
										29
									
								
								.eslintrc
									
									
									
									
									
								
							| @@ -1,29 +1,4 @@ | |||||||
| { | { | ||||||
|   "extends": "eslint-config-airbnb/base", |   "root": true, | ||||||
|   "parser": "babel-eslint", |   "extends": "@react-native" | ||||||
|   "env": { |  | ||||||
|     "browser": true, |  | ||||||
|     "node": true, |  | ||||||
|     "mocha": true |  | ||||||
|   }, |  | ||||||
|   "rules": { |  | ||||||
|     // Disable for console/alert |  | ||||||
|     "no-console": 0, |  | ||||||
|     "no-alert": 0, |  | ||||||
|  |  | ||||||
|     "indent": [2, 2, {"SwitchCase": 1}] |  | ||||||
|   }, |  | ||||||
|   "plugins": [ |  | ||||||
|     "import" |  | ||||||
|   ], |  | ||||||
|   "settings": { |  | ||||||
|     "import/parser": "babel-eslint", |  | ||||||
|     "import/resolve": { |  | ||||||
|       "moduleDirectory": ["node_modules", "src"] |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   "globals": { |  | ||||||
|     "__DEV__": true, |  | ||||||
|     "__OPTION__": true |  | ||||||
|   } |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,53 +0,0 @@ | |||||||
| import { logger } from './utils'; |  | ||||||
|  |  | ||||||
| let currentEndpoint = 'https://update.react-native.cn/api'; |  | ||||||
| let backupEndpoints: string[] = ['https://update.reactnative.cn/api']; |  | ||||||
| let backupEndpointsQueryUrl: string | null = null; |  | ||||||
|  |  | ||||||
| export async function updateBackupEndpoints() { |  | ||||||
|   if (backupEndpointsQueryUrl) { |  | ||||||
|     try { |  | ||||||
|       const resp = await fetch(backupEndpointsQueryUrl); |  | ||||||
|       const remoteEndpoints = await resp.json(); |  | ||||||
|       if (Array.isArray(remoteEndpoints)) { |  | ||||||
|         backupEndpoints = Array.from( |  | ||||||
|           new Set([...backupEndpoints, ...remoteEndpoints]), |  | ||||||
|         ); |  | ||||||
|         logger('fetch remote endpoints:', remoteEndpoints); |  | ||||||
|         logger('merged backup endpoints:', backupEndpoints); |  | ||||||
|       } |  | ||||||
|     } catch (e) { |  | ||||||
|       logger('fetch remote endpoints failed'); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|   return backupEndpoints; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export function getCheckUrl(APPKEY, endpoint = currentEndpoint) { |  | ||||||
|   return `${endpoint}/checkUpdate/${APPKEY}`; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * @param {string} main - The main api endpoint |  | ||||||
|  * @param {string[]} [backups] - The back up endpoints. |  | ||||||
|  * @param {string} [backupQueryUrl] - An url that return a json file containing an array of endpoint. |  | ||||||
|  *                                    like: ["https://backup.api/1", "https://backup.api/2"] |  | ||||||
|  */ |  | ||||||
| export function setCustomEndpoints({ |  | ||||||
|   main, |  | ||||||
|   backups, |  | ||||||
|   backupQueryUrl, |  | ||||||
| }: { |  | ||||||
|   main: string; |  | ||||||
|   backups?: string[]; |  | ||||||
|   backupQueryUrl?: string; |  | ||||||
| }) { |  | ||||||
|   currentEndpoint = main; |  | ||||||
|   backupEndpointsQueryUrl = null; |  | ||||||
|   if (Array.isArray(backups) && backups.length > 0) { |  | ||||||
|     backupEndpoints = backups; |  | ||||||
|   } |  | ||||||
|   if (typeof backupQueryUrl === 'string') { |  | ||||||
|     backupEndpointsQueryUrl = backupQueryUrl; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -1,2 +0,0 @@ | |||||||
| export * from './main'; |  | ||||||
| export * from './simpleUpdate'; |  | ||||||
| @@ -1,18 +0,0 @@ | |||||||
| export const downloadRootDir = ''; |  | ||||||
| export const packageVersion = ''; |  | ||||||
| export const currentVersion = ''; |  | ||||||
| export const isFirstTime = false; |  | ||||||
| export const isRolledBack = false; |  | ||||||
|  |  | ||||||
| const noop = () => {}; |  | ||||||
|  |  | ||||||
| export const checkUpdate = noop; |  | ||||||
| export const downloadUpdate = noop; |  | ||||||
| export const switchVersion = noop; |  | ||||||
| export const switchVersionLater = noop; |  | ||||||
| export const markSuccess = noop; |  | ||||||
| export const downloadAndInstallApk = noop; |  | ||||||
| export const setCustomEndpoints = noop; |  | ||||||
| export const getCurrentVersionInfo = noop; |  | ||||||
| export const simpleUpdate = (app) => app; |  | ||||||
| export const onPushyEvents = noop; |  | ||||||
							
								
								
									
										398
									
								
								lib/main.ts
									
									
									
									
									
								
							
							
						
						
									
										398
									
								
								lib/main.ts
									
									
									
									
									
								
							| @@ -1,398 +0,0 @@ | |||||||
| import { |  | ||||||
|   updateBackupEndpoints, |  | ||||||
|   getCheckUrl, |  | ||||||
|   setCustomEndpoints, |  | ||||||
| } from './endpoint'; |  | ||||||
| import { |  | ||||||
|   NativeEventEmitter, |  | ||||||
|   NativeModules, |  | ||||||
|   Platform, |  | ||||||
|   PermissionsAndroid, |  | ||||||
| } from 'react-native'; |  | ||||||
| import { |  | ||||||
|   CheckResult, |  | ||||||
|   EventType, |  | ||||||
|   ProgressData, |  | ||||||
|   UpdateAvailableResult, |  | ||||||
|   UpdateEventsListener, |  | ||||||
| } from './type'; |  | ||||||
| import { assertRelease, logger } from './utils'; |  | ||||||
| export { setCustomEndpoints }; |  | ||||||
| const { |  | ||||||
|   version: v, |  | ||||||
| } = require('react-native/Libraries/Core/ReactNativeVersion'); |  | ||||||
| const RNVersion = `${v.major}.${v.minor}.${v.patch}`; |  | ||||||
| const isTurboModuleEnabled = global.__turboModuleProxy != null; |  | ||||||
|  |  | ||||||
| export const PushyModule = isTurboModuleEnabled |  | ||||||
|   ? require('./NativeUpdate').default |  | ||||||
|   : NativeModules.Pushy; |  | ||||||
|  |  | ||||||
| if (!PushyModule) { |  | ||||||
|   throw new Error('react-native-update模块无法加载,请对照安装文档检查配置。'); |  | ||||||
| } |  | ||||||
| const PushyConstants = isTurboModuleEnabled |  | ||||||
|   ? PushyModule.getConstants() |  | ||||||
|   : PushyModule; |  | ||||||
|  |  | ||||||
| export const downloadRootDir = PushyConstants.downloadRootDir; |  | ||||||
| export const packageVersion = PushyConstants.packageVersion; |  | ||||||
| export const currentVersion = PushyConstants.currentVersion; |  | ||||||
| export const isFirstTime = PushyConstants.isFirstTime; |  | ||||||
| const rolledBackVersion = PushyConstants.rolledBackVersion; |  | ||||||
| export const isRolledBack = typeof rolledBackVersion === 'string'; |  | ||||||
|  |  | ||||||
| export const buildTime = PushyConstants.buildTime; |  | ||||||
| let blockUpdate = PushyConstants.blockUpdate; |  | ||||||
| let uuid = PushyConstants.uuid; |  | ||||||
|  |  | ||||||
| if (Platform.OS === 'android' && !PushyConstants.isUsingBundleUrl) { |  | ||||||
|   throw new Error( |  | ||||||
|     'react-native-update模块无法加载,请对照文档检查Bundle URL的配置', |  | ||||||
|   ); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function setLocalHashInfo(hash: string, info: Record<string, any>) { |  | ||||||
|   PushyModule.setLocalHashInfo(hash, JSON.stringify(info)); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| async function getLocalHashInfo(hash: string) { |  | ||||||
|   return JSON.parse(await PushyModule.getLocalHashInfo(hash)); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export async function getCurrentVersionInfo(): Promise<{ |  | ||||||
|   name?: string; |  | ||||||
|   description?: string; |  | ||||||
|   metaInfo?: string; |  | ||||||
| }> { |  | ||||||
|   return currentVersion ? (await getLocalHashInfo(currentVersion)) || {} : {}; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| const eventEmitter = new NativeEventEmitter(PushyModule); |  | ||||||
|  |  | ||||||
| if (!uuid) { |  | ||||||
|   uuid = require('nanoid/non-secure').nanoid(); |  | ||||||
|   PushyModule.setUuid(uuid); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| const noop = () => {}; |  | ||||||
| let reporter: UpdateEventsListener = noop; |  | ||||||
|  |  | ||||||
| export function onPushyEvents(customReporter: UpdateEventsListener) { |  | ||||||
|   reporter = customReporter; |  | ||||||
|   if (isRolledBack) { |  | ||||||
|     report({ |  | ||||||
|       type: 'rollback', |  | ||||||
|       data: { |  | ||||||
|         rolledBackVersion, |  | ||||||
|       }, |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function report({ |  | ||||||
|   type, |  | ||||||
|   message = '', |  | ||||||
|   data = {}, |  | ||||||
| }: { |  | ||||||
|   type: EventType; |  | ||||||
|   message?: string; |  | ||||||
|   data?: Record<string, string | number>; |  | ||||||
| }) { |  | ||||||
|   logger(type + ' ' + message); |  | ||||||
|   reporter({ |  | ||||||
|     type, |  | ||||||
|     data: { |  | ||||||
|       currentVersion, |  | ||||||
|       cInfo, |  | ||||||
|       packageVersion, |  | ||||||
|       buildTime, |  | ||||||
|       message, |  | ||||||
|       ...data, |  | ||||||
|     }, |  | ||||||
|   }); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| logger('uuid: ' + uuid); |  | ||||||
|  |  | ||||||
| export const cInfo = { |  | ||||||
|   pushy: require('../package.json').version, |  | ||||||
|   rn: RNVersion, |  | ||||||
|   os: Platform.OS + ' ' + Platform.Version, |  | ||||||
|   uuid, |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| let lastChecking; |  | ||||||
| const empty = {}; |  | ||||||
| let lastResult: CheckResult; |  | ||||||
| export async function checkUpdate(APPKEY: string) { |  | ||||||
|   assertRelease(); |  | ||||||
|   const now = Date.now(); |  | ||||||
|   if (lastResult && lastChecking && now - lastChecking < 1000 * 60) { |  | ||||||
|     // logger('repeated checking, ignored'); |  | ||||||
|     return lastResult; |  | ||||||
|   } |  | ||||||
|   lastChecking = now; |  | ||||||
|   if (blockUpdate && blockUpdate.until > Date.now() / 1000) { |  | ||||||
|     report({ |  | ||||||
|       type: 'errorChecking', |  | ||||||
|       message: `热更新已暂停,原因:${blockUpdate.reason}。请在"${new Date( |  | ||||||
|         blockUpdate.until * 1000, |  | ||||||
|       ).toLocaleString()}"之后重试。`, |  | ||||||
|     }); |  | ||||||
|     return lastResult || empty; |  | ||||||
|   } |  | ||||||
|   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(getCheckUrl(APPKEY), fetchPayload); |  | ||||||
|   } catch (e) { |  | ||||||
|     report({ |  | ||||||
|       type: 'errorChecking', |  | ||||||
|       message: '无法连接主更新服务器,尝试备用节点', |  | ||||||
|     }); |  | ||||||
|     const backupEndpoints = await updateBackupEndpoints(); |  | ||||||
|     if (backupEndpoints) { |  | ||||||
|       try { |  | ||||||
|         resp = await Promise.race( |  | ||||||
|           backupEndpoints.map((endpoint) => |  | ||||||
|             fetch(getCheckUrl(APPKEY, endpoint), fetchPayload), |  | ||||||
|           ), |  | ||||||
|         ); |  | ||||||
|       } catch {} |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|   if (!resp) { |  | ||||||
|     report({ |  | ||||||
|       type: 'errorChecking', |  | ||||||
|       message: '无法连接更新服务器,请检查网络连接后重试', |  | ||||||
|     }); |  | ||||||
|     return lastResult || empty; |  | ||||||
|   } |  | ||||||
|   const result: CheckResult = await resp.json(); |  | ||||||
|  |  | ||||||
|   lastResult = result; |  | ||||||
|   // @ts-ignore |  | ||||||
|   checkOperation(result.op); |  | ||||||
|  |  | ||||||
|   if (resp.status !== 200) { |  | ||||||
|     report({ |  | ||||||
|       type: 'errorChecking', |  | ||||||
|       //@ts-ignore |  | ||||||
|       message: result.message, |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   return result; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function checkOperation( |  | ||||||
|   op: { type: string; reason: string; duration: number }[], |  | ||||||
| ) { |  | ||||||
|   if (!Array.isArray(op)) { |  | ||||||
|     return; |  | ||||||
|   } |  | ||||||
|   op.forEach((action) => { |  | ||||||
|     if (action.type === 'block') { |  | ||||||
|       blockUpdate = { |  | ||||||
|         reason: action.reason, |  | ||||||
|         until: Math.round((Date.now() + action.duration) / 1000), |  | ||||||
|       }; |  | ||||||
|       PushyModule.setBlockUpdate(blockUpdate); |  | ||||||
|     } |  | ||||||
|   }); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| let downloadingThrottling = false; |  | ||||||
| let downloadedHash: string; |  | ||||||
| export async function downloadUpdate( |  | ||||||
|   options: UpdateAvailableResult, |  | ||||||
|   eventListeners?: { |  | ||||||
|     onDownloadProgress?: (data: ProgressData) => void; |  | ||||||
|   }, |  | ||||||
| ) { |  | ||||||
|   assertRelease(); |  | ||||||
|   if (!options.update) { |  | ||||||
|     return; |  | ||||||
|   } |  | ||||||
|   if (rolledBackVersion === options.hash) { |  | ||||||
|     logger(`rolledback hash ${rolledBackVersion}, ignored`); |  | ||||||
|     return; |  | ||||||
|   } |  | ||||||
|   if (downloadedHash === options.hash) { |  | ||||||
|     logger(`duplicated downloaded hash ${downloadedHash}, ignored`); |  | ||||||
|     return downloadedHash; |  | ||||||
|   } |  | ||||||
|   if (downloadingThrottling) { |  | ||||||
|     logger('repeated downloading, ignored'); |  | ||||||
|     return; |  | ||||||
|   } |  | ||||||
|   downloadingThrottling = true; |  | ||||||
|   setTimeout(() => { |  | ||||||
|     downloadingThrottling = false; |  | ||||||
|   }, 3000); |  | ||||||
|   let progressHandler; |  | ||||||
|   if (eventListeners) { |  | ||||||
|     if (eventListeners.onDownloadProgress) { |  | ||||||
|       const downloadCallback = eventListeners.onDownloadProgress; |  | ||||||
|       progressHandler = eventEmitter.addListener( |  | ||||||
|         'RCTPushyDownloadProgress', |  | ||||||
|         (progressData) => { |  | ||||||
|           if (progressData.hash === options.hash) { |  | ||||||
|             downloadCallback(progressData); |  | ||||||
|           } |  | ||||||
|         }, |  | ||||||
|       ); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|   let succeeded = false; |  | ||||||
|   report({ type: 'downloading' }); |  | ||||||
|   if (options.diffUrl) { |  | ||||||
|     logger('downloading diff'); |  | ||||||
|     try { |  | ||||||
|       await PushyModule.downloadPatchFromPpk({ |  | ||||||
|         updateUrl: options.diffUrl, |  | ||||||
|         hash: options.hash, |  | ||||||
|         originHash: currentVersion, |  | ||||||
|       }); |  | ||||||
|       succeeded = true; |  | ||||||
|     } catch (e) { |  | ||||||
|       logger(`diff error: ${e.message}, try pdiff`); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|   if (!succeeded && options.pdiffUrl) { |  | ||||||
|     logger('downloading pdiff'); |  | ||||||
|     try { |  | ||||||
|       await PushyModule.downloadPatchFromPackage({ |  | ||||||
|         updateUrl: options.pdiffUrl, |  | ||||||
|         hash: options.hash, |  | ||||||
|       }); |  | ||||||
|       succeeded = true; |  | ||||||
|     } catch (e) { |  | ||||||
|       logger(`pdiff error: ${e.message}, try full patch`); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|   if (!succeeded && options.updateUrl) { |  | ||||||
|     logger('downloading full patch'); |  | ||||||
|     try { |  | ||||||
|       await PushyModule.downloadFullUpdate({ |  | ||||||
|         updateUrl: options.updateUrl, |  | ||||||
|         hash: options.hash, |  | ||||||
|       }); |  | ||||||
|       succeeded = true; |  | ||||||
|     } catch (e) { |  | ||||||
|       logger(`full patch error: ${e.message}`); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|   progressHandler && progressHandler.remove(); |  | ||||||
|   if (!succeeded) { |  | ||||||
|     return report({ type: 'errorUpdate', data: { newVersion: options.hash } }); |  | ||||||
|   } |  | ||||||
|   setLocalHashInfo(options.hash, { |  | ||||||
|     name: options.name, |  | ||||||
|     description: options.description, |  | ||||||
|     metaInfo: options.metaInfo, |  | ||||||
|   }); |  | ||||||
|   downloadedHash = options.hash; |  | ||||||
|   return options.hash; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function assertHash(hash: string) { |  | ||||||
|   if (!downloadedHash) { |  | ||||||
|     logger(`no downloaded hash`); |  | ||||||
|     return; |  | ||||||
|   } |  | ||||||
|   if (hash !== downloadedHash) { |  | ||||||
|     logger(`use downloaded hash ${downloadedHash} first`); |  | ||||||
|     return; |  | ||||||
|   } |  | ||||||
|   return true; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| let applyingUpdate = false; |  | ||||||
| export function switchVersion(hash: string) { |  | ||||||
|   assertRelease(); |  | ||||||
|   if (assertHash(hash) && !applyingUpdate) { |  | ||||||
|     logger('switchVersion: ' + hash); |  | ||||||
|     applyingUpdate = true; |  | ||||||
|     PushyModule.reloadUpdate({ hash }); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export function switchVersionLater(hash: string) { |  | ||||||
|   assertRelease(); |  | ||||||
|   if (assertHash(hash)) { |  | ||||||
|     logger('switchVersionLater: ' + hash); |  | ||||||
|     PushyModule.setNeedUpdate({ hash }); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| let marked = false; |  | ||||||
| export function markSuccess() { |  | ||||||
|   assertRelease(); |  | ||||||
|   if (marked) { |  | ||||||
|     logger('repeated markSuccess, ignored'); |  | ||||||
|     return; |  | ||||||
|   } |  | ||||||
|   marked = true; |  | ||||||
|   PushyModule.markSuccess(); |  | ||||||
|   report({ type: 'markSuccess' }); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export async function downloadAndInstallApk({ |  | ||||||
|   url, |  | ||||||
|   onDownloadProgress, |  | ||||||
| }: { |  | ||||||
|   url: string; |  | ||||||
|   onDownloadProgress?: (data: ProgressData) => void; |  | ||||||
| }) { |  | ||||||
|   if (Platform.OS !== 'android') { |  | ||||||
|     return; |  | ||||||
|   } |  | ||||||
|   report({ type: 'downloadingApk' }); |  | ||||||
|   if (Platform.Version <= 23) { |  | ||||||
|     try { |  | ||||||
|       const granted = await PermissionsAndroid.request( |  | ||||||
|         PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE, |  | ||||||
|       ); |  | ||||||
|       if (granted !== PermissionsAndroid.RESULTS.GRANTED) { |  | ||||||
|         return report({ type: 'rejectStoragePermission' }); |  | ||||||
|       } |  | ||||||
|     } catch (err) { |  | ||||||
|       return report({ type: 'errorStoragePermission' }); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|   let hash = Date.now().toString(); |  | ||||||
|   let progressHandler; |  | ||||||
|   if (onDownloadProgress) { |  | ||||||
|     progressHandler = eventEmitter.addListener( |  | ||||||
|       'RCTPushyDownloadProgress', |  | ||||||
|       (progressData: ProgressData) => { |  | ||||||
|         if (progressData.hash === hash) { |  | ||||||
|           onDownloadProgress(progressData); |  | ||||||
|         } |  | ||||||
|       }, |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|   await PushyModule.downloadAndInstallApk({ |  | ||||||
|     url, |  | ||||||
|     target: 'update.apk', |  | ||||||
|     hash, |  | ||||||
|   }).catch(() => { |  | ||||||
|     report({ type: 'errowDownloadAndInstallApk' }); |  | ||||||
|   }); |  | ||||||
|   progressHandler && progressHandler.remove(); |  | ||||||
| } |  | ||||||
| @@ -1,135 +0,0 @@ | |||||||
| import React, { PureComponent, ComponentType } from 'react'; |  | ||||||
| import { |  | ||||||
|   Platform, |  | ||||||
|   Alert, |  | ||||||
|   Linking, |  | ||||||
|   AppState, |  | ||||||
|   NativeEventSubscription, |  | ||||||
| } from 'react-native'; |  | ||||||
|  |  | ||||||
| import { |  | ||||||
|   isFirstTime, |  | ||||||
|   isRolledBack, |  | ||||||
|   checkUpdate, |  | ||||||
|   downloadUpdate, |  | ||||||
|   switchVersion, |  | ||||||
|   switchVersionLater, |  | ||||||
|   markSuccess, |  | ||||||
|   downloadAndInstallApk, |  | ||||||
|   onPushyEvents, |  | ||||||
| } from './main'; |  | ||||||
| import { UpdateEventsListener } from './type'; |  | ||||||
|  |  | ||||||
| export function simpleUpdate( |  | ||||||
|   WrappedComponent: ComponentType, |  | ||||||
|   options: { appKey?: string; onPushyEvents?: UpdateEventsListener } = {}, |  | ||||||
| ) { |  | ||||||
|   const { appKey, onPushyEvents: eventListeners } = options; |  | ||||||
|   if (!appKey) { |  | ||||||
|     throw new Error('appKey is required for simpleUpdate()'); |  | ||||||
|   } |  | ||||||
|   if (typeof eventListeners === 'function') { |  | ||||||
|     onPushyEvents(eventListeners); |  | ||||||
|   } |  | ||||||
|   return __DEV__ |  | ||||||
|     ? WrappedComponent |  | ||||||
|     : class AppUpdate extends PureComponent { |  | ||||||
|         stateListener: NativeEventSubscription; |  | ||||||
|         componentDidMount() { |  | ||||||
|           if (isRolledBack) { |  | ||||||
|             Alert.alert('抱歉', '刚刚更新遭遇错误,已为您恢复到更新前版本'); |  | ||||||
|           } else if (isFirstTime) { |  | ||||||
|             markSuccess(); |  | ||||||
|           } |  | ||||||
|           this.stateListener = AppState.addEventListener( |  | ||||||
|             'change', |  | ||||||
|             (nextAppState) => { |  | ||||||
|               if (nextAppState === 'active') { |  | ||||||
|                 this.checkUpdate(); |  | ||||||
|               } |  | ||||||
|             }, |  | ||||||
|           ); |  | ||||||
|           this.checkUpdate(); |  | ||||||
|         } |  | ||||||
|         componentWillUnmount() { |  | ||||||
|           this.stateListener && this.stateListener.remove(); |  | ||||||
|         } |  | ||||||
|         doUpdate = async (info) => { |  | ||||||
|           try { |  | ||||||
|             const hash = await downloadUpdate(info); |  | ||||||
|             if (!hash) { |  | ||||||
|               return; |  | ||||||
|             } |  | ||||||
|             this.stateListener && this.stateListener.remove(); |  | ||||||
|             Alert.alert('提示', '下载完毕,是否立即更新?', [ |  | ||||||
|               { |  | ||||||
|                 text: '以后再说', |  | ||||||
|                 style: 'cancel', |  | ||||||
|                 onPress: () => { |  | ||||||
|                   switchVersionLater(hash); |  | ||||||
|                 }, |  | ||||||
|               }, |  | ||||||
|               { |  | ||||||
|                 text: '立即更新', |  | ||||||
|                 style: 'default', |  | ||||||
|                 onPress: () => { |  | ||||||
|                   switchVersion(hash); |  | ||||||
|                 }, |  | ||||||
|               }, |  | ||||||
|             ]); |  | ||||||
|           } catch (err) { |  | ||||||
|             Alert.alert('更新失败', err.message); |  | ||||||
|           } |  | ||||||
|         }; |  | ||||||
|  |  | ||||||
|         checkUpdate = async () => { |  | ||||||
|           let info; |  | ||||||
|           try { |  | ||||||
|             info = await checkUpdate(appKey!); |  | ||||||
|           } catch (err) { |  | ||||||
|             Alert.alert('更新检查失败', err.message); |  | ||||||
|             return; |  | ||||||
|           } |  | ||||||
|           if (info.expired) { |  | ||||||
|             Alert.alert('提示', '您的应用版本已更新,点击确定下载安装新版本', [ |  | ||||||
|               { |  | ||||||
|                 text: '确定', |  | ||||||
|                 onPress: () => { |  | ||||||
|                   if (info.downloadUrl) { |  | ||||||
|                     if ( |  | ||||||
|                       Platform.OS === 'android' && |  | ||||||
|                       info.downloadUrl.endsWith('.apk') |  | ||||||
|                     ) { |  | ||||||
|                       downloadAndInstallApk({ |  | ||||||
|                         url: info.downloadUrl, |  | ||||||
|                       }); |  | ||||||
|                     } else { |  | ||||||
|                       Linking.openURL(info.downloadUrl); |  | ||||||
|                     } |  | ||||||
|                   } |  | ||||||
|                 }, |  | ||||||
|               }, |  | ||||||
|             ]); |  | ||||||
|           } else if (info.update) { |  | ||||||
|             Alert.alert( |  | ||||||
|               '提示', |  | ||||||
|               '检查到新的版本' + info.name + ',是否下载?\n' + info.description, |  | ||||||
|               [ |  | ||||||
|                 { text: '否', style: 'cancel' }, |  | ||||||
|                 { |  | ||||||
|                   text: '是', |  | ||||||
|                   style: 'default', |  | ||||||
|                   onPress: () => { |  | ||||||
|                     this.doUpdate(info); |  | ||||||
|                   }, |  | ||||||
|                 }, |  | ||||||
|               ], |  | ||||||
|             ); |  | ||||||
|           } |  | ||||||
|         }; |  | ||||||
|  |  | ||||||
|         render() { |  | ||||||
|           return <WrappedComponent {...this.props} />; |  | ||||||
|         } |  | ||||||
|       }; |  | ||||||
| } |  | ||||||
							
								
								
									
										12
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								package.json
									
									
									
									
									
								
							| @@ -55,17 +55,21 @@ | |||||||
|     ] |     ] | ||||||
|   }, |   }, | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|  |     "@react-native/eslint-config": "^0.73.2", | ||||||
|  |     "@types/react": "^18.2.46", | ||||||
|  |     "eslint": "^8.56.0", | ||||||
|  |     "prettier": "^2", | ||||||
|  |     "react": "18.2.0", | ||||||
|  |     "react-native": "0.73", | ||||||
|  |     "typescript": "^5.3.3", | ||||||
|     "@types/fs-extra": "^9.0.13", |     "@types/fs-extra": "^9.0.13", | ||||||
|     "@types/jest": "^29.2.1", |     "@types/jest": "^29.2.1", | ||||||
|     "@types/node": "^20.8.9", |     "@types/node": "^20.8.9", | ||||||
|     "@types/react": "^18.2.33", |  | ||||||
|     "detox": "^20.5.0", |     "detox": "^20.5.0", | ||||||
|     "firebase-tools": "^11.24.1", |     "firebase-tools": "^11.24.1", | ||||||
|     "fs-extra": "^9.1.0", |     "fs-extra": "^9.1.0", | ||||||
|     "jest": "^29.2.1", |     "jest": "^29.2.1", | ||||||
|     "pod-install": "^0.1.37", |     "pod-install": "^0.1.37", | ||||||
|     "react-native": "^0.72.6", |     "ts-jest": "^29.0.3" | ||||||
|     "ts-jest": "^29.0.3", |  | ||||||
|     "typescript": "^5.2.2" |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,5 +1,7 @@ | |||||||
| require 'json' | require 'json' | ||||||
|  |  | ||||||
|  | new_arch_enabled = ENV['RCT_NEW_ARCH_ENABLED'] == '1' | ||||||
|  |  | ||||||
| package = JSON.parse(File.read(File.join(__dir__, 'package.json'))) | package = JSON.parse(File.read(File.join(__dir__, 'package.json'))) | ||||||
|  |  | ||||||
| Pod::Spec.new do |s| | Pod::Spec.new do |s| | ||||||
| @@ -40,5 +42,25 @@ Pod::Spec.new do |s| | |||||||
|                        'android/jni/lzma/C/Lzma2Dec.{h,c}'] |                        'android/jni/lzma/C/Lzma2Dec.{h,c}'] | ||||||
|     ss.private_header_files = 'ios/RCTPushy/HDiffPatch/**/*.h' |     ss.private_header_files = 'ios/RCTPushy/HDiffPatch/**/*.h' | ||||||
|   end |   end | ||||||
|   install_modules_dependencies(s) |    | ||||||
|  |   if defined?(install_modules_dependencies()) != nil | ||||||
|  |     install_modules_dependencies(s); | ||||||
|  |   else | ||||||
|  |     if new_arch_enabled | ||||||
|  |       folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32' | ||||||
|  |  | ||||||
|  |       s.compiler_flags = folly_compiler_flags + " -DRCT_NEW_ARCH_ENABLED=1" | ||||||
|  |  | ||||||
|  |       s.pod_target_xcconfig = { | ||||||
|  |           "HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost\"", | ||||||
|  |           "CLANG_CXX_LANGUAGE_STANDARD" => "c++17" | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       s.dependency "React-Codegen" | ||||||
|  |       s.dependency "RCT-Folly" | ||||||
|  |       s.dependency "RCTRequired" | ||||||
|  |       s.dependency "RCTTypeSafety" | ||||||
|  |       s.dependency "ReactCommon/turbomodule/core" | ||||||
|  |     end | ||||||
|  |   end | ||||||
| end | end | ||||||
|   | |||||||
							
								
								
									
										311
									
								
								src/client.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										311
									
								
								src/client.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,311 @@ | |||||||
|  | import { CheckResult, PushyOptions, ProgressData } from './type'; | ||||||
|  | import { assertRelease, log } from './utils'; | ||||||
|  | import { | ||||||
|  |   EmitterSubscription, | ||||||
|  |   PermissionsAndroid, | ||||||
|  |   Platform, | ||||||
|  | } from 'react-native'; | ||||||
|  | import { | ||||||
|  |   PushyModule, | ||||||
|  |   buildTime, | ||||||
|  |   cInfo, | ||||||
|  |   pushyNativeEventEmitter, | ||||||
|  |   currentVersion, | ||||||
|  |   packageVersion, | ||||||
|  |   report, | ||||||
|  |   rolledBackVersion, | ||||||
|  |   setLocalHashInfo, | ||||||
|  | } 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 = {}; | ||||||
|  | export class Pushy { | ||||||
|  |   options: PushyOptions = { | ||||||
|  |     appKey: '', | ||||||
|  |     server: defaultServer, | ||||||
|  |     autoMarkSuccess: true, | ||||||
|  |     useAlert: true, | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   lastChecking: number; | ||||||
|  |   lastResult: CheckResult; | ||||||
|  |  | ||||||
|  |   progressHandlers: Record<string, EmitterSubscription> = {}; | ||||||
|  |   downloadedHash: string; | ||||||
|  |  | ||||||
|  |   marked = false; | ||||||
|  |   applyingUpdate = false; | ||||||
|  |  | ||||||
|  |   constructor(options: PushyOptions) { | ||||||
|  |     if (!options.appKey) { | ||||||
|  |       throw new Error('appKey is required for withUpdates()'); | ||||||
|  |     } | ||||||
|  |     for (const [key, value] of Object.entries(options)) { | ||||||
|  |       if (value !== undefined) { | ||||||
|  |         this.options[key] = value; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   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(); | ||||||
|  |     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; | ||||||
|  |     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) { | ||||||
|  |       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) { | ||||||
|  |       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) { | ||||||
|  |       report({ | ||||||
|  |         type: 'errorChecking', | ||||||
|  |         //@ts-ignore | ||||||
|  |         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(); | ||||||
|  |     if (!('update' in info)) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     const { hash, diffUrl, pdiffUrl, updateUrl, name, description, metaInfo } = | ||||||
|  |       info; | ||||||
|  |     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; | ||||||
|  |     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 report({ | ||||||
|  |         type: 'errorUpdate', | ||||||
|  |         data: { newVersion: hash }, | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |     setLocalHashInfo(hash, { | ||||||
|  |       name, | ||||||
|  |       description, | ||||||
|  |       metaInfo, | ||||||
|  |     }); | ||||||
|  |     this.downloadedHash = hash; | ||||||
|  |     return hash; | ||||||
|  |   }; | ||||||
|  |   downloadAndInstallApk = async ( | ||||||
|  |     url: string, | ||||||
|  |     onDownloadProgress?: (data: ProgressData) => void, | ||||||
|  |   ) => { | ||||||
|  |     if (Platform.OS !== 'android') { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     report({ type: 'downloadingApk' }); | ||||||
|  |     if (Platform.Version <= 23) { | ||||||
|  |       try { | ||||||
|  |         const granted = await PermissionsAndroid.request( | ||||||
|  |           PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE, | ||||||
|  |         ); | ||||||
|  |         if (granted !== PermissionsAndroid.RESULTS.GRANTED) { | ||||||
|  |           return report({ type: 'rejectStoragePermission' }); | ||||||
|  |         } | ||||||
|  |       } catch (err) { | ||||||
|  |         return 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(() => { | ||||||
|  |       report({ type: 'errowDownloadAndInstallApk' }); | ||||||
|  |     }); | ||||||
|  |     if (this.progressHandlers[progressKey]) { | ||||||
|  |       this.progressHandlers[progressKey].remove(); | ||||||
|  |       delete this.progressHandlers[progressKey]; | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  | } | ||||||
							
								
								
									
										26
									
								
								src/context.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								src/context.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | |||||||
|  | import { createContext, useContext } from 'react'; | ||||||
|  | import { CheckResult, ProgressData } from './type'; | ||||||
|  |  | ||||||
|  | const empty = {}; | ||||||
|  | const noop = () => {}; | ||||||
|  |  | ||||||
|  | export const defaultContext = { | ||||||
|  |   checkUpdate: () => Promise.resolve(empty), | ||||||
|  |   switchVersion: noop, | ||||||
|  |   switchVersionLater: noop, | ||||||
|  |   markSuccess: noop, | ||||||
|  |   dismissError: noop, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export const PushyContext = createContext<{ | ||||||
|  |   checkUpdate: () => void; | ||||||
|  |   switchVersion: () => void; | ||||||
|  |   switchVersionLater: () => void; | ||||||
|  |   progress?: ProgressData; | ||||||
|  |   markSuccess: () => void; | ||||||
|  |   updateInfo?: CheckResult; | ||||||
|  |   lastError?: Error; | ||||||
|  |   dismissError: () => void; | ||||||
|  | }>(defaultContext); | ||||||
|  |  | ||||||
|  | export const usePushy = () => useContext(PushyContext); | ||||||
							
								
								
									
										106
									
								
								src/core.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								src/core.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,106 @@ | |||||||
|  | import { NativeEventEmitter, NativeModules, Platform } from 'react-native'; | ||||||
|  | import { EventType, UpdateEventsLogger } from './type'; | ||||||
|  | import { log } from './utils'; | ||||||
|  | const { | ||||||
|  |   version: v, | ||||||
|  | } = require('react-native/Libraries/Core/ReactNativeVersion'); | ||||||
|  | const RNVersion = `${v.major}.${v.minor}.${v.patch}`; | ||||||
|  | const isTurboModuleEnabled = global.__turboModuleProxy != null; | ||||||
|  |  | ||||||
|  | export const PushyModule = isTurboModuleEnabled | ||||||
|  |   ? require('./turboModuleSpec').default | ||||||
|  |   : NativeModules.Pushy; | ||||||
|  |  | ||||||
|  | if (!PushyModule) { | ||||||
|  |   throw new Error('react-native-update模块无法加载,请对照安装文档检查配置。'); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const PushyConstants = isTurboModuleEnabled | ||||||
|  |   ? PushyModule.getConstants() | ||||||
|  |   : PushyModule; | ||||||
|  |  | ||||||
|  | export const downloadRootDir = PushyConstants.downloadRootDir; | ||||||
|  | export const packageVersion = PushyConstants.packageVersion; | ||||||
|  | export const currentVersion = PushyConstants.currentVersion; | ||||||
|  | export const isFirstTime = PushyConstants.isFirstTime; | ||||||
|  | export const rolledBackVersion = PushyConstants.rolledBackVersion; | ||||||
|  | export const isRolledBack = typeof rolledBackVersion === 'string'; | ||||||
|  |  | ||||||
|  | export const buildTime = PushyConstants.buildTime; | ||||||
|  | let uuid = PushyConstants.uuid; | ||||||
|  |  | ||||||
|  | if (Platform.OS === 'android' && !PushyConstants.isUsingBundleUrl) { | ||||||
|  |   throw new Error( | ||||||
|  |     'react-native-update模块无法加载,请对照文档检查Bundle URL的配置', | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export function setLocalHashInfo(hash: string, info: Record<string, any>) { | ||||||
|  |   PushyModule.setLocalHashInfo(hash, JSON.stringify(info)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function getLocalHashInfo(hash: string) { | ||||||
|  |   return JSON.parse(await PushyModule.getLocalHashInfo(hash)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export async function getCurrentVersionInfo(): Promise<{ | ||||||
|  |   name?: string; | ||||||
|  |   description?: string; | ||||||
|  |   metaInfo?: string; | ||||||
|  | }> { | ||||||
|  |   return currentVersion ? (await getLocalHashInfo(currentVersion)) || {} : {}; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export const pushyNativeEventEmitter = new NativeEventEmitter(PushyModule); | ||||||
|  |  | ||||||
|  | if (!uuid) { | ||||||
|  |   uuid = require('nanoid/non-secure').nanoid(); | ||||||
|  |   PushyModule.setUuid(uuid); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const noop = () => {}; | ||||||
|  | let reporter: UpdateEventsLogger = noop; | ||||||
|  |  | ||||||
|  | export function onPushyEvents(customReporter: UpdateEventsLogger) { | ||||||
|  |   reporter = customReporter; | ||||||
|  |   if (isRolledBack) { | ||||||
|  |     report({ | ||||||
|  |       type: 'rollback', | ||||||
|  |       data: { | ||||||
|  |         rolledBackVersion, | ||||||
|  |       }, | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export function report({ | ||||||
|  |   type, | ||||||
|  |   message = '', | ||||||
|  |   data = {}, | ||||||
|  | }: { | ||||||
|  |   type: EventType; | ||||||
|  |   message?: string; | ||||||
|  |   data?: Record<string, string | number>; | ||||||
|  | }) { | ||||||
|  |   log(type + ' ' + message); | ||||||
|  |   reporter({ | ||||||
|  |     type, | ||||||
|  |     data: { | ||||||
|  |       currentVersion, | ||||||
|  |       cInfo, | ||||||
|  |       packageVersion, | ||||||
|  |       buildTime, | ||||||
|  |       message, | ||||||
|  |       ...data, | ||||||
|  |     }, | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | log('uuid: ' + uuid); | ||||||
|  |  | ||||||
|  | export const cInfo = { | ||||||
|  |   pushy: require('../package.json').version, | ||||||
|  |   rn: RNVersion, | ||||||
|  |   os: Platform.OS + ' ' + Platform.Version, | ||||||
|  |   uuid, | ||||||
|  | }; | ||||||
							
								
								
									
										3
									
								
								src/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | export { Pushy } from './client'; | ||||||
|  | export { PushyContext, usePushy } from './context'; | ||||||
|  | export { PushyProvider } from './provider'; | ||||||
							
								
								
									
										17
									
								
								src/index.web.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/index.web.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | |||||||
|  | import { Fragment } from 'react'; | ||||||
|  |  | ||||||
|  | const noop = () => {}; | ||||||
|  | export class Pushy { | ||||||
|  |   constructor() { | ||||||
|  |     console.warn('react-native-update is not supported and will do nothing on web.'); | ||||||
|  |     return new Proxy(this, { | ||||||
|  |       get() { | ||||||
|  |         return noop; | ||||||
|  |       }, | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export { PushyContext, usePushy } from './context'; | ||||||
|  |  | ||||||
|  | export const PushyProvider = Fragment; | ||||||
							
								
								
									
										189
									
								
								src/provider.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										189
									
								
								src/provider.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,189 @@ | |||||||
|  | import React, { | ||||||
|  |   ReactNode, | ||||||
|  |   useCallback, | ||||||
|  |   useEffect, | ||||||
|  |   useRef, | ||||||
|  |   useState, | ||||||
|  | } from 'react'; | ||||||
|  | import { | ||||||
|  |   Alert, | ||||||
|  |   NativeEventSubscription, | ||||||
|  |   AppState, | ||||||
|  |   Platform, | ||||||
|  |   Linking, | ||||||
|  | } from 'react-native'; | ||||||
|  | import { Pushy } from './client'; | ||||||
|  | import { isFirstTime } from './core'; | ||||||
|  | import { UpdateAvailableResult, CheckResult } from './type'; | ||||||
|  | import { PushyContext } from './context'; | ||||||
|  |  | ||||||
|  | export const PushyProvider = ({ | ||||||
|  |   client, | ||||||
|  |   children, | ||||||
|  | }: { | ||||||
|  |   client: Pushy; | ||||||
|  |   children: ReactNode; | ||||||
|  | }) => { | ||||||
|  |   const { strategy, useAlert } = client.options; | ||||||
|  |   const stateListener = useRef<NativeEventSubscription>(); | ||||||
|  |   const [updateInfo, setUpdateInfo] = useState<CheckResult>(); | ||||||
|  |   const [lastError, setLastError] = useState<Error>(); | ||||||
|  |  | ||||||
|  |   const dismissError = useCallback(() => { | ||||||
|  |     if (lastError) { | ||||||
|  |       setLastError(undefined); | ||||||
|  |     } | ||||||
|  |   }, [lastError]); | ||||||
|  |  | ||||||
|  |   const showAlert = useCallback( | ||||||
|  |     (...args: Parameters<typeof Alert.alert>) => { | ||||||
|  |       if (useAlert) { | ||||||
|  |         Alert.alert(...args); | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     [useAlert], | ||||||
|  |   ); | ||||||
|  |  | ||||||
|  |   const switchVersion = useCallback(() => { | ||||||
|  |     if (updateInfo && 'hash' in updateInfo) { | ||||||
|  |       client.switchVersion(updateInfo.hash); | ||||||
|  |     } | ||||||
|  |   }, [client, updateInfo]); | ||||||
|  |  | ||||||
|  |   const switchVersionLater = useCallback(() => { | ||||||
|  |     if (updateInfo && 'hash' in updateInfo) { | ||||||
|  |       client.switchVersionLater(updateInfo.hash); | ||||||
|  |     } | ||||||
|  |   }, [client, updateInfo]); | ||||||
|  |  | ||||||
|  |   const doUpdate = useCallback( | ||||||
|  |     async (info: UpdateAvailableResult) => { | ||||||
|  |       try { | ||||||
|  |         const hash = await client.downloadUpdate(info); | ||||||
|  |         if (!hash) { | ||||||
|  |           return; | ||||||
|  |         } | ||||||
|  |         setUpdateInfo(info); | ||||||
|  |         stateListener.current && stateListener.current.remove(); | ||||||
|  |         showAlert('Download complete', 'Do you want to apply the update now?', [ | ||||||
|  |           { | ||||||
|  |             text: 'Later', | ||||||
|  |             style: 'cancel', | ||||||
|  |             onPress: () => { | ||||||
|  |               client.switchVersionLater(hash); | ||||||
|  |             }, | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             text: 'Now', | ||||||
|  |             style: 'default', | ||||||
|  |             onPress: () => { | ||||||
|  |               client.switchVersion(hash); | ||||||
|  |             }, | ||||||
|  |           }, | ||||||
|  |         ]); | ||||||
|  |       } catch (err) { | ||||||
|  |         setLastError(err); | ||||||
|  |         showAlert('Failed to update', err.message); | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     [client, showAlert], | ||||||
|  |   ); | ||||||
|  |  | ||||||
|  |   const checkUpdate = useCallback(async () => { | ||||||
|  |     let info: CheckResult; | ||||||
|  |     try { | ||||||
|  |       info = await client.checkUpdate(); | ||||||
|  |     } catch (err) { | ||||||
|  |       setLastError(err); | ||||||
|  |       showAlert('Failed to check update', err.message); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     if ('expired' in info) { | ||||||
|  |       const { downloadUrl } = info; | ||||||
|  |       setUpdateInfo(info); | ||||||
|  |       showAlert( | ||||||
|  |         'Major update', | ||||||
|  |         'A full update is required to download and install to continue.', | ||||||
|  |         [ | ||||||
|  |           { | ||||||
|  |             text: 'OK', | ||||||
|  |             onPress: () => { | ||||||
|  |               if (downloadUrl) { | ||||||
|  |                 if (Platform.OS === 'android' && downloadUrl.endsWith('.apk')) { | ||||||
|  |                   client.downloadAndInstallApk(downloadUrl); | ||||||
|  |                 } else { | ||||||
|  |                   Linking.openURL(downloadUrl); | ||||||
|  |                 } | ||||||
|  |               } | ||||||
|  |             }, | ||||||
|  |           }, | ||||||
|  |         ], | ||||||
|  |       ); | ||||||
|  |     } else if ('update' in info) { | ||||||
|  |       showAlert( | ||||||
|  |         `Version ${info.name} available`, | ||||||
|  |         `What's new\n | ||||||
|  | 	  ${info.description} | ||||||
|  | 	  `, | ||||||
|  |         [ | ||||||
|  |           { text: 'Cancel', style: 'cancel' }, | ||||||
|  |           { | ||||||
|  |             text: 'OK', | ||||||
|  |             style: 'default', | ||||||
|  |             onPress: () => { | ||||||
|  |               doUpdate(info as UpdateAvailableResult); | ||||||
|  |             }, | ||||||
|  |           }, | ||||||
|  |         ], | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |   }, [client, doUpdate, showAlert]); | ||||||
|  |  | ||||||
|  |   const markSuccess = client.markSuccess; | ||||||
|  |  | ||||||
|  |   useEffect(() => { | ||||||
|  |     if (isFirstTime) { | ||||||
|  |       markSuccess(); | ||||||
|  |     } | ||||||
|  |     if (strategy === 'both' || strategy === 'onAppResume') { | ||||||
|  |       stateListener.current = AppState.addEventListener( | ||||||
|  |         'change', | ||||||
|  |         (nextAppState) => { | ||||||
|  |           if (nextAppState === 'active') { | ||||||
|  |             checkUpdate(); | ||||||
|  |           } | ||||||
|  |         }, | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |     if (strategy === 'both' || strategy === 'onAppStart') { | ||||||
|  |       checkUpdate(); | ||||||
|  |     } | ||||||
|  |     let dismissErrorTimer: ReturnType<typeof setTimeout>; | ||||||
|  |     const { dismissErrorAfter } = client.options; | ||||||
|  |     if (typeof dismissErrorAfter === 'number' && dismissErrorAfter > 0) { | ||||||
|  |       dismissErrorTimer = setTimeout(() => { | ||||||
|  |         dismissError(); | ||||||
|  |       }, dismissErrorAfter); | ||||||
|  |     } | ||||||
|  |     return () => { | ||||||
|  |       stateListener.current && stateListener.current.remove(); | ||||||
|  |       clearTimeout(dismissErrorTimer); | ||||||
|  |     }; | ||||||
|  |   }, [checkUpdate, client.options, dismissError, markSuccess, strategy]); | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <PushyContext.Provider | ||||||
|  |       value={{ | ||||||
|  |         checkUpdate, | ||||||
|  |         switchVersion, | ||||||
|  |         switchVersionLater, | ||||||
|  |         dismissError, | ||||||
|  |         updateInfo, | ||||||
|  |         lastError, | ||||||
|  |         markSuccess, | ||||||
|  |       }} | ||||||
|  |     > | ||||||
|  |       {children} | ||||||
|  |     </PushyContext.Provider> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
| @@ -9,14 +9,12 @@ export interface Spec extends TurboModule { | |||||||
|     isFirstTime: boolean; |     isFirstTime: boolean; | ||||||
|     rolledBackVersion: string; |     rolledBackVersion: string; | ||||||
|     buildTime: string; |     buildTime: string; | ||||||
|     blockUpdate: Object; |  | ||||||
|     uuid: string; |     uuid: string; | ||||||
|     isUsingBundleUrl: boolean; |     isUsingBundleUrl: boolean; | ||||||
|   }; |   }; | ||||||
|   setLocalHashInfo(hash: string, info: string): Promise<void>; |   setLocalHashInfo(hash: string, info: string): Promise<void>; | ||||||
|   getLocalHashInfo(hash: string): Promise<string>; |   getLocalHashInfo(hash: string): Promise<string>; | ||||||
|   setUuid(uuid: string): Promise<void>; |   setUuid(uuid: string): Promise<void>; | ||||||
|   setBlockUpdate(options: { reason: string; until: number }): Promise<void>; |  | ||||||
|   reloadUpdate(options: { hash: string }): Promise<void>; |   reloadUpdate(options: { hash: string }): Promise<void>; | ||||||
|   setNeedUpdate(options: { hash: string }): Promise<void>; |   setNeedUpdate(options: { hash: string }): Promise<void>; | ||||||
|   markSuccess(): Promise<void>; |   markSuccess(): Promise<void>; | ||||||
| @@ -1,18 +1,15 @@ | |||||||
| export interface ExpiredResult { | export interface ExpiredResult { | ||||||
|   upToDate?: false; |  | ||||||
|   expired: true; |   expired: true; | ||||||
|   downloadUrl: string; |   downloadUrl: string; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export interface UpTodateResult { | export interface UpTodateResult { | ||||||
|   expired?: false; |  | ||||||
|   upToDate: true; |   upToDate: true; | ||||||
|   paused?: 'app' | 'package'; |   paused?: 'app' | 'package'; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export interface UpdateAvailableResult { | export interface UpdateAvailableResult { | ||||||
|   expired?: false; |   upToDate: false; | ||||||
|   upToDate?: false; |  | ||||||
|   update: true; |   update: true; | ||||||
|   name: string; // version name
 |   name: string; // version name
 | ||||||
|   hash: string; |   hash: string; | ||||||
| @@ -62,10 +59,25 @@ export interface EventData { | |||||||
|   newVersion?: string; |   newVersion?: string; | ||||||
|   [key: string]: any; |   [key: string]: any; | ||||||
| } | } | ||||||
| export type UpdateEventsListener = ({ | export type UpdateEventsLogger = ({ | ||||||
|   type, |   type, | ||||||
|   data, |   data, | ||||||
| }: { | }: { | ||||||
|   type: EventType; |   type: EventType; | ||||||
|   data: EventData; |   data: EventData; | ||||||
| }) => void; | }) => void; | ||||||
|  | 
 | ||||||
|  | export interface PushyServerConfig { | ||||||
|  |   main: string; | ||||||
|  |   backups?: string[]; | ||||||
|  |   queryUrl?: string; | ||||||
|  | } | ||||||
|  | export interface PushyOptions { | ||||||
|  |   appKey: string; | ||||||
|  |   server?: PushyServerConfig; | ||||||
|  |   logger?: UpdateEventsLogger; | ||||||
|  |   useAlert?: boolean; | ||||||
|  |   strategy?: 'onAppStart' | 'onAppResume' | 'both'; | ||||||
|  |   autoMarkSuccess?: boolean; | ||||||
|  |   dismissErrorAfter?: number; | ||||||
|  | } | ||||||
| @@ -1,5 +1,5 @@ | |||||||
| export function logger(...args: any[]) { | export function log(...args: any[]) { | ||||||
|   console.log('Pushy: ', ...args); |   console.log('pushy: ', ...args); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function assertRelease() { | export function assertRelease() { | ||||||
		Reference in New Issue
	
	Block a user
	 sunnylqm
					sunnylqm