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", | ||||
|   "parser": "babel-eslint", | ||||
|   "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 | ||||
|   } | ||||
|   "root": true, | ||||
|   "extends": "@react-native" | ||||
| } | ||||
|   | ||||
| @@ -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": { | ||||
|     "@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/jest": "^29.2.1", | ||||
|     "@types/node": "^20.8.9", | ||||
|     "@types/react": "^18.2.33", | ||||
|     "detox": "^20.5.0", | ||||
|     "firebase-tools": "^11.24.1", | ||||
|     "fs-extra": "^9.1.0", | ||||
|     "jest": "^29.2.1", | ||||
|     "pod-install": "^0.1.37", | ||||
|     "react-native": "^0.72.6", | ||||
|     "ts-jest": "^29.0.3", | ||||
|     "typescript": "^5.2.2" | ||||
|     "ts-jest": "^29.0.3" | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,5 +1,7 @@ | ||||
| require 'json' | ||||
|  | ||||
| new_arch_enabled = ENV['RCT_NEW_ARCH_ENABLED'] == '1' | ||||
|  | ||||
| package = JSON.parse(File.read(File.join(__dir__, 'package.json'))) | ||||
|  | ||||
| Pod::Spec.new do |s| | ||||
| @@ -40,5 +42,25 @@ Pod::Spec.new do |s| | ||||
|                        'android/jni/lzma/C/Lzma2Dec.{h,c}'] | ||||
|     ss.private_header_files = 'ios/RCTPushy/HDiffPatch/**/*.h' | ||||
|   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 | ||||
|   | ||||
							
								
								
									
										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; | ||||
|     rolledBackVersion: string; | ||||
|     buildTime: string; | ||||
|     blockUpdate: Object; | ||||
|     uuid: string; | ||||
|     isUsingBundleUrl: boolean; | ||||
|   }; | ||||
|   setLocalHashInfo(hash: string, info: string): Promise<void>; | ||||
|   getLocalHashInfo(hash: string): Promise<string>; | ||||
|   setUuid(uuid: string): Promise<void>; | ||||
|   setBlockUpdate(options: { reason: string; until: number }): Promise<void>; | ||||
|   reloadUpdate(options: { hash: string }): Promise<void>; | ||||
|   setNeedUpdate(options: { hash: string }): Promise<void>; | ||||
|   markSuccess(): Promise<void>; | ||||
| @@ -1,18 +1,15 @@ | ||||
| export interface ExpiredResult { | ||||
|   upToDate?: false; | ||||
|   expired: true; | ||||
|   downloadUrl: string; | ||||
| } | ||||
| 
 | ||||
| export interface UpTodateResult { | ||||
|   expired?: false; | ||||
|   upToDate: true; | ||||
|   paused?: 'app' | 'package'; | ||||
| } | ||||
| 
 | ||||
| export interface UpdateAvailableResult { | ||||
|   expired?: false; | ||||
|   upToDate?: false; | ||||
|   upToDate: false; | ||||
|   update: true; | ||||
|   name: string; // version name
 | ||||
|   hash: string; | ||||
| @@ -62,10 +59,25 @@ export interface EventData { | ||||
|   newVersion?: string; | ||||
|   [key: string]: any; | ||||
| } | ||||
| export type UpdateEventsListener = ({ | ||||
| export type UpdateEventsLogger = ({ | ||||
|   type, | ||||
|   data, | ||||
| }: { | ||||
|   type: EventType; | ||||
|   data: EventData; | ||||
| }) => 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[]) { | ||||
|   console.log('Pushy: ', ...args); | ||||
| export function log(...args: any[]) { | ||||
|   console.log('pushy: ', ...args); | ||||
| } | ||||
| 
 | ||||
| export function assertRelease() { | ||||
		Reference in New Issue
	
	Block a user
	 sunnylqm
					sunnylqm