Compare commits
32 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
40742e16d8 | ||
![]() |
598ae1a506 | ||
![]() |
e5424591d1 | ||
![]() |
2cf7336b6a | ||
![]() |
7eac48ab5d | ||
![]() |
18d9b75545 | ||
![]() |
e8ec85c65f | ||
![]() |
48a776d506 | ||
![]() |
8ad526148f | ||
![]() |
cea39f1746 | ||
![]() |
aa56c2ec0f | ||
![]() |
00a989d567 | ||
![]() |
83ca3a6c05 | ||
![]() |
257f2697e0 | ||
![]() |
77aa345f11 | ||
![]() |
66332d007a | ||
![]() |
37849b1730 | ||
![]() |
c771672fcd | ||
![]() |
2978454298 | ||
![]() |
905413e424 | ||
![]() |
ee36fd9334 | ||
![]() |
5b46071834 | ||
![]() |
a6802bdd44 | ||
![]() |
97848e7b13 | ||
![]() |
bf3a0808f6 | ||
![]() |
4a7bb19ca1 | ||
![]() |
7a8640d582 | ||
![]() |
ff50e03446 | ||
![]() |
7888010061 | ||
![]() |
a9c360620f | ||
![]() |
05738ec204 | ||
![]() |
e4ef93595b |
@@ -6,6 +6,7 @@
|
||||
Example
|
||||
android/build
|
||||
.vscode
|
||||
.github/
|
||||
|
||||
# OSX
|
||||
#
|
||||
@@ -45,8 +46,12 @@ node_modules/
|
||||
npm-debug.log
|
||||
Example
|
||||
yarn.lock
|
||||
bun.lock
|
||||
|
||||
domains.json
|
||||
endpoints.json
|
||||
endpoints_cresc.json
|
||||
|
||||
tea.yaml
|
||||
tea.yaml
|
||||
|
||||
e2e/
|
@@ -1 +0,0 @@
|
||||
nodeLinker: node-modules
|
36
Example/expoUsePushy/.gitignore
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
|
||||
|
||||
# dependencies
|
||||
node_modules/
|
||||
|
||||
# Expo
|
||||
.expo/
|
||||
dist/
|
||||
web-build/
|
||||
expo-env.d.ts
|
||||
|
||||
# Native
|
||||
*.orig.*
|
||||
*.jks
|
||||
*.p8
|
||||
*.p12
|
||||
*.key
|
||||
*.mobileprovision
|
||||
|
||||
# Metro
|
||||
.metro-health-check*
|
||||
|
||||
# debug
|
||||
npm-debug.*
|
||||
yarn-debug.*
|
||||
yarn-error.*
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
221
Example/expoUsePushy/App.tsx
Normal file
@@ -0,0 +1,221 @@
|
||||
/* eslint-disable react/no-unstable-nested-components */
|
||||
/* eslint-disable react-native/no-inline-styles */
|
||||
import React, {useState} from 'react';
|
||||
import {StyleSheet, Text, View, TouchableOpacity, Image} from 'react-native';
|
||||
|
||||
import TestConsole from './TestConsole';
|
||||
|
||||
import _updateConfig from './update.json';
|
||||
import {PushyProvider, Pushy, usePushy} from 'react-native-update';
|
||||
const {appKey} = _updateConfig.android;
|
||||
|
||||
function Home() {
|
||||
const {
|
||||
client,
|
||||
checkUpdate,
|
||||
downloadUpdate,
|
||||
switchVersionLater,
|
||||
switchVersion,
|
||||
updateInfo,
|
||||
packageVersion,
|
||||
currentHash,
|
||||
progress: {received, total} = {},
|
||||
} = usePushy();
|
||||
const [useDefaultAlert, setUseDefaultAlert] = useState(false);
|
||||
const [showTestConsole, setShowTestConsole] = useState(false);
|
||||
const [showUpdateBanner, setShowUpdateBanner] = useState(false);
|
||||
const [showUpdateSnackbar, setShowUpdateSnackbar] = useState(false);
|
||||
// if (updateInfo) {
|
||||
// updateInfo!.name = 'name';
|
||||
// updateInfo!.update = true;
|
||||
// }
|
||||
const snackbarVisible =
|
||||
!useDefaultAlert && showUpdateSnackbar && updateInfo?.update;
|
||||
|
||||
if (showTestConsole) {
|
||||
return (
|
||||
<TestConsole visible={true} onClose={() => setShowTestConsole(false)} />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.welcome}>欢迎使用Pushy热更新服务</Text>
|
||||
{/* <Text style={styles.welcome}>😁hdiffFromAPP更新成功!!!</Text> */}
|
||||
{/* <Text style={styles.welcome}>😁hdiffFromPPk更新成功!!!</Text> */}
|
||||
<View style={{flexDirection: 'row'}}>
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
client?.setOptions({
|
||||
updateStrategy: !useDefaultAlert ? null : 'alwaysAlert',
|
||||
});
|
||||
setShowUpdateSnackbar(useDefaultAlert);
|
||||
setUseDefaultAlert(!useDefaultAlert);
|
||||
}}
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
}}>
|
||||
<View
|
||||
style={{
|
||||
width: 20,
|
||||
height: 20,
|
||||
borderWidth: 1,
|
||||
borderColor: '#999',
|
||||
backgroundColor: useDefaultAlert ? 'blue' : 'white',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}}>
|
||||
{useDefaultAlert && <Text style={{color: 'white'}}>✓</Text>}
|
||||
</View>
|
||||
<Text style={{marginLeft: 8}}>
|
||||
{' '}
|
||||
{useDefaultAlert ? '当前使用' : '当前不使用'}默认的alert更新提示
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<Image
|
||||
resizeMode={'contain'}
|
||||
source={require('./assets/shezhi.png')}
|
||||
style={styles.image}
|
||||
/>
|
||||
<Text style={styles.instructions}>
|
||||
这是版本一 {'\n'}
|
||||
当前原生包版本号: {packageVersion}
|
||||
{'\n'}
|
||||
当前热更新版本Hash: {currentHash || '(空)'}
|
||||
{'\n'}
|
||||
</Text>
|
||||
<Text>
|
||||
下载进度:{received} / {total}
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
checkUpdate();
|
||||
setShowUpdateSnackbar(true);
|
||||
}}>
|
||||
<Text style={styles.instructions}>点击这里检查更新</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
testID="testcase"
|
||||
style={{marginTop: 15}}
|
||||
onPress={() => {
|
||||
setShowTestConsole(true);
|
||||
}}>
|
||||
<Text style={styles.instructions}>
|
||||
react-native-update版本:{client?.version}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
{snackbarVisible && (
|
||||
<View style={styles.overlay}>
|
||||
<View
|
||||
style={{
|
||||
width: '100%',
|
||||
backgroundColor: '#333',
|
||||
padding: 16,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
}}>
|
||||
<Text style={{color: 'white'}}>
|
||||
有新版本({updateInfo.name})可用,是否更新?
|
||||
</Text>
|
||||
<View style={{flexDirection: 'row'}}>
|
||||
<TouchableOpacity
|
||||
onPress={() => setShowUpdateSnackbar(false)}
|
||||
style={{marginRight: 10}}>
|
||||
<Text style={{color: 'white'}}>取消</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
onPress={async () => {
|
||||
setShowUpdateSnackbar(false);
|
||||
await downloadUpdate();
|
||||
setShowUpdateBanner(true);
|
||||
}}>
|
||||
<Text style={{color: '#2196F3'}}>更新</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
{showUpdateBanner && (
|
||||
<View style={styles.overlay}>
|
||||
<View
|
||||
style={{
|
||||
width: '100%',
|
||||
backgroundColor: '#fff',
|
||||
padding: 16,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#eee',
|
||||
}}>
|
||||
<View style={{flexDirection: 'row', alignItems: 'center'}}>
|
||||
<Text>更新已完成,是否立即重启?</Text>
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-end',
|
||||
marginTop: 10,
|
||||
}}>
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
switchVersionLater();
|
||||
setShowUpdateBanner(false);
|
||||
}}
|
||||
style={{marginRight: 20}}>
|
||||
<Text style={{color: '#2196F3'}}>下次再说</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity onPress={switchVersion}>
|
||||
<Text style={{color: '#2196F3'}}>立即重启</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
overlay: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#F5FCFF',
|
||||
},
|
||||
welcome: {
|
||||
fontSize: 20,
|
||||
textAlign: 'center',
|
||||
margin: 10,
|
||||
},
|
||||
instructions: {
|
||||
textAlign: 'center',
|
||||
color: '#333333',
|
||||
marginBottom: 5,
|
||||
},
|
||||
image: {},
|
||||
});
|
||||
|
||||
const pushyClient = new Pushy({
|
||||
appKey,
|
||||
debug: true,
|
||||
});
|
||||
|
||||
export default function HomeScreen() {
|
||||
return (
|
||||
<PushyProvider client={pushyClient}>
|
||||
<Home />
|
||||
</PushyProvider>
|
||||
);
|
||||
}
|
274
Example/expoUsePushy/TestConsole.js
Normal file
@@ -0,0 +1,274 @@
|
||||
/* eslint-disable react-native/no-inline-styles */
|
||||
/* eslint-disable react/react-in-jsx-scope */
|
||||
import {useCallback, useMemo, useState} from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
TextInput,
|
||||
Button,
|
||||
StyleSheet,
|
||||
SafeAreaView,
|
||||
Text,
|
||||
View,
|
||||
TouchableOpacity,
|
||||
} from 'react-native';
|
||||
|
||||
import {PushyModule} from 'react-native-update';
|
||||
const Hash = '9D5CE6EBA420717BE7E7D308B11F8207681B066C951D68F3994D19828F342474';
|
||||
const UUID = '00000000-0000-0000-0000-000000000000';
|
||||
const DownloadUrl = 'https://localhost:3000/diff.ppk-patch';
|
||||
const AppPatchDownloadUrl = 'https://github.com/bozaigao/test_pushy_server/raw/refs/heads/main/hdiff.app-patch';
|
||||
const AppPatchHash = 'f5ba92c7c04250d4b8a446c8267ef459';
|
||||
const PPKDownloadUrl = 'https://github.com/bozaigao/test_pushy_server/raw/refs/heads/main/hdiff.ppk-patch';
|
||||
const PPKPatchHash = '6b3d26b7d868d1f67aedadb7f0b342d9';
|
||||
const OriginHash = 'f5ba92c7c04250d4b8a446c8267ef459';
|
||||
|
||||
|
||||
const CustomDialog = ({title, visible, onConfirm}) => {
|
||||
if (!visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.overlay}>
|
||||
<View style={styles.dialog}>
|
||||
<Text style={styles.title}>{title}</Text>
|
||||
<TouchableOpacity
|
||||
testID="done"
|
||||
style={styles.button}
|
||||
onPress={onConfirm}>
|
||||
<Text style={styles.buttonText}>确认</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
export default function TestConsole({visible, onClose}) {
|
||||
const [text, setText] = useState('');
|
||||
const [running, setRunning] = useState(false);
|
||||
const [options, setOptions] = useState();
|
||||
const [alertVisible, setAlertVisible] = useState(false);
|
||||
const [alertMsg, setAlertMsg] = useState('');
|
||||
const NativeTestMethod = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
name: 'setLocalHashInfo',
|
||||
invoke: () => {
|
||||
setText(
|
||||
`setLocalHashInfo\n${Hash}\n{\"version\":\"1.0.0\",\"size\":\"19M\"}`,
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'getLocalHashInfo',
|
||||
invoke: () => {
|
||||
setText(`getLocalHashInfo\n${Hash}`);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'setUuid',
|
||||
invoke: () => {
|
||||
setText(`setUuid\n${UUID}`);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'reloadUpdate',
|
||||
invoke: () => {
|
||||
setText('reloadUpdate');
|
||||
setOptions({hash: Hash});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'setNeedUpdateForApp',
|
||||
invoke: () => {
|
||||
setText('setNeedUpdate');
|
||||
setOptions({hash: AppPatchHash});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'setNeedUpdateForPPK',
|
||||
invoke: () => {
|
||||
setText('setNeedUpdate');
|
||||
setOptions({hash: PPKPatchHash});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'markSuccess',
|
||||
invoke: () => {
|
||||
setText('markSuccess');
|
||||
setOptions(undefined);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'downloadPatchFromPpk',
|
||||
invoke: () => {
|
||||
setText('downloadPatchFromPpk');
|
||||
setOptions({updateUrl: PPKDownloadUrl, hash: PPKPatchHash, originHash: OriginHash});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'downloadPatchFromPackage',
|
||||
invoke: () => {
|
||||
setText('downloadPatchFromPackage');
|
||||
setOptions({updateUrl: AppPatchDownloadUrl, hash: AppPatchHash});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'downloadFullUpdate',
|
||||
invoke: () => {
|
||||
setText('downloadFullUpdate');
|
||||
setOptions({updateUrl: DownloadUrl, hash: Hash});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'downloadAndInstallApk',
|
||||
invoke: () => {
|
||||
setText('downloadAndInstallApk');
|
||||
setOptions({url: DownloadUrl, target: Hash, hash: Hash});
|
||||
},
|
||||
},
|
||||
];
|
||||
}, []);
|
||||
|
||||
const renderTestView = useCallback(() => {
|
||||
const views = [];
|
||||
for (let i = 0; i < NativeTestMethod.length; i++) {
|
||||
views.push(
|
||||
<TouchableOpacity
|
||||
key={i}
|
||||
testID={NativeTestMethod[i].name}
|
||||
onPress={() => {
|
||||
NativeTestMethod[i].invoke();
|
||||
}}>
|
||||
<Text>{NativeTestMethod[i].name}</Text>
|
||||
</TouchableOpacity>,
|
||||
);
|
||||
}
|
||||
return <View>{views}</View>;
|
||||
}, [NativeTestMethod]);
|
||||
if (!visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<SafeAreaView style={{flex: 1, padding: 10}}>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: 10,
|
||||
}}>
|
||||
<Text>调试Pushy方法(方法名,参数,值换行)</Text>
|
||||
<Button title="关闭" onPress={() => onClose()} />
|
||||
</View>
|
||||
<TextInput
|
||||
autoCorrect={false}
|
||||
autoCapitalize="none"
|
||||
style={{
|
||||
borderWidth: StyleSheet.hairlineWidth * 4,
|
||||
borderColor: 'black',
|
||||
height: '30%',
|
||||
marginTop: 20,
|
||||
marginBottom: 20,
|
||||
padding: 10,
|
||||
fontSize: 20,
|
||||
}}
|
||||
textAlignVertical="top"
|
||||
multiline={true}
|
||||
value={text}
|
||||
onChangeText={setText}
|
||||
/>
|
||||
{running && <ActivityIndicator />}
|
||||
<TouchableOpacity
|
||||
style={{
|
||||
backgroundColor: 'rgb(0,140,237)',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
paddingTop: 10,
|
||||
paddingBottom: 10,
|
||||
marginBottom: 5,
|
||||
}}
|
||||
testID="submit"
|
||||
onPress={async () => {
|
||||
setRunning(true);
|
||||
try {
|
||||
const inputs = text.split('\n');
|
||||
const methodName = inputs[0];
|
||||
let params = [];
|
||||
if (inputs.length === 1) {
|
||||
if (options) {
|
||||
await PushyModule[methodName](options);
|
||||
} else {
|
||||
await PushyModule[methodName]();
|
||||
}
|
||||
} else {
|
||||
if (inputs.length === 2) {
|
||||
params = [inputs[1]];
|
||||
} else {
|
||||
params = [inputs[1], inputs[2]];
|
||||
console.log({inputs, params});
|
||||
}
|
||||
await PushyModule[methodName](...params);
|
||||
}
|
||||
setAlertVisible(true);
|
||||
setAlertMsg('done');
|
||||
} catch (e) {
|
||||
setAlertVisible(true);
|
||||
setAlertMsg(e.message);
|
||||
}
|
||||
setRunning(false);
|
||||
}}>
|
||||
<Text style={{color: 'white'}}>执行</Text>
|
||||
</TouchableOpacity>
|
||||
<Button title="重置" onPress={() => setText('')} />
|
||||
{renderTestView()}
|
||||
<CustomDialog
|
||||
title={alertMsg}
|
||||
visible={alertVisible}
|
||||
onConfirm={() => {
|
||||
setAlertVisible(false);
|
||||
}}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
overlay: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
dialog: {
|
||||
backgroundColor: 'white',
|
||||
borderRadius: 10,
|
||||
padding: 20,
|
||||
width: '80%',
|
||||
alignItems: 'center',
|
||||
},
|
||||
title: {
|
||||
fontSize: 18,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 20,
|
||||
},
|
||||
button: {
|
||||
backgroundColor: '#2196F3',
|
||||
borderRadius: 5,
|
||||
paddingVertical: 10,
|
||||
paddingHorizontal: 20,
|
||||
},
|
||||
buttonText: {
|
||||
color: 'white',
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
});
|
29
Example/expoUsePushy/app.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"expo": {
|
||||
"name": "expoUsePushy",
|
||||
"slug": "expoUsePushy",
|
||||
"version": "1.0.0",
|
||||
"orientation": "portrait",
|
||||
"icon": "./assets/icon.png",
|
||||
"userInterfaceStyle": "light",
|
||||
"newArchEnabled": true,
|
||||
"splash": {
|
||||
"image": "./assets/splash-icon.png",
|
||||
"resizeMode": "contain",
|
||||
"backgroundColor": "#ffffff"
|
||||
},
|
||||
"ios": {
|
||||
"supportsTablet": true,
|
||||
"bundleIdentifier": "com.anonymous.expoUsePushy"
|
||||
},
|
||||
"android": {
|
||||
"adaptiveIcon": {
|
||||
"foregroundImage": "./assets/adaptive-icon.png",
|
||||
"backgroundColor": "#ffffff"
|
||||
}
|
||||
},
|
||||
"web": {
|
||||
"favicon": "./assets/favicon.png"
|
||||
}
|
||||
}
|
||||
}
|
BIN
Example/expoUsePushy/assets/adaptive-icon.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
Example/expoUsePushy/assets/favicon.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
Example/expoUsePushy/assets/icon.png
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
Example/expoUsePushy/assets/shezhi.png
Normal file
After Width: | Height: | Size: 696 B |
BIN
Example/expoUsePushy/assets/shezhi@2x.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
Example/expoUsePushy/assets/shezhi@3x.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
Example/expoUsePushy/assets/splash-icon.png
Normal file
After Width: | Height: | Size: 17 KiB |
1765
Example/expoUsePushy/bun.lock
Normal file
8
Example/expoUsePushy/index.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import { registerRootComponent } from 'expo';
|
||||
|
||||
import App from './App';
|
||||
|
||||
// registerRootComponent calls AppRegistry.registerComponent('main', () => App);
|
||||
// It also ensures that whether you load the app in Expo Go or in a native build,
|
||||
// the environment is set up appropriately
|
||||
registerRootComponent(App);
|
24
Example/expoUsePushy/package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "expousepushy",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "expo start",
|
||||
"android": "expo run:android",
|
||||
"ios": "expo run:ios",
|
||||
"web": "expo start --web"
|
||||
},
|
||||
"dependencies": {
|
||||
"expo": "~52.0.46",
|
||||
"expo-status-bar": "~2.0.1",
|
||||
"react": "18.3.1",
|
||||
"react-native": "0.76.9",
|
||||
"react-native-update": "^10.28.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.25.2",
|
||||
"@types/react": "~18.3.12",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"private": true
|
||||
}
|
6
Example/expoUsePushy/tsconfig.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "expo/tsconfig.base",
|
||||
"compilerOptions": {
|
||||
"strict": true
|
||||
}
|
||||
}
|
14
Example/expoUsePushy/update.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"ios": {
|
||||
"appId": 29439,
|
||||
"appKey": "jNA71vpFHTDpEqeZd9yx87zj"
|
||||
},
|
||||
"android": {
|
||||
"appId": 29413,
|
||||
"appKey": "vdZWPXU6eyaPE6Avk96-YvwK"
|
||||
},
|
||||
"harmony": {
|
||||
"appId": 29140,
|
||||
"appKey": "JLklGflGIRbY-cMebjQwm1J1"
|
||||
}
|
||||
}
|
@@ -1,18 +1,19 @@
|
||||
## 运行harmony_use_pushy项目步骤
|
||||
|
||||
### 1. 先在react-native-update根目录执行下面命令同步C++模块。
|
||||
### 1. 在项目根目录执行下面命令安装第三方依赖。
|
||||
```
|
||||
yarn submodule
|
||||
bun install
|
||||
```
|
||||
|
||||
### 2. 在项目根目录执行下面命令安装第三方依赖。
|
||||
### 2. 本地debug 模式
|
||||
```
|
||||
yarn install
|
||||
bun run start
|
||||
```
|
||||

|
||||
|
||||
### 3. 在项目根目录执行下面命令生成bundle包文件。
|
||||
### 3. release 模式: 在项目根目录执行下面命令生成bundle包文件。
|
||||
```
|
||||
yarn build
|
||||
bun run build
|
||||
```
|
||||
说明:这个命令会在harmony/entry/src/main/resources/rawfile目录生成Hbundle.harmony.js和assets文件,同时会基于该内容在.pushy/output目录生成ppk包。
|
||||
|
||||
|
2130
Example/harmony_use_pushy/bun.lock
Normal file
BIN
Example/harmony_use_pushy/debug.png
Normal file
After Width: | Height: | Size: 743 KiB |
@@ -36,10 +36,6 @@
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'pushy',
|
||||
srcPath: '../node_modules/react-native-update/harmony',
|
||||
}
|
||||
]
|
||||
}
|
@@ -7,7 +7,7 @@
|
||||
"license": "",
|
||||
"dependencies": {
|
||||
"@rnoh/react-native-openharmony": "0.72.38",
|
||||
"pushy": "file:../../node_modules/react-native-update/harmony"
|
||||
"pushy": "file:../../node_modules/react-native-update/harmony/pushy.har",
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* This code was generated by "react-native codegen-harmony"
|
||||
*
|
||||
* Do not edit this file as changes may cause incorrect behavior and will be
|
||||
* lost once the code is regenerated.
|
||||
*
|
||||
* @generatorVersion: 1
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "RNOH/Package.h"
|
||||
#include "RNOH/ArkTSTurboModule.h"
|
||||
|
||||
namespace rnoh {
|
||||
|
||||
class RNOHGeneratedPackageTurboModuleFactoryDelegate : public TurboModuleFactoryDelegate {
|
||||
public:
|
||||
SharedTurboModule createTurboModule(Context ctx, const std::string &name) const override {
|
||||
return nullptr;
|
||||
};
|
||||
};
|
||||
|
||||
class GeneratedEventEmitRequestHandler : public EventEmitRequestHandler {
|
||||
public:
|
||||
void handleEvent(Context const &ctx) override {
|
||||
auto eventEmitter = ctx.shadowViewRegistry->getEventEmitter<facebook::react::EventEmitter>(ctx.tag);
|
||||
if (eventEmitter == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<std::string> supportedEventNames = {
|
||||
};
|
||||
if (std::find(supportedEventNames.begin(), supportedEventNames.end(), ctx.eventName) != supportedEventNames.end()) {
|
||||
eventEmitter->dispatchEvent(ctx.eventName, ArkJS(ctx.env).getDynamic(ctx.payload));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
class RNOHGeneratedPackage : public Package {
|
||||
public:
|
||||
RNOHGeneratedPackage(Package::Context ctx) : Package(ctx){};
|
||||
|
||||
std::unique_ptr<TurboModuleFactoryDelegate> createTurboModuleFactoryDelegate() override {
|
||||
return std::make_unique<RNOHGeneratedPackageTurboModuleFactoryDelegate>();
|
||||
}
|
||||
|
||||
std::vector<facebook::react::ComponentDescriptorProvider> createComponentDescriptorProviders() override {
|
||||
return {
|
||||
};
|
||||
}
|
||||
|
||||
ComponentJSIBinderByString createComponentJSIBinderByName() override {
|
||||
return {
|
||||
};
|
||||
};
|
||||
|
||||
EventEmitRequestHandlers createEventEmitRequestHandlers() override {
|
||||
return {
|
||||
std::make_shared<GeneratedEventEmitRequestHandler>(),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace rnoh
|
@@ -1,5 +1,6 @@
|
||||
import { FileJSBundleProvider } from 'pushy/src/main/ets/FileJSBundleProvider';
|
||||
import { ComponentBuilderContext, RNOHCoreContext,RNAbility } from '@rnoh/react-native-openharmony';
|
||||
import { PushyFileJSBundleProvider } from 'pushy/src/main/ets/PushyFileJSBundleProvider';
|
||||
import { ComponentBuilderContext, RNOHCoreContext,RNAbility,
|
||||
MetroJSBundleProvider } from '@rnoh/react-native-openharmony';
|
||||
import {
|
||||
RNApp,
|
||||
AnyJSBundleProvider,
|
||||
@@ -61,9 +62,10 @@ struct Index {
|
||||
},
|
||||
jsBundleProvider: new TraceJSBundleProviderDecorator(
|
||||
new AnyJSBundleProvider([
|
||||
// MetroJSBundleProvider.fromServerIp('127.0.0.1'),
|
||||
// new ResourceJSBundleProvider(rnohCoreContext.uiAbilityContext.resourceManager, 'hermes_bundle.hbc'),
|
||||
new FileJSBundleProvider(this.rnohCoreContext.uiAbilityContext),
|
||||
// local debug mode
|
||||
new MetroJSBundleProvider(),
|
||||
// release mode
|
||||
new PushyFileJSBundleProvider(this.rnohCoreContext.uiAbilityContext),
|
||||
new ResourceJSBundleProvider(this.rnohCoreContext.uiAbilityContext.resourceManager, 'bundle.harmony.js')
|
||||
]),
|
||||
this.rnohCoreContext.logger),
|
||||
|
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"pushy_build_time": "2025-03-09T01:57:42.464Z",
|
||||
"pushy_build_time": "2025-04-30T02:46:33.340Z",
|
||||
"versionName": "1.0.0"
|
||||
}
|
@@ -0,0 +1,5 @@
|
||||
|
||||
/**
|
||||
*/
|
||||
|
||||
export {}
|
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* This code was generated by "react-native codegen-harmony"
|
||||
*
|
||||
* Do not edit this file as changes may cause incorrect behavior and will be
|
||||
* lost once the code is regenerated.
|
||||
*/
|
||||
|
||||
export * from "./ts"
|
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* This code was generated by "react-native codegen-harmony"
|
||||
*
|
||||
* Do not edit this file as changes may cause incorrect behavior and will be
|
||||
* lost once the code is regenerated.
|
||||
*/
|
||||
|
||||
export * as RNC from "./components/ts"
|
||||
export * as TM from "./turboModules/ts"
|
@@ -0,0 +1,5 @@
|
||||
|
||||
/**
|
||||
*/
|
||||
|
||||
export {}
|
12423
Example/harmony_use_pushy/package-lock.json
generated
@@ -6,7 +6,8 @@
|
||||
"android": "react-native run-android",
|
||||
"ios": "react-native run-ios",
|
||||
"lint": "eslint .",
|
||||
"start": "react-native start",
|
||||
"start": "npm run codegen && hdc rport tcp:8081 tcp:8081 && react-native start",
|
||||
"codegen": "react-native codegen-harmony --rnoh-module-path ./harmony/react_native_openharmony",
|
||||
"build": "pushy bundle --platform harmony",
|
||||
"test": "jest",
|
||||
"hdiffFromPPK": "pushy hdiffFromPPK .pushy/output/harmony.1735052610653.ppk .pushy/output/harmony.1735052678646.ppk .pushy/output/hdiff.ppk-patch",
|
||||
@@ -14,10 +15,10 @@
|
||||
"hash": "pushy hash /Users/yanbo.he/Desktop/HarmonyOS/react-native-pushy/Example/harmony_use_pushy/.pushy/output/harmony.1735048297258.ppk"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-native-oh/react-native-harmony": "^0.72.59",
|
||||
"react": "18.2.0",
|
||||
"react-native": "0.72.5",
|
||||
"react-native-update": "file:../../",
|
||||
"@react-native-oh/react-native-harmony": "^0.72.43"
|
||||
"react-native-update": "latest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.20.0",
|
||||
|
4
Example/testHotUpdate/.gitignore
vendored
@@ -62,3 +62,7 @@ buck-out/
|
||||
# Ruby / CocoaPods
|
||||
/ios/Pods/
|
||||
/vendor/bundle/
|
||||
|
||||
# react-native-update
|
||||
.update
|
||||
.pushy
|
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
@@ -17,33 +17,33 @@
|
||||
"form-data": "^4.0.2",
|
||||
"patch-package": "^8.0.0",
|
||||
"react": "19.0.0",
|
||||
"react-native": "0.78.0",
|
||||
"react-native-camera-kit": "^14.2.0",
|
||||
"react-native-paper": "^5.13.1",
|
||||
"react-native-safe-area-context": "^5.3.0",
|
||||
"react-native-svg": "^15.11.2",
|
||||
"react-native-update": "^10.26.4",
|
||||
"react-native": "0.79.2",
|
||||
"react-native-camera-kit": "^15.0.1",
|
||||
"react-native-paper": "^5.14.5",
|
||||
"react-native-safe-area-context": "^5.4.1",
|
||||
"react-native-svg": "^15.12.0",
|
||||
"react-native-update": "^10.28.11",
|
||||
"react-native-vector-icons": "^10.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.26.0",
|
||||
"@babel/preset-env": "^7.26.0",
|
||||
"@babel/runtime": "^7.26.0",
|
||||
"@react-native-community/cli": "15.0.1",
|
||||
"@react-native-community/cli-platform-android": "15.0.1",
|
||||
"@react-native-community/cli-platform-ios": "15.0.1",
|
||||
"@react-native/babel-preset": "0.78.0",
|
||||
"@react-native/eslint-config": "0.78.0",
|
||||
"@react-native/metro-config": "0.78.0",
|
||||
"@react-native/typescript-config": "0.78.0",
|
||||
"@babel/core": "^7.27.3",
|
||||
"@babel/preset-env": "^7.27.2",
|
||||
"@babel/runtime": "^7.27.3",
|
||||
"@react-native-community/cli": "18.0.0",
|
||||
"@react-native-community/cli-platform-android": "18.0.0",
|
||||
"@react-native-community/cli-platform-ios": "18.0.0",
|
||||
"@react-native/babel-preset": "0.79.2",
|
||||
"@react-native/eslint-config": "0.79.2",
|
||||
"@react-native/metro-config": "0.79.2",
|
||||
"@react-native/typescript-config": "0.79.2",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-test-renderer": "^19.0.0",
|
||||
"detox": "^20.32.0",
|
||||
"detox": "^20.39.0",
|
||||
"eslint": "^8.19.0",
|
||||
"jest": "^29.6.3",
|
||||
"prettier": "2.8.8",
|
||||
"react-test-renderer": "19.0.0",
|
||||
"typescript": "5.8.2"
|
||||
"typescript": "5.8.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
|
@@ -204,17 +204,18 @@ const styles = StyleSheet.create({
|
||||
});
|
||||
|
||||
// use Pushy for China users
|
||||
// const updateClient = new Pushy({
|
||||
const updateClient = new Pushy({
|
||||
appKey,
|
||||
debug: true,
|
||||
// updateStrategy: 'silentAndLater',
|
||||
});
|
||||
|
||||
// use Cresc for global users
|
||||
// const updateClient = new Cresc({
|
||||
// appKey,
|
||||
// debug: true,
|
||||
// });
|
||||
|
||||
// use Cresc for global users
|
||||
const updateClient = new Cresc({
|
||||
appKey,
|
||||
debug: true,
|
||||
});
|
||||
|
||||
export default function Root() {
|
||||
return (
|
||||
<UpdateProvider client={updateClient}>
|
||||
|
@@ -22,21 +22,49 @@ def supportsNamespace() {
|
||||
return major >= 8
|
||||
}
|
||||
|
||||
def isExpoProject() {
|
||||
def checkProjectInfo() {
|
||||
def hasExpoModulesCore = rootProject.subprojects.any { it.name == 'expo-modules-core' }
|
||||
|
||||
def packageJsonFile = new File(rootProject.projectDir.parentFile, 'package.json')
|
||||
|
||||
def hasExpoDependency = false
|
||||
def projectVersion = '1.0.0' // Default version
|
||||
|
||||
if (packageJsonFile.exists()) {
|
||||
def packageJson = new groovy.json.JsonSlurper().parseText(packageJsonFile.text)
|
||||
hasExpoDependency = (packageJson.dependencies?.expo != null) ||
|
||||
(packageJson.devDependencies?.expo != null)
|
||||
projectVersion = packageJson.version ?: '1.0.0' // Get project version
|
||||
|
||||
// Check for expo dependency and version >= 50
|
||||
String expoVersionString = packageJson.dependencies?.expo ?: packageJson.devDependencies?.expo
|
||||
boolean expoVersionIsHighEnough = false
|
||||
if (expoVersionString) {
|
||||
try {
|
||||
// Extract the first number sequence as the major version
|
||||
def matcher = (expoVersionString =~ /(\d+)/)
|
||||
if (matcher.find()) {
|
||||
int majorVersion = matcher[0][0].toInteger()
|
||||
if (majorVersion >= 50) {
|
||||
expoVersionIsHighEnough = true
|
||||
}
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
// Handle error if version parsing fails, maybe log a warning
|
||||
println "Warning: Could not parse Expo version string: ${expoVersionString}"
|
||||
}
|
||||
}
|
||||
hasExpoDependency = expoVersionIsHighEnough // Update based on version check
|
||||
}
|
||||
|
||||
return hasExpoModulesCore || hasExpoDependency
|
||||
def isExpo = hasExpoModulesCore && hasExpoDependency
|
||||
|
||||
// Return a map containing both pieces of information
|
||||
return [isExpo: isExpo, version: projectVersion]
|
||||
}
|
||||
|
||||
def expoProject = isExpoProject()
|
||||
// Get project info map
|
||||
def projectInfo = checkProjectInfo()
|
||||
// Extract info into variables
|
||||
def projectVersion = projectInfo.version
|
||||
def expoProject = projectInfo.isExpo
|
||||
|
||||
apply plugin: 'com.android.library'
|
||||
if (isNewArchitectureEnabled()) {
|
||||
@@ -45,16 +73,16 @@ if (isNewArchitectureEnabled()) {
|
||||
|
||||
if (expoProject) {
|
||||
group = 'expo.modules.pushy'
|
||||
version = '1.0.0'
|
||||
version = projectVersion
|
||||
|
||||
def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
|
||||
apply from: expoModulesCorePlugin
|
||||
applyKotlinExpoModulesCorePlugin()
|
||||
// useExpoPublishing()
|
||||
useCoreDependencies()
|
||||
useExpoPublishing()
|
||||
} else {
|
||||
group = 'cn.reactnative.modules.update'
|
||||
version = '1.0.0'
|
||||
version = projectVersion
|
||||
}
|
||||
|
||||
android {
|
||||
@@ -120,7 +148,6 @@ repositories {
|
||||
dependencies {
|
||||
implementation 'com.facebook.react:react-native:+'
|
||||
implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.0'
|
||||
implementation 'com.jakewharton:process-phoenix:3.0.0'
|
||||
}
|
||||
if (isNewArchitectureEnabled()) {
|
||||
react {
|
||||
|
@@ -120,14 +120,13 @@ class DownloadTask extends AsyncTask<DownloadTaskParams, long[], Void> {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onProgressUpdate(long[]... values) {
|
||||
protected void onProgressUpdate(final long[]... values) {
|
||||
super.onProgressUpdate(values);
|
||||
WritableMap params = Arguments.createMap();
|
||||
params.putDouble("received", (values[0][0]));
|
||||
params.putDouble("total", (values[0][1]));
|
||||
params.putString("hash", this.hash);
|
||||
sendEvent("RCTPushyDownloadProgress", params);
|
||||
|
||||
}
|
||||
|
||||
byte[] buffer = new byte[1024*4];
|
||||
@@ -452,7 +451,7 @@ class DownloadTask extends AsyncTask<DownloadTaskParams, long[], Void> {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Void doInBackground(DownloadTaskParams... params) {
|
||||
protected Void doInBackground(final DownloadTaskParams... params) {
|
||||
int taskType = params[0].type;
|
||||
try {
|
||||
switch (taskType) {
|
||||
|
@@ -24,7 +24,7 @@ public class UpdateModuleImpl {
|
||||
|
||||
public static final String NAME = "Pushy";
|
||||
|
||||
public static void downloadFullUpdate(UpdateContext updateContext, ReadableMap options, Promise promise) {
|
||||
public static void downloadFullUpdate(UpdateContext updateContext, final ReadableMap options, final Promise promise) {
|
||||
String url = options.getString("updateUrl");
|
||||
String hash = options.getString("hash");
|
||||
updateContext.downloadFullUpdate(url, hash, new UpdateContext.DownloadFileListener() {
|
||||
@@ -40,7 +40,7 @@ public class UpdateModuleImpl {
|
||||
});
|
||||
}
|
||||
|
||||
public static void downloadAndInstallApk(UpdateContext updateContext, ReadableMap options, Promise promise) {
|
||||
public static void downloadAndInstallApk(UpdateContext updateContext, final ReadableMap options, final Promise promise) {
|
||||
String url = options.getString("url");
|
||||
String hash = options.getString("hash");
|
||||
String target = options.getString("target");
|
||||
@@ -63,7 +63,7 @@ public class UpdateModuleImpl {
|
||||
UpdateModule.installApk(toInstall);
|
||||
}
|
||||
|
||||
public static void downloadPatchFromPackage(UpdateContext updateContext, ReadableMap options, Promise promise) {
|
||||
public static void downloadPatchFromPackage(UpdateContext updateContext, final ReadableMap options, final Promise promise) {
|
||||
String url = options.getString("updateUrl");
|
||||
String hash = options.getString("hash");
|
||||
updateContext.downloadPatchFromApk(url, hash, new UpdateContext.DownloadFileListener() {
|
||||
@@ -79,7 +79,7 @@ public class UpdateModuleImpl {
|
||||
});
|
||||
}
|
||||
|
||||
public static void downloadPatchFromPpk(UpdateContext updateContext, ReadableMap options, Promise promise) {
|
||||
public static void downloadPatchFromPpk(UpdateContext updateContext, final ReadableMap options, final Promise promise) {
|
||||
try {
|
||||
String url = options.getString("updateUrl");
|
||||
String hash = options.getString("hash");
|
||||
@@ -102,7 +102,7 @@ public class UpdateModuleImpl {
|
||||
}
|
||||
}
|
||||
|
||||
public static void reloadUpdate(UpdateContext updateContext, ReactApplicationContext mContext, ReadableMap options, Promise promise) {
|
||||
public static void reloadUpdate(final UpdateContext updateContext,final ReactApplicationContext mContext, final ReadableMap options, final Promise promise) {
|
||||
final String hash = options.getString("hash");
|
||||
UiThreadUtil.runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
@@ -152,8 +152,13 @@ public class UpdateModuleImpl {
|
||||
reactHostDelegateField.setAccessible(true);
|
||||
Object reactHostDelegate = reactHostDelegateField.get(reactHost);
|
||||
|
||||
String bundleFieldName = "jsBundleLoader";
|
||||
if (reactHostDelegate.getClass().getCanonicalName().equals("expo.modules.ExpoReactHostFactory.ExpoReactHostDelegate")) {
|
||||
bundleFieldName = "_jsBundleLoader";
|
||||
}
|
||||
|
||||
// Modify the jsBundleLoader field
|
||||
Field jsBundleLoaderField = reactHostDelegate.getClass().getDeclaredField("jsBundleLoader");
|
||||
Field jsBundleLoaderField = reactHostDelegate.getClass().getDeclaredField(bundleFieldName);
|
||||
jsBundleLoaderField.setAccessible(true);
|
||||
jsBundleLoaderField.set(reactHostDelegate, loader);
|
||||
|
||||
@@ -176,7 +181,7 @@ public class UpdateModuleImpl {
|
||||
});
|
||||
}
|
||||
|
||||
public static void restartApp(final ReactApplicationContext mContext, Promise promise) {
|
||||
public static void restartApp(final ReactApplicationContext mContext, final Promise promise) {
|
||||
UiThreadUtil.runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
@@ -206,7 +211,7 @@ public class UpdateModuleImpl {
|
||||
});
|
||||
}
|
||||
|
||||
public static void setNeedUpdate(UpdateContext updateContext, ReadableMap options, Promise promise) {
|
||||
public static void setNeedUpdate(final UpdateContext updateContext, final ReadableMap options, final Promise promise) {
|
||||
final String hash = options.getString("hash");
|
||||
UiThreadUtil.runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
@@ -222,7 +227,7 @@ public class UpdateModuleImpl {
|
||||
});
|
||||
}
|
||||
|
||||
public static void markSuccess(UpdateContext updateContext, Promise promise) {
|
||||
public static void markSuccess(final UpdateContext updateContext, final Promise promise) {
|
||||
UiThreadUtil.runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
@@ -232,7 +237,7 @@ public class UpdateModuleImpl {
|
||||
});
|
||||
}
|
||||
|
||||
public static void setUuid(UpdateContext updateContext, String uuid, Promise promise) {
|
||||
public static void setUuid(final UpdateContext updateContext, final String uuid, final Promise promise) {
|
||||
UiThreadUtil.runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
@@ -253,7 +258,7 @@ public class UpdateModuleImpl {
|
||||
}
|
||||
|
||||
|
||||
public static void setLocalHashInfo(UpdateContext updateContext, final String hash, final String info, Promise promise) {
|
||||
public static void setLocalHashInfo(final UpdateContext updateContext, final String hash, final String info, final Promise promise) {
|
||||
UiThreadUtil.runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
@@ -268,7 +273,7 @@ public class UpdateModuleImpl {
|
||||
});
|
||||
}
|
||||
|
||||
public static void getLocalHashInfo(UpdateContext updateContext, final String hash, Promise promise) {
|
||||
public static void getLocalHashInfo(UpdateContext updateContext, final String hash, final Promise promise) {
|
||||
String value = updateContext.getKv("hash_" + hash);
|
||||
if (check(value)) {
|
||||
promise.resolve(value);
|
||||
|
@@ -99,7 +99,7 @@ public class UpdateModule extends NativePushySpec {
|
||||
|
||||
@Override
|
||||
public void restartApp(Promise promise) {
|
||||
UpdateModuleImpl.restartApp(updateContext, mContext, promise);
|
||||
UpdateModuleImpl.restartApp(mContext, promise);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -20,7 +20,6 @@ import com.facebook.react.bridge.UiThreadUtil;
|
||||
import com.facebook.react.bridge.JSBundleLoader;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.facebook.react.modules.core.DeviceEventManagerModule;
|
||||
import com.jakewharton.processphoenix.ProcessPhoenix;
|
||||
|
||||
import java.io.File;
|
||||
import java.lang.reflect.Field;
|
||||
|
@@ -1 +0,0 @@
|
||||
../../../../../harmony/oh_modules/.ohpm/@rnoh+react-native-openharmony@0.72.38/oh_modules/@rnoh/react-native-openharmony
|
BIN
harmony/pushy.har
Normal file
@@ -3,7 +3,7 @@ import fileIo from '@ohos.file.fs';
|
||||
import common from '@ohos.app.ability.common';
|
||||
import { UpdateContext } from './UpdateContext';
|
||||
|
||||
export class FileJSBundleProvider extends JSBundleProvider {
|
||||
export class PushyFileJSBundleProvider extends JSBundleProvider {
|
||||
private updateContext: UpdateContext;
|
||||
private filePath: string = ''
|
||||
|
@@ -1,10 +1,40 @@
|
||||
import ExpoModulesCore
|
||||
import react_native_update
|
||||
import React
|
||||
|
||||
public final class ExpoPushyReactDelegateHandler: ExpoReactDelegateHandler {
|
||||
private weak var reactDelegate: ExpoReactDelegate?
|
||||
|
||||
public override func bundleURL(reactDelegate: ExpoReactDelegate) -> URL? {
|
||||
return RCTPushy.bundleURL()
|
||||
}
|
||||
#if EXPO_SUPPORTS_BUNDLEURL
|
||||
// This code block compiles only if EXPO_SUPPORTS_BUNDLEURL is defined
|
||||
// For expo-modules-core >= 1.12.0
|
||||
|
||||
// Override bundleURL, which is the primary mechanism for these versions.
|
||||
// Expo's default createBridge implementation should respect this.
|
||||
override public func bundleURL(reactDelegate: ExpoReactDelegate) -> URL? {
|
||||
let bundleURL = RCTPushy.bundleURL()
|
||||
print("PushyHandler: Using bundleURL: \(bundleURL?.absoluteString ?? "nil")")
|
||||
return bundleURL
|
||||
}
|
||||
|
||||
// No createBridge override needed here, rely on default behavior using the bundleURL override.
|
||||
|
||||
#else
|
||||
// This code block compiles only if EXPO_SUPPORTS_BUNDLEURL is NOT defined
|
||||
// For expo-modules-core < 1.12.0
|
||||
|
||||
// No bundleURL override possible here.
|
||||
|
||||
// createBridge is the mechanism to customize the URL here.
|
||||
// We completely override it and do not call super.
|
||||
override public func createBridge(reactDelegate: ExpoReactDelegate, bridgeDelegate: RCTBridgeDelegate, launchOptions: [AnyHashable: Any]?) -> RCTBridge? {
|
||||
let bundleURL = RCTPushy.bundleURL()
|
||||
// Print the URL being provided to the initializer
|
||||
print("PushyHandler: createBridge bundleURL: \(bundleURL?.absoluteString ?? "nil")")
|
||||
|
||||
// Directly create the bridge using the bundleURL initializer.
|
||||
// Pass nil for moduleProvider, assuming default behavior is sufficient.
|
||||
// WARNING: If bundleURL is nil, this initialization might fail silently or crash.
|
||||
return RCTBridge(bundleURL: bundleURL, moduleProvider: nil, launchOptions: launchOptions)
|
||||
}
|
||||
|
||||
#endif // EXPO_SUPPORTS_BUNDLEURL
|
||||
}
|
||||
|
46
package.json
@@ -1,28 +1,29 @@
|
||||
{
|
||||
"name": "react-native-update",
|
||||
"version": "10.28.0",
|
||||
"version": "10.29.0",
|
||||
"description": "react-native hot update",
|
||||
"main": "src/index",
|
||||
"scripts": {
|
||||
"prepack": "yarn submodule && yarn lint",
|
||||
"postinstall": "node scripts/check-expo-version.js",
|
||||
"prepack": "bun submodule && bun lint",
|
||||
"lint": "eslint \"src/*.@(ts|tsx|js|jsx)\" && tsc --noEmit",
|
||||
"submodule": "git submodule update --init --recursive",
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"build-lib": "yarn submodule && $ANDROID_HOME/ndk/20.1.5948944/ndk-build NDK_PROJECT_PATH=android APP_BUILD_SCRIPT=android/jni/Android.mk NDK_APPLICATION_MK=android/jni/Application.mk NDK_LIBS_OUT=android/lib",
|
||||
"build:ios-debug": "cd Example/testHotUpdate && yarn && detox build --configuration ios.sim.debug",
|
||||
"build:ios-release": "cd Example/testHotUpdate && yarn && detox build --configuration ios.sim.release",
|
||||
"build-lib": "bun submodule && $ANDROID_HOME/ndk/20.1.5948944/ndk-build NDK_PROJECT_PATH=android APP_BUILD_SCRIPT=android/jni/Android.mk NDK_APPLICATION_MK=android/jni/Application.mk NDK_LIBS_OUT=android/lib",
|
||||
"build:ios-debug": "cd Example/testHotUpdate && bun && detox build --configuration ios.sim.debug",
|
||||
"build:ios-release": "cd Example/testHotUpdate && bun && detox build --configuration ios.sim.release",
|
||||
"test:ios-debug": "cd Example/testHotUpdate && detox test --configuration ios.sim.debug",
|
||||
"test:ios-release": "cd Example/testHotUpdate && yarn detox test --configuration ios.sim.release",
|
||||
"build:android-debug": "cd Example/testHotUpdate && yarn && detox build --configuration android.emu.debug",
|
||||
"build:android-release": "cd Example/testHotUpdate && yarn && detox build --configuration android.emu.release",
|
||||
"test:android-release": "cd Example/testHotUpdate && yarn detox test --configuration android.emu.release --headless --record-logs all",
|
||||
"test:ios-release": "cd Example/testHotUpdate && bun detox test --configuration ios.sim.release",
|
||||
"build:android-debug": "cd Example/testHotUpdate && bun && detox build --configuration android.emu.debug",
|
||||
"build:android-release": "cd Example/testHotUpdate && bun && detox build --configuration android.emu.release",
|
||||
"test:android-release": "cd Example/testHotUpdate && bun detox test --configuration android.emu.release --headless --record-logs all",
|
||||
"test:android-debug": "cd Example/testHotUpdate && detox test --configuration android.emu.debug --headless --record-logs all",
|
||||
"e2e:ios": "npm run build:ios-release && npm run test:ios-release",
|
||||
"e2e:android": "npm run build:android-release && npm run test:android-release",
|
||||
"tests:emulator:prepare": "cd .github/workflows/scripts/functions && yarn && yarn build",
|
||||
"tests:emulator:start-ci": "yarn tests:emulator:prepare && cd ./.github/workflows/scripts && ./start-firebase-emulator.sh",
|
||||
"e2e:ios": "bun build:ios-release && bun test:ios-release",
|
||||
"e2e:android": "bun build:android-release && bun test:android-release",
|
||||
"tests:emulator:prepare": "cd .github/workflows/scripts/functions && bun && bun build",
|
||||
"tests:emulator:start-ci": "bun tests:emulator:prepare && cd ./.github/workflows/scripts && ./start-firebase-emulator.sh",
|
||||
"tests:packager:jet-ci": "cd Example/testHotUpdate && cross-env TMPDIR=$HOME/.metro REACT_DEBUGGER=\"echo nope\" node_modules/.bin/react-native start --no-interactive",
|
||||
"tests:ios:pod:install": "cd Example/testHotUpdate && yarn && yarn pod-install"
|
||||
"tests:ios:pod:install": "cd Example/testHotUpdate && bun && bun pod-install"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -56,23 +57,20 @@
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.25.8",
|
||||
"@react-native/babel-preset": "^0.73.21",
|
||||
"@react-native/eslint-config": "^0.73.2",
|
||||
"@react-native/typescript-config": "^0.74.0",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/jest": "^29.5.13",
|
||||
"@types/node": "^22.7.6",
|
||||
"@react-native/eslint-config": "0.79.1",
|
||||
"@react-native/typescript-config": "0.79.1",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^22.15.2",
|
||||
"@types/react": "^18.3.11",
|
||||
"detox": "^20.27.3",
|
||||
"detox": "^20.37.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-ft-flow": "^3.0.7",
|
||||
"firebase-tools": "^13.22.1",
|
||||
"fs-extra": "^11.2.0",
|
||||
"jest": "^29.7.0",
|
||||
"pod-install": "^0.2.2",
|
||||
"pod-install": "^0.3.7",
|
||||
"prettier": "^2",
|
||||
"react": "18.2.0",
|
||||
"react-native": "0.73",
|
||||
"ts-jest": "^29.2.5",
|
||||
"ts-jest": "^29.3.2",
|
||||
"typescript": "^5.6.3"
|
||||
}
|
||||
}
|
||||
|
@@ -1,12 +1,83 @@
|
||||
require 'json'
|
||||
require 'rubygems' # Required for version comparison
|
||||
|
||||
|
||||
new_arch_enabled = ENV['RCT_NEW_ARCH_ENABLED'] == '1'
|
||||
|
||||
package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
|
||||
|
||||
podspec_dir = File.dirname(__FILE__)
|
||||
|
||||
Pod::Spec.new do |s|
|
||||
|
||||
is_expo_in_podfile = false
|
||||
begin
|
||||
# Check Podfile for use_expo_modules!
|
||||
podfile_path = File.join(Pod::Config.instance.installation_root, 'Podfile')
|
||||
if File.exist?(podfile_path)
|
||||
podfile_content = File.read(podfile_path)
|
||||
is_expo_in_podfile = podfile_content.include?('use_expo_modules!')
|
||||
end
|
||||
rescue => e
|
||||
# Silently ignore errors during check
|
||||
end
|
||||
|
||||
# Determine final validity by checking Podfile presence AND Expo version
|
||||
valid_expo_project = false # Default
|
||||
if is_expo_in_podfile
|
||||
# Only check expo version if use_expo_modules! is present
|
||||
is_version_sufficient = false
|
||||
begin
|
||||
expo_version_str = `node --print \"require('expo/package.json').version\"`.strip
|
||||
if expo_version_str && !expo_version_str.empty?
|
||||
match = expo_version_str.match(/^\d+/)
|
||||
if match
|
||||
major_version = match[0].to_i
|
||||
is_version_sufficient = major_version >= 50
|
||||
end
|
||||
end
|
||||
rescue
|
||||
# Node command failed, version remains insufficient
|
||||
end
|
||||
|
||||
# Final check
|
||||
valid_expo_project = is_version_sufficient
|
||||
end
|
||||
|
||||
# Set platform based on whether it's a valid Expo project and if we can parse its target
|
||||
final_ios_deployment_target = '11.0' # Default target
|
||||
|
||||
if valid_expo_project
|
||||
# --- Try to find and parse ExpoModulesCore.podspec only if it's an Expo project ---
|
||||
parsed_expo_ios_target = nil
|
||||
expo_modules_core_podspec_path = begin
|
||||
package_json_path = `node -p "require.resolve('expo-modules-core/package.json')"`.strip
|
||||
File.join(File.dirname(package_json_path), 'ExpoModulesCore.podspec') if $?.success? && package_json_path && !package_json_path.empty?
|
||||
rescue
|
||||
nil
|
||||
end
|
||||
|
||||
if expo_modules_core_podspec_path && File.exist?(expo_modules_core_podspec_path)
|
||||
begin
|
||||
content = File.read(expo_modules_core_podspec_path)
|
||||
match = content.match(/s\.platforms\s*=\s*\{[\s\S]*?:ios\s*=>\s*'([^\']+)'/) # Match within s.platforms hash
|
||||
if match && match[1]
|
||||
parsed_expo_ios_target = match[1]
|
||||
else
|
||||
match = content.match(/s\.platform\s*=\s*:ios,\s*'([^\']+)'/) # Fallback to s.platform = :ios, 'version'
|
||||
if match && match[1]
|
||||
parsed_expo_ios_target = match[1]
|
||||
end
|
||||
end
|
||||
rescue => e
|
||||
# Pod::UI.warn "Failed to read or parse ExpoModulesCore.podspec content: #{e.message}"
|
||||
end
|
||||
end
|
||||
if parsed_expo_ios_target
|
||||
final_ios_deployment_target = parsed_expo_ios_target
|
||||
end
|
||||
end
|
||||
|
||||
s.platforms = { :ios => final_ios_deployment_target }
|
||||
|
||||
s.name = package['name']
|
||||
s.version = package['version']
|
||||
s.summary = package['description']
|
||||
@@ -16,10 +87,16 @@ Pod::Spec.new do |s|
|
||||
s.homepage = package['homepage']
|
||||
|
||||
s.cocoapods_version = '>= 1.6.0'
|
||||
s.platform = :ios, "8.0"
|
||||
s.platforms = { :ios => "11.0" }
|
||||
|
||||
s.source = { :git => 'https://github.com/reactnativecn/react-native-update.git', :tag => '#{s.version}' }
|
||||
s.source_files = Dir.glob("ios/**/*.{h,m,mm,swift}").reject { |f| f.start_with?("ios/Expo/") }
|
||||
|
||||
# Conditionally set source files
|
||||
if valid_expo_project
|
||||
s.source_files = Dir.glob("ios/**/*.{h,m,mm,swift}") # Include Expo files
|
||||
else
|
||||
s.source_files = Dir.glob("ios/**/*.{h,m,mm,swift}").reject { |f| f.start_with?("ios/Expo/") } # Exclude Expo files
|
||||
end
|
||||
|
||||
s.libraries = 'bz2', 'z'
|
||||
s.vendored_libraries = 'RCTPushy/libRCTPushy.a'
|
||||
s.pod_target_xcconfig = {
|
||||
@@ -33,23 +110,14 @@ Pod::Spec.new do |s|
|
||||
s.dependency "React-Core"
|
||||
s.dependency 'SSZipArchive'
|
||||
|
||||
project_root = File.expand_path('../../', __dir__)
|
||||
project_package_json = File.join(project_root, 'package.json')
|
||||
is_expo_project = false
|
||||
|
||||
if (File.exist?(project_package_json))
|
||||
package_json = JSON.parse(File.read(project_package_json))
|
||||
has_expo_dependency = package_json['dependencies'] && package_json['dependencies']['expo']
|
||||
has_expo_modules_core = Dir.exist?('node_modules/expo-modules-core')
|
||||
is_expo_project = has_expo_dependency || has_expo_modules_core
|
||||
if is_expo_project
|
||||
s.dependency 'ExpoModulesCore'
|
||||
end
|
||||
# Conditionally add Expo dependency
|
||||
if valid_expo_project
|
||||
s.dependency 'ExpoModulesCore'
|
||||
end
|
||||
|
||||
s.subspec 'RCTPushy' do |ss|
|
||||
ss.source_files = 'ios/RCTPushy/*.{h,m,mm,swift}'
|
||||
ss.public_header_files = ['ios/RCTPushy/RCTPushy.h']
|
||||
ss.public_header_files = ['ios/RCTPushy/*.h']
|
||||
end
|
||||
|
||||
s.subspec 'HDiffPatch' do |ss|
|
||||
@@ -62,9 +130,36 @@ Pod::Spec.new do |s|
|
||||
ss.public_header_files = 'ios/RCTPushy/HDiffPatch/**/*.h'
|
||||
end
|
||||
|
||||
if is_expo_project
|
||||
# Conditionally add Expo subspec and check ExpoModulesCore version
|
||||
if valid_expo_project
|
||||
supports_bundle_url_final = false # Default
|
||||
|
||||
# 1. Try executing node to get the version string
|
||||
expo_modules_core_version_str = begin
|
||||
# Use node to directly require expo-modules-core/package.json and get its version
|
||||
`node --print \"require('expo-modules-core/package.json').version\"` # Execute, keep raw output
|
||||
rescue
|
||||
# Node command failed (e.g., node not found, package not found). Return empty string.
|
||||
''
|
||||
end
|
||||
|
||||
# 2. Process the obtained version string (if not empty)
|
||||
if expo_modules_core_version_str && !expo_modules_core_version_str.empty?
|
||||
begin
|
||||
# Compare versions using Gem::Version (handles trailing newline)
|
||||
installed_version = Gem::Version.new(expo_modules_core_version_str)
|
||||
target_version = Gem::Version.new('1.12.0')
|
||||
supports_bundle_url_final = installed_version >= target_version
|
||||
rescue ArgumentError
|
||||
# If Gem::Version fails parsing, supports_bundle_url_final remains false.
|
||||
end
|
||||
end
|
||||
|
||||
s.subspec 'Expo' do |ss|
|
||||
ss.source_files = 'ios/Expo/**/*.{h,m,mm,swift}'
|
||||
if supports_bundle_url_final
|
||||
ss.pod_target_xcconfig = { 'SWIFT_ACTIVE_COMPILATION_CONDITIONS' => 'EXPO_SUPPORTS_BUNDLEURL' }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -79,7 +174,7 @@ Pod::Spec.new do |s|
|
||||
s.pod_target_xcconfig = {
|
||||
"HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost\"",
|
||||
"CLANG_CXX_LANGUAGE_STANDARD" => "c++17"
|
||||
}
|
||||
}.merge(s.pod_target_xcconfig)
|
||||
s.dependency "React-Codegen"
|
||||
s.dependency "RCT-Folly"
|
||||
s.dependency "RCTRequired"
|
||||
|
92
scripts/check-expo-version.js
Normal file
@@ -0,0 +1,92 @@
|
||||
const ownPackageJson = require('../package.json');
|
||||
|
||||
if (process.env.npm_package_name === ownPackageJson.name) {
|
||||
console.log('Skipping postinstall during local development.');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const projectRoot = path.resolve(__dirname, '..'); // react-native-update module root
|
||||
const expoConfigPath = path.resolve(projectRoot, 'expo-module.config.json');
|
||||
|
||||
function getExpoMajorVersion() {
|
||||
let resolvedExpoPackagePath;
|
||||
try {
|
||||
// Use require.resolve to find expo's package.json from the host project's perspective
|
||||
resolvedExpoPackagePath = require.resolve('expo/package.json', {
|
||||
paths: [path.resolve(projectRoot, '..', '..')],
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(
|
||||
'Expo not found in project node_modules (via require.resolve).',
|
||||
);
|
||||
return null; // Expo not found or resolvable
|
||||
}
|
||||
|
||||
// Check if the resolved path actually exists (belt-and-suspenders)
|
||||
if (!fs.existsSync(resolvedExpoPackagePath)) {
|
||||
console.log(
|
||||
`Expo package.json path resolved to ${resolvedExpoPackagePath}, but file does not exist.`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const packageJson = JSON.parse(
|
||||
fs.readFileSync(resolvedExpoPackagePath, 'utf8'),
|
||||
);
|
||||
const version = packageJson.version;
|
||||
if (!version) {
|
||||
console.log('Expo package.json does not contain a version.');
|
||||
return null; // Version not found
|
||||
}
|
||||
|
||||
// Extract the first number sequence as the major version
|
||||
const match = version.match(/\d+/);
|
||||
if (!match) {
|
||||
console.log(
|
||||
`Could not parse major version from Expo version string: ${version}`,
|
||||
);
|
||||
return null; // Cannot parse version
|
||||
}
|
||||
|
||||
return parseInt(match[0], 10);
|
||||
} catch (error) {
|
||||
console.error('Error reading or parsing Expo package.json:', error);
|
||||
return null; // Error during processing
|
||||
}
|
||||
}
|
||||
|
||||
function checkAndCleanExpoConfig() {
|
||||
const majorVersion = getExpoMajorVersion();
|
||||
|
||||
// Condition: Expo not found OR major version is less than 50
|
||||
if (majorVersion === null || majorVersion < 50) {
|
||||
if (fs.existsSync(expoConfigPath)) {
|
||||
try {
|
||||
fs.unlinkSync(expoConfigPath);
|
||||
console.log(
|
||||
`Expo version (${
|
||||
majorVersion !== null ? majorVersion : 'not found'
|
||||
}) is < 50 or Expo not found. Deleted ${expoConfigPath}`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(`Failed to delete ${expoConfigPath}:`, error);
|
||||
}
|
||||
} else {
|
||||
console.log(
|
||||
`Expo version (${
|
||||
majorVersion !== null ? majorVersion : 'not found'
|
||||
}) is < 50 or Expo not found. ${expoConfigPath} does not exist, no action needed.`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
console.log(
|
||||
`Expo version (${majorVersion}) is >= 50. Kept ${expoConfigPath}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
checkAndCleanExpoConfig();
|
257
src/client.ts
@@ -1,32 +1,33 @@
|
||||
import { CheckResult, ClientOptions, ProgressData, EventType } from './type';
|
||||
import {
|
||||
assertDev,
|
||||
DeviceEventEmitter,
|
||||
EmitterSubscription,
|
||||
Platform,
|
||||
} from 'react-native';
|
||||
import {
|
||||
PushyModule,
|
||||
buildTime,
|
||||
cInfo,
|
||||
currentVersion,
|
||||
getCurrentVersionInfo,
|
||||
isFirstTime,
|
||||
isRolledBack,
|
||||
packageVersion,
|
||||
pushyNativeEventEmitter,
|
||||
rolledBackVersion,
|
||||
setLocalHashInfo,
|
||||
} from './core';
|
||||
import { PermissionsAndroid } from './permissions';
|
||||
import { CheckResult, ClientOptions, EventType, ProgressData } from './type';
|
||||
import {
|
||||
assertWeb,
|
||||
emptyObj,
|
||||
enhancedFetch,
|
||||
joinUrls,
|
||||
log,
|
||||
noop,
|
||||
promiseAny,
|
||||
testUrls,
|
||||
} from './utils';
|
||||
import {
|
||||
EmitterSubscription,
|
||||
Platform,
|
||||
DeviceEventEmitter,
|
||||
} from 'react-native';
|
||||
import { PermissionsAndroid } from './permissions';
|
||||
import {
|
||||
PushyModule,
|
||||
buildTime,
|
||||
cInfo,
|
||||
pushyNativeEventEmitter,
|
||||
currentVersion,
|
||||
packageVersion,
|
||||
rolledBackVersion,
|
||||
setLocalHashInfo,
|
||||
isFirstTime,
|
||||
isRolledBack,
|
||||
} from './core';
|
||||
|
||||
const SERVER_PRESETS = {
|
||||
// cn
|
||||
@@ -60,6 +61,31 @@ const defaultClientOptions: ClientOptions = {
|
||||
throwError: false,
|
||||
};
|
||||
|
||||
export const sharedState: {
|
||||
progressHandlers: Record<string, EmitterSubscription>;
|
||||
downloadedHash?: string;
|
||||
apkStatus: 'downloading' | 'downloaded' | null;
|
||||
marked: boolean;
|
||||
applyingUpdate: boolean;
|
||||
} = {
|
||||
progressHandlers: {},
|
||||
downloadedHash: undefined,
|
||||
apkStatus: null,
|
||||
marked: false,
|
||||
applyingUpdate: false,
|
||||
};
|
||||
|
||||
const assertHash = (hash: string) => {
|
||||
if (!sharedState.downloadedHash) {
|
||||
return;
|
||||
}
|
||||
if (hash !== sharedState.downloadedHash) {
|
||||
log(`use downloaded hash ${sharedState.downloadedHash} first`);
|
||||
return;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// for China users
|
||||
export class Pushy {
|
||||
options = defaultClientOptions;
|
||||
@@ -67,13 +93,6 @@ export class Pushy {
|
||||
lastChecking?: number;
|
||||
lastRespJson?: Promise<any>;
|
||||
|
||||
static progressHandlers: Record<string, EmitterSubscription> = {};
|
||||
static downloadedHash?: string;
|
||||
|
||||
static apkStatus: 'downloading' | 'downloaded' | null = null;
|
||||
|
||||
static marked = false;
|
||||
static applyingUpdate = false;
|
||||
version = cInfo.rnu;
|
||||
loggerPromise = (() => {
|
||||
let resolve: (value?: unknown) => void = () => {};
|
||||
@@ -128,6 +147,7 @@ export class Pushy {
|
||||
log(type + ' ' + message);
|
||||
await this.loggerPromise.promise;
|
||||
const { logger = noop, appKey } = this.options;
|
||||
const info = await getCurrentVersionInfo();
|
||||
logger({
|
||||
type,
|
||||
data: {
|
||||
@@ -137,6 +157,7 @@ export class Pushy {
|
||||
packageVersion,
|
||||
buildTime,
|
||||
message,
|
||||
...info,
|
||||
...data,
|
||||
},
|
||||
});
|
||||
@@ -149,55 +170,47 @@ export class Pushy {
|
||||
getCheckUrl = (endpoint: string = this.options.server!.main) => {
|
||||
return `${endpoint}/checkUpdate/${this.options.appKey}`;
|
||||
};
|
||||
static assertHash = (hash: string) => {
|
||||
if (!this.downloadedHash) {
|
||||
return;
|
||||
}
|
||||
if (hash !== this.downloadedHash) {
|
||||
log(`use downloaded hash ${Pushy.downloadedHash} first`);
|
||||
return;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
assertDebug = () => {
|
||||
assertDebug = (matter: string) => {
|
||||
if (__DEV__ && !this.options.debug) {
|
||||
console.info(
|
||||
'You are currently in the development environment and have not enabled debug mode. The hot update check will not be performed. If you need to debug hot updates in the development environment, please set debug to true in the client.',
|
||||
`You are currently in the development environment and have not enabled debug mode.
|
||||
${matter} will not be performed.
|
||||
If you need to debug ${matter} in the development environment, please set debug to true in the client.`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
markSuccess = () => {
|
||||
if (Pushy.marked || __DEV__ || !isFirstTime) {
|
||||
if (sharedState.marked || __DEV__ || !isFirstTime) {
|
||||
return;
|
||||
}
|
||||
Pushy.marked = true;
|
||||
sharedState.marked = true;
|
||||
PushyModule.markSuccess();
|
||||
this.report({ type: 'markSuccess' });
|
||||
};
|
||||
switchVersion = async (hash: string) => {
|
||||
if (!assertDev('switchVersion()')) {
|
||||
if (!this.assertDebug('switchVersion()')) {
|
||||
return;
|
||||
}
|
||||
if (Pushy.assertHash(hash) && !Pushy.applyingUpdate) {
|
||||
if (assertHash(hash) && !sharedState.applyingUpdate) {
|
||||
log('switchVersion: ' + hash);
|
||||
Pushy.applyingUpdate = true;
|
||||
sharedState.applyingUpdate = true;
|
||||
return PushyModule.reloadUpdate({ hash });
|
||||
}
|
||||
};
|
||||
|
||||
switchVersionLater = async (hash: string) => {
|
||||
if (!assertDev('switchVersionLater()')) {
|
||||
if (!this.assertDebug('switchVersionLater()')) {
|
||||
return;
|
||||
}
|
||||
if (Pushy.assertHash(hash)) {
|
||||
if (assertHash(hash)) {
|
||||
log('switchVersionLater: ' + hash);
|
||||
return PushyModule.setNeedUpdate({ hash });
|
||||
}
|
||||
};
|
||||
checkUpdate = async (extra?: Record<string, any>) => {
|
||||
if (!this.assertDebug()) {
|
||||
if (!this.assertDebug('checkUpdate()')) {
|
||||
return;
|
||||
}
|
||||
if (!assertWeb()) {
|
||||
@@ -250,7 +263,7 @@ export class Pushy {
|
||||
type: 'checking',
|
||||
message: this.options.appKey + ': ' + stringifyBody,
|
||||
});
|
||||
resp = await fetch(this.getCheckUrl(), fetchPayload);
|
||||
resp = await enhancedFetch(this.getCheckUrl(), fetchPayload);
|
||||
} catch (e: any) {
|
||||
this.report({
|
||||
type: 'errorChecking',
|
||||
@@ -261,7 +274,7 @@ export class Pushy {
|
||||
try {
|
||||
resp = await promiseAny(
|
||||
backupEndpoints.map(endpoint =>
|
||||
fetch(this.getCheckUrl(endpoint), fetchPayload),
|
||||
enhancedFetch(this.getCheckUrl(endpoint), fetchPayload),
|
||||
),
|
||||
);
|
||||
} catch (err: any) {
|
||||
@@ -346,18 +359,18 @@ export class Pushy {
|
||||
log(`rolledback hash ${rolledBackVersion}, ignored`);
|
||||
return;
|
||||
}
|
||||
if (Pushy.downloadedHash === hash) {
|
||||
log(`duplicated downloaded hash ${Pushy.downloadedHash}, ignored`);
|
||||
return Pushy.downloadedHash;
|
||||
if (sharedState.downloadedHash === hash) {
|
||||
log(`duplicated downloaded hash ${sharedState.downloadedHash}, ignored`);
|
||||
return sharedState.downloadedHash;
|
||||
}
|
||||
if (Pushy.progressHandlers[hash]) {
|
||||
if (sharedState.progressHandlers[hash]) {
|
||||
return;
|
||||
}
|
||||
const patchStartTime = Date.now();
|
||||
if (onDownloadProgress) {
|
||||
// @ts-expect-error harmony not in existing platforms
|
||||
if (Platform.OS === 'harmony') {
|
||||
Pushy.progressHandlers[hash] = DeviceEventEmitter.addListener(
|
||||
sharedState.progressHandlers[hash] = DeviceEventEmitter.addListener(
|
||||
'RCTPushyDownloadProgress',
|
||||
progressData => {
|
||||
if (progressData.hash === hash) {
|
||||
@@ -366,14 +379,15 @@ export class Pushy {
|
||||
},
|
||||
);
|
||||
} else {
|
||||
Pushy.progressHandlers[hash] = pushyNativeEventEmitter.addListener(
|
||||
'RCTPushyDownloadProgress',
|
||||
progressData => {
|
||||
if (progressData.hash === hash) {
|
||||
onDownloadProgress(progressData);
|
||||
}
|
||||
},
|
||||
);
|
||||
sharedState.progressHandlers[hash] =
|
||||
pushyNativeEventEmitter.addListener(
|
||||
'RCTPushyDownloadProgress',
|
||||
progressData => {
|
||||
if (progressData.hash === hash) {
|
||||
onDownloadProgress(progressData);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
let succeeded = '';
|
||||
@@ -381,7 +395,7 @@ export class Pushy {
|
||||
let lastError: any;
|
||||
let errorMessages: string[] = [];
|
||||
const diffUrl = await testUrls(joinUrls(paths, diff));
|
||||
if (diffUrl) {
|
||||
if (diffUrl && !__DEV__) {
|
||||
log('downloading diff');
|
||||
try {
|
||||
await PushyModule.downloadPatchFromPpk({
|
||||
@@ -394,59 +408,55 @@ export class Pushy {
|
||||
const errorMessage = `diff error: ${e.message}`;
|
||||
errorMessages.push(errorMessage);
|
||||
lastError = new Error(errorMessage);
|
||||
if (__DEV__) {
|
||||
succeeded = 'diff';
|
||||
} else {
|
||||
log(errorMessage);
|
||||
}
|
||||
log(errorMessage);
|
||||
}
|
||||
}
|
||||
const pdiffUrl = await testUrls(joinUrls(paths, pdiff));
|
||||
if (!succeeded && pdiffUrl) {
|
||||
log('downloading pdiff');
|
||||
try {
|
||||
await PushyModule.downloadPatchFromPackage({
|
||||
updateUrl: pdiffUrl,
|
||||
hash,
|
||||
});
|
||||
succeeded = 'pdiff';
|
||||
} catch (e: any) {
|
||||
const errorMessage = `pdiff error: ${e.message}`;
|
||||
errorMessages.push(errorMessage);
|
||||
lastError = new Error(errorMessage);
|
||||
if (__DEV__) {
|
||||
if (!succeeded) {
|
||||
const pdiffUrl = await testUrls(joinUrls(paths, pdiff));
|
||||
if (pdiffUrl && !__DEV__) {
|
||||
log('downloading pdiff');
|
||||
try {
|
||||
await PushyModule.downloadPatchFromPackage({
|
||||
updateUrl: pdiffUrl,
|
||||
hash,
|
||||
});
|
||||
succeeded = 'pdiff';
|
||||
} else {
|
||||
} catch (e: any) {
|
||||
const errorMessage = `pdiff error: ${e.message}`;
|
||||
errorMessages.push(errorMessage);
|
||||
lastError = new Error(errorMessage);
|
||||
log(errorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
const fullUrl = await testUrls(joinUrls(paths, full));
|
||||
if (!succeeded && fullUrl) {
|
||||
log('downloading full patch');
|
||||
try {
|
||||
await PushyModule.downloadFullUpdate({
|
||||
updateUrl: fullUrl,
|
||||
hash,
|
||||
});
|
||||
succeeded = 'full';
|
||||
} catch (e: any) {
|
||||
const errorMessage = `full patch error: ${e.message}`;
|
||||
errorMessages.push(errorMessage);
|
||||
lastError = new Error(errorMessage);
|
||||
if (__DEV__) {
|
||||
if (!succeeded) {
|
||||
const fullUrl = await testUrls(joinUrls(paths, full));
|
||||
if (fullUrl) {
|
||||
log('downloading full patch');
|
||||
try {
|
||||
await PushyModule.downloadFullUpdate({
|
||||
updateUrl: fullUrl,
|
||||
hash,
|
||||
});
|
||||
succeeded = 'full';
|
||||
} else {
|
||||
} catch (e: any) {
|
||||
const errorMessage = `full patch error: ${e.message}`;
|
||||
errorMessages.push(errorMessage);
|
||||
lastError = new Error(errorMessage);
|
||||
log(errorMessage);
|
||||
}
|
||||
} else if (__DEV__) {
|
||||
log(
|
||||
`当前是开发环境,无法执行增量式热更新,重启不会生效。
|
||||
如果需要在开发环境中测试可生效的全量热更新(但也会在再次重启后重新连接 metro),
|
||||
请打开“忽略时间戳”开关再重试。`,
|
||||
);
|
||||
succeeded = 'full';
|
||||
}
|
||||
}
|
||||
if (Pushy.progressHandlers[hash]) {
|
||||
Pushy.progressHandlers[hash].remove();
|
||||
delete Pushy.progressHandlers[hash];
|
||||
}
|
||||
if (__DEV__) {
|
||||
return hash;
|
||||
if (sharedState.progressHandlers[hash]) {
|
||||
sharedState.progressHandlers[hash].remove();
|
||||
delete sharedState.progressHandlers[hash];
|
||||
}
|
||||
if (!succeeded) {
|
||||
this.report({
|
||||
@@ -479,7 +489,7 @@ export class Pushy {
|
||||
description,
|
||||
metaInfo,
|
||||
});
|
||||
Pushy.downloadedHash = hash;
|
||||
sharedState.downloadedHash = hash;
|
||||
return hash;
|
||||
};
|
||||
downloadAndInstallApk = async (
|
||||
@@ -489,10 +499,10 @@ export class Pushy {
|
||||
if (Platform.OS !== 'android') {
|
||||
return;
|
||||
}
|
||||
if (Pushy.apkStatus === 'downloading') {
|
||||
if (sharedState.apkStatus === 'downloading') {
|
||||
return;
|
||||
}
|
||||
if (Pushy.apkStatus === 'downloaded') {
|
||||
if (sharedState.apkStatus === 'downloaded') {
|
||||
this.report({ type: 'errorInstallApk' });
|
||||
this.throwIfEnabled(new Error('errorInstallApk'));
|
||||
return;
|
||||
@@ -513,35 +523,36 @@ export class Pushy {
|
||||
return;
|
||||
}
|
||||
}
|
||||
Pushy.apkStatus = 'downloading';
|
||||
sharedState.apkStatus = 'downloading';
|
||||
this.report({ type: 'downloadingApk' });
|
||||
const progressKey = 'downloadingApk';
|
||||
if (onDownloadProgress) {
|
||||
if (Pushy.progressHandlers[progressKey]) {
|
||||
Pushy.progressHandlers[progressKey].remove();
|
||||
if (sharedState.progressHandlers[progressKey]) {
|
||||
sharedState.progressHandlers[progressKey].remove();
|
||||
}
|
||||
Pushy.progressHandlers[progressKey] = pushyNativeEventEmitter.addListener(
|
||||
'RCTPushyDownloadProgress',
|
||||
(progressData: ProgressData) => {
|
||||
if (progressData.hash === progressKey) {
|
||||
onDownloadProgress(progressData);
|
||||
}
|
||||
},
|
||||
);
|
||||
sharedState.progressHandlers[progressKey] =
|
||||
pushyNativeEventEmitter.addListener(
|
||||
'RCTPushyDownloadProgress',
|
||||
(progressData: ProgressData) => {
|
||||
if (progressData.hash === progressKey) {
|
||||
onDownloadProgress(progressData);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
await PushyModule.downloadAndInstallApk({
|
||||
url,
|
||||
target: 'update.apk',
|
||||
hash: progressKey,
|
||||
}).catch(() => {
|
||||
Pushy.apkStatus = null;
|
||||
sharedState.apkStatus = null;
|
||||
this.report({ type: 'errorDownloadAndInstallApk' });
|
||||
this.throwIfEnabled(new Error('errorDownloadAndInstallApk'));
|
||||
});
|
||||
Pushy.apkStatus = 'downloaded';
|
||||
if (Pushy.progressHandlers[progressKey]) {
|
||||
Pushy.progressHandlers[progressKey].remove();
|
||||
delete Pushy.progressHandlers[progressKey];
|
||||
sharedState.apkStatus = 'downloaded';
|
||||
if (sharedState.progressHandlers[progressKey]) {
|
||||
sharedState.progressHandlers[progressKey].remove();
|
||||
delete sharedState.progressHandlers[progressKey];
|
||||
}
|
||||
};
|
||||
restartApp = async () => {
|
||||
|
@@ -12,7 +12,7 @@ import {
|
||||
Platform,
|
||||
Linking,
|
||||
} from 'react-native';
|
||||
import { Pushy, Cresc } from './client';
|
||||
import { Pushy, Cresc, sharedState } from './client';
|
||||
import { currentVersion, packageVersion, getCurrentVersionInfo } from './core';
|
||||
import { CheckResult, ProgressData, UpdateTestPayload } from './type';
|
||||
import { UpdateContext } from './context';
|
||||
@@ -171,7 +171,7 @@ export const UpdateProvider = ({
|
||||
return;
|
||||
}
|
||||
const rollout = info.config?.rollout?.[packageVersion];
|
||||
if (rollout) {
|
||||
if (info.update && rollout) {
|
||||
if (!isInRollout(rollout)) {
|
||||
log(`not in ${rollout}% rollout, ignored`);
|
||||
return;
|
||||
@@ -182,8 +182,15 @@ export const UpdateProvider = ({
|
||||
updateInfoRef.current = info;
|
||||
setUpdateInfo(info);
|
||||
if (info.expired) {
|
||||
if (
|
||||
options.onPackageExpired &&
|
||||
(await options.onPackageExpired(info)) === false
|
||||
) {
|
||||
log('onPackageExpired returned false, skipping');
|
||||
return;
|
||||
}
|
||||
const { downloadUrl } = info;
|
||||
if (downloadUrl && Pushy.apkStatus === null) {
|
||||
if (downloadUrl && sharedState.apkStatus === null) {
|
||||
if (options.updateStrategy === 'silentAndNow') {
|
||||
if (Platform.OS === 'android' && downloadUrl.endsWith('.apk')) {
|
||||
downloadAndInstallApk(downloadUrl);
|
||||
@@ -234,7 +241,7 @@ export const UpdateProvider = ({
|
||||
client,
|
||||
alertError,
|
||||
throwErrorIfEnabled,
|
||||
options.updateStrategy,
|
||||
options,
|
||||
alertUpdate,
|
||||
downloadAndInstallApk,
|
||||
downloadUpdate,
|
||||
@@ -244,7 +251,7 @@ export const UpdateProvider = ({
|
||||
const markSuccess = client.markSuccess;
|
||||
|
||||
useEffect(() => {
|
||||
if (!client.assertDebug()) {
|
||||
if (!client.assertDebug('checkUpdate()')) {
|
||||
return;
|
||||
}
|
||||
const { checkStrategy, dismissErrorAfter, autoMarkSuccess } = options;
|
||||
|
@@ -54,6 +54,9 @@ export interface EventData {
|
||||
message?: string;
|
||||
rolledBackVersion?: string;
|
||||
newVersion?: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
metaInfo?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
@@ -89,6 +92,7 @@ export interface ClientOptions {
|
||||
beforeCheckUpdate?: () => Promise<boolean>;
|
||||
beforeDownloadUpdate?: (info: CheckResult) => Promise<boolean>;
|
||||
afterDownloadUpdate?: (info: CheckResult) => Promise<boolean>;
|
||||
onPackageExpired?: (info: CheckResult) => Promise<boolean>;
|
||||
}
|
||||
|
||||
export interface UpdateTestPayload {
|
||||
|
34
src/utils.ts
@@ -40,13 +40,13 @@ const ping =
|
||||
: async (url: string) => {
|
||||
let pingFinished = false;
|
||||
return Promise.race([
|
||||
fetch(url, {
|
||||
enhancedFetch(url, {
|
||||
method: 'HEAD',
|
||||
})
|
||||
.then(({ status, statusText }) => {
|
||||
.then(({ status, statusText, url: finalUrl }) => {
|
||||
pingFinished = true;
|
||||
if (status === 200) {
|
||||
return url;
|
||||
return finalUrl;
|
||||
}
|
||||
log('ping failed', url, status, statusText);
|
||||
throw new Error('Ping failed');
|
||||
@@ -62,14 +62,14 @@ const ping =
|
||||
if (!pingFinished) {
|
||||
log('ping timeout', url);
|
||||
}
|
||||
}, 2000),
|
||||
}, 5000),
|
||||
),
|
||||
]);
|
||||
};
|
||||
|
||||
export function joinUrls(paths: string[], fileName?: string) {
|
||||
if (fileName) {
|
||||
return paths.map(path => 'https://' + path + '/' + fileName);
|
||||
return paths.map(path => `https://${path}/${fileName}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,6 +81,7 @@ export const testUrls = async (urls?: string[]) => {
|
||||
try {
|
||||
const ret = await promiseAny(urls.map(ping));
|
||||
if (ret) {
|
||||
log('ping success, use url:', ret);
|
||||
return ret;
|
||||
}
|
||||
} catch {}
|
||||
@@ -98,12 +99,19 @@ export const assertWeb = () => {
|
||||
return true;
|
||||
};
|
||||
|
||||
export const assertDev = (matter: string) => {
|
||||
if (__DEV__) {
|
||||
console.warn(
|
||||
`${matter} is not supported in development environment; no action taken.`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
// export const isAndroid70AndBelow = () => {
|
||||
// // android 7.0 and below devices do not support letsencrypt cert
|
||||
// // https://letsencrypt.org/2023/07/10/cross-sign-expiration/
|
||||
// return Platform.OS === 'android' && Platform.Version <= 24;
|
||||
// };
|
||||
|
||||
export const enhancedFetch = async (
|
||||
url: string,
|
||||
params: Parameters<typeof fetch>[1],
|
||||
) => {
|
||||
return fetch(url, params).catch(e => {
|
||||
log('fetch error', url, e);
|
||||
log('trying fallback to http');
|
||||
return fetch(url.replace('https', 'http'), params);
|
||||
});
|
||||
};
|
||||
|