3 | /* Header for class cn_reactnative_modules_update_DownloadTask */
4 |
5 | #ifndef _Included_cn_reactnative_modules_update_DownloadTask
6 | #define _Included_cn_reactnative_modules_update_DownloadTask
7 | #ifdef __cplusplus
8 | extern "C" {
9 | #endif
10 | /*
11 | * Class: cn_reactnative_modules_update_DownloadTask
12 | * Method: hdiffPatch
13 | * Signature: ([B[B)[B
14 | */
15 | JNIEXPORT jbyteArray JNICALL Java_cn_reactnative_modules_update_DownloadTask_hdiffPatch
16 | (JNIEnv *, jclass, jbyteArray, jbyteArray);
17 |
18 | #ifdef __cplusplus
19 | }
20 | #endif
21 | #endif
22 |
--------------------------------------------------------------------------------
/Example/testHotUpdate/android/app/build_defs.bzl:
--------------------------------------------------------------------------------
1 | """Helper definitions to glob .aar and .jar targets"""
2 |
3 | def create_aar_targets(aarfiles):
4 | for aarfile in aarfiles:
5 | name = "aars__" + aarfile[aarfile.rindex("/") + 1:aarfile.rindex(".aar")]
6 | lib_deps.append(":" + name)
7 | android_prebuilt_aar(
8 | name = name,
9 | aar = aarfile,
10 | )
11 |
12 | def create_jar_targets(jarfiles):
13 | for jarfile in jarfiles:
14 | name = "jars__" + jarfile[jarfile.rindex("/") + 1:jarfile.rindex(".jar")]
15 | lib_deps.append(":" + name)
16 | prebuilt_jar(
17 | name = name,
18 | binary_jar = jarfile,
19 | )
20 |
--------------------------------------------------------------------------------
/Example/harmony_use_pushy/android/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | buildscript {
4 | ext {
5 | buildToolsVersion = "33.0.0"
6 | minSdkVersion = 21
7 | compileSdkVersion = 33
8 | targetSdkVersion = 33
9 |
10 | // We use NDK 23 which has both M1 support and is the side-by-side NDK version from AGP.
11 | ndkVersion = "23.1.7779620"
12 | }
13 | repositories {
14 | google()
15 | mavenCentral()
16 | }
17 | dependencies {
18 | classpath("com.android.tools.build:gradle")
19 | classpath("com.facebook.react:react-native-gradle-plugin")
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/android/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
10 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/.github/workflows/scripts/functions/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "functions",
3 | "scripts": {
4 | "build": "tsc",
5 | "serve": "npm run build && firebase emulators:start --only functions",
6 | "shell": "npm run build && firebase functions:shell",
7 | "start": "npm run shell",
8 | "deploy": "firebase deploy --only functions",
9 | "logs": "firebase functions:log"
10 | },
11 | "engines": {
12 | "node": "16"
13 | },
14 | "main": "lib/index.js",
15 | "dependencies": {
16 | "firebase-admin": "^11.3.0",
17 | "firebase-functions": "^4.2.1"
18 | },
19 | "devDependencies": {
20 | "firebase-functions-test": "^3.0.0",
21 | "typescript": "^4.9.5"
22 | },
23 | "private": true
24 | }
25 |
--------------------------------------------------------------------------------
/Example/harmony_use_pushy/metro.config.js:
--------------------------------------------------------------------------------
1 | const {mergeConfig, getDefaultConfig} = require('@react-native/metro-config');
2 | const {createHarmonyMetroConfig} = require('@react-native-oh/react-native-harmony/metro.config');
3 |
4 | /**
5 | * @type {import("metro-config").ConfigT}
6 | */
7 | const config = {
8 | transformer: {
9 | getTransformOptions: async () => ({
10 | transform: {
11 | experimentalImportSupport: false,
12 | inlineRequires: true,
13 | },
14 | }),
15 | },
16 | };
17 |
18 | module.exports = mergeConfig(
19 | getDefaultConfig(__dirname),
20 | createHarmonyMetroConfig({
21 | reactNativeHarmonyPackageName: '@react-native-oh/react-native-harmony',
22 | }),
23 | config,
24 | );
25 |
--------------------------------------------------------------------------------
/e2e/starter.test.js:
--------------------------------------------------------------------------------
1 | describe('Example', () => {
2 | beforeAll(async () => {
3 | await device.launchApp();
4 | });
5 |
6 | beforeEach(async () => {
7 | await device.reloadReactNative();
8 | });
9 |
10 | it('should have welcome screen', async () => {
11 | await expect(element(by.id('welcome'))).toBeVisible();
12 | });
13 |
14 | it('should show hello screen after tap', async () => {
15 | await element(by.id('hello_button')).tap();
16 | await expect(element(by.text('Hello!!!'))).toBeVisible();
17 | });
18 |
19 | it('should show world screen after tap', async () => {
20 | await element(by.id('world_button')).tap();
21 | await expect(element(by.text('World!!!'))).toBeVisible();
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/Example/harmony_use_pushy/harmony/entry/build-profile.json5:
--------------------------------------------------------------------------------
1 | {
2 | "apiType": "stageMode",
3 | "buildOption": {
4 | "externalNativeOptions": {
5 | "path": "./src/main/cpp/CMakeLists.txt",
6 | "arguments": "",
7 | "cppFlags": "",
8 | },
9 | },
10 | "buildOptionSet": [
11 | {
12 | "name": "release",
13 | "arkOptions": {
14 | "obfuscation": {
15 | "ruleOptions": {
16 | "enable": false,
17 | "files": [
18 | "./obfuscation-rules.txt"
19 | ]
20 | }
21 | }
22 | }
23 | },
24 | ],
25 | "targets": [
26 | {
27 | "name": "default"
28 | },
29 | {
30 | "name": "ohosTest",
31 | }
32 | ]
33 | }
--------------------------------------------------------------------------------
/harmony/pushy/oh-package-lock.json5:
--------------------------------------------------------------------------------
1 | {
2 | "meta": {
3 | "stableOrder": true,
4 | "enableUnifiedLockfile": false
5 | },
6 | "lockfileVersion": 3,
7 | "ATTENTION": "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.",
8 | "specifiers": {
9 | "@rnoh/react-native-openharmony@^0.72.96": "@rnoh/react-native-openharmony@0.72.96"
10 | },
11 | "packages": {
12 | "@rnoh/react-native-openharmony@0.72.96": {
13 | "name": "",
14 | "version": "0.72.96",
15 | "integrity": "sha512-gBbm8LLyqi5UE7qHWdZYeQnjyncfEpCczKZUP/9M2U1Z7exR0Kya8PMKMwr1ta5ujy7w/hZVC2LomEV4QvBeqA==",
16 | "resolved": "https://ohpm.openharmony.cn/ohpm/@rnoh/react-native-openharmony/-/react-native-openharmony-0.72.96.har",
17 | "registryType": "ohpm"
18 | }
19 | }
20 | }
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .babelrc
2 | .npmignore
3 | .eslintrc
4 | .nvmrc
5 | .travis.yml
6 | Example
7 | android/build
8 | .vscode
9 | .github/
10 |
11 | # OSX
12 | #
13 | .DS_Store
14 |
15 | # Xcode
16 | #
17 | build/
18 | *.pbxuser
19 | !default.pbxuser
20 | *.mode1v3
21 | !default.mode1v3
22 | *.mode2v3
23 | !default.mode2v3
24 | *.perspectivev3
25 | !default.perspectivev3
26 | xcuserdata
27 | *.xccheckout
28 | *.moved-aside
29 | DerivedData
30 | *.hmap
31 | *.ipa
32 | *.xcuserstate
33 | project.xcworkspace
34 |
35 | # Android/IJ
36 | #
37 | .idea
38 | .gradle
39 | local.properties
40 | android/build
41 | android/obj
42 |
43 | # node.js
44 | #
45 | node_modules/
46 | npm-debug.log
47 | Example
48 | yarn.lock
49 | bun.lock
50 |
51 | domains.json
52 | endpoints.json
53 | endpoints_cresc.json
54 |
55 | tea.yaml
56 |
57 | e2e/
--------------------------------------------------------------------------------
/.github/workflows/scripts/firebase.json:
--------------------------------------------------------------------------------
1 | {
2 | "firestore": {
3 | "rules": "firestore.rules",
4 | "indexes": "firestore.indexes.json"
5 | },
6 | "functions": {
7 | "predeploy": [
8 | "yarn",
9 | "yarn --prefix \"$RESOURCE_DIR\" build"
10 | ],
11 | "source": "functions"
12 | },
13 | "database": {
14 | "rules": "database.rules"
15 | },
16 | "storage": {
17 | "rules": "storage.rules"
18 | },
19 | "emulators": {
20 | "auth": {
21 | "port": 9099
22 | },
23 | "database": {
24 | "port": 9000
25 | },
26 | "firestore": {
27 | "port": 8080
28 | },
29 | "functions": {
30 | "port": 5001
31 | },
32 | "storage": {
33 | "port": 9199
34 | },
35 | "ui": {
36 | "enabled": true
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/harmony/pushy/src/main/ets/PushyPackage.ts:
--------------------------------------------------------------------------------
1 | import { RNPackage, TurboModulesFactory } from '@rnoh/react-native-openharmony/ts';
2 | import type { TurboModule, TurboModuleContext } from '@rnoh/react-native-openharmony/ts';
3 | import { PushyTurboModule } from './PushyTurboModule';
4 |
5 | class PushyTurboModulesFactory extends TurboModulesFactory {
6 | createTurboModule(name: string): TurboModule | null {
7 | if (name === 'Pushy') {
8 | return new PushyTurboModule(this.ctx);
9 | }
10 | return null;
11 | }
12 |
13 | hasTurboModule(name: string): boolean {
14 | return name === 'Pushy';
15 | }
16 | }
17 |
18 | export class PushyPackage extends RNPackage {
19 | createTurboModulesFactory(ctx: TurboModuleContext): TurboModulesFactory {
20 | return new PushyTurboModulesFactory(ctx);
21 | }
22 | }
--------------------------------------------------------------------------------
/android/src/main/java/cn/reactnative/modules/update/DownloadTaskParams.java:
--------------------------------------------------------------------------------
1 | package cn.reactnative.modules.update;
2 |
3 | import java.io.File;
4 |
5 | /**
6 | * Created by tdzl2003 on 3/31/16.
7 | */
8 | class DownloadTaskParams {
9 | static final int TASK_TYPE_CLEANUP = 0; //Keep hash & originHash
10 |
11 | static final int TASK_TYPE_PATCH_FULL = 1;
12 | static final int TASK_TYPE_PATCH_FROM_APK = 2;
13 | static final int TASK_TYPE_PATCH_FROM_PPK = 3;
14 | static final int TASK_TYPE_PLAIN_DOWNLOAD = 4;
15 |
16 |
17 | int type;
18 | String url;
19 | String hash;
20 | String originHash;
21 | File targetFile;
22 | File unzipDirectory;
23 | File originDirectory;
24 | UpdateContext.DownloadFileListener listener;
25 | }
26 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Publish Package to npmjs
2 | on:
3 | release:
4 | types: [published]
5 |
6 | permissions:
7 | id-token: write # Required for OIDC
8 | contents: read
9 |
10 | jobs:
11 | build:
12 | runs-on: ubuntu-latest
13 | permissions:
14 | contents: read
15 | id-token: write
16 | steps:
17 | - uses: actions/checkout@v5
18 | - uses: oven-sh/setup-bun@v2
19 | # Setup .npmrc file to publish to npm
20 | - uses: actions/setup-node@v6
21 | with:
22 | node-version: 24
23 | registry-url: 'https://registry.npmjs.org'
24 | - run: bun install --frozen-lockfile
25 | # Ensure npm 11.5.1 or later is installed
26 | - name: Update npm
27 | run: npm install -g npm@latest
28 | - run: npm publish --provenance --access public
29 |
--------------------------------------------------------------------------------
/Example/harmony_use_pushy/android/app/src/release/java/com/harmony_use_pushy/ReactNativeFlipper.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) Meta Platforms, Inc. and affiliates.
3 | *
4 | * This source code is licensed under the MIT license found in the LICENSE file in the root
5 | * directory of this source tree.
6 | */
7 | package com.harmony_use_pushy;
8 |
9 | import android.content.Context;
10 | import com.facebook.react.ReactInstanceManager;
11 |
12 | /**
13 | * Class responsible of loading Flipper inside your React Native application. This is the release
14 | * flavor of it so it's empty as we don't want to load Flipper.
15 | */
16 | public class ReactNativeFlipper {
17 | public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) {
18 | // Do nothing as we don't want to initialize Flipper on Release.
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Example/expoUsePushy/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo": {
3 | "name": "expoUsePushy",
4 | "slug": "expoUsePushy",
5 | "version": "1.0.0",
6 | "orientation": "portrait",
7 | "icon": "./assets/icon.png",
8 | "userInterfaceStyle": "light",
9 | "newArchEnabled": true,
10 | "splash": {
11 | "image": "./assets/splash-icon.png",
12 | "resizeMode": "contain",
13 | "backgroundColor": "#ffffff"
14 | },
15 | "ios": {
16 | "supportsTablet": true,
17 | "bundleIdentifier": "com.anonymous.expoUsePushy"
18 | },
19 | "android": {
20 | "adaptiveIcon": {
21 | "foregroundImage": "./assets/adaptive-icon.png",
22 | "backgroundColor": "#ffffff"
23 | },
24 | "package": "com.anonymous.expoUsePushy"
25 | },
26 | "web": {
27 | "favicon": "./assets/favicon.png"
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/Example/harmony_use_pushy/ios/harmony_use_pushyTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 |
24 |
25 |
--------------------------------------------------------------------------------
/Example/harmony_use_pushy/ios/harmony_use_pushy/AppDelegate.mm:
--------------------------------------------------------------------------------
1 | #import "AppDelegate.h"
2 |
3 | #import
4 |
5 | @implementation AppDelegate
6 |
7 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
8 | {
9 | self.moduleName = @"harmony_use_pushy";
10 | // You can add your custom initial props in the dictionary below.
11 | // They will be passed down to the ViewController used by React Native.
12 | self.initialProps = @{};
13 |
14 | return [super application:application didFinishLaunchingWithOptions:launchOptions];
15 | }
16 |
17 | - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
18 | {
19 | #if DEBUG
20 | return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"];
21 | #else
22 | return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
23 | #endif
24 | }
25 |
26 | @end
27 |
--------------------------------------------------------------------------------
/android/proguard.pro:
--------------------------------------------------------------------------------
1 | # Keep our update module classes
2 | -keepnames class cn.reactnative.modules.update.DownloadTask { *; }
3 | -keepnames class cn.reactnative.modules.update.UpdateModuleImpl { *; }
4 | -keepnames class cn.reactnative.modules.update.** { *; }
5 |
6 | # Keep React Native classes
7 | -keepnames class com.facebook.react.ReactInstanceManager { *; }
8 | -keepnames class com.facebook.react.** { *; }
9 | -keepnames class com.facebook.react.bridge.** { *; }
10 | -keepnames class com.facebook.react.devsupport.** { *; }
11 |
12 | # Keep fields used in reflection
13 | -keepclassmembers class com.facebook.react.ReactActivity { *; }
14 | -keepclassmembers class com.facebook.react.ReactInstanceManager { *; }
15 | -keepclassmembers class com.facebook.react.ReactDelegate { *; }
16 | -keepclassmembers class com.facebook.react.ReactHost { *; }
17 |
18 | -keepnames class expo.modules.ExpoReactHostFactory$ExpoReactHostDelegate { *; }
19 |
--------------------------------------------------------------------------------
/ios/RCTPushy/HDiffPatch/HDiffPatch.mm:
--------------------------------------------------------------------------------
1 | //
2 | // HDiffPatch.m
3 | // RCTPushy
4 | //
5 | // Created by HouSisong, All rights reserved.
6 | //
7 |
8 | #import "HDiffPatch.h"
9 | #include "../../../android/jni/hpatch.h"
10 |
11 | @implementation HDiffPatch
12 |
13 | + (BOOL)hdiffPatch:(NSString *)patch
14 | origin:(NSString *)origin
15 | toDestination:(NSString *)destination
16 | {
17 | if (![[NSFileManager defaultManager] fileExistsAtPath:patch]) {
18 | return NO;
19 | }
20 | if (![[NSFileManager defaultManager] fileExistsAtPath:origin]) {
21 | return NO;
22 | }
23 |
24 | if ([[NSFileManager defaultManager] fileExistsAtPath:destination]) {
25 | [[NSFileManager defaultManager] removeItemAtPath:destination error:nil];
26 | }
27 |
28 | int err = hpatch_by_file([origin UTF8String], [destination UTF8String], [patch UTF8String]);
29 | if (err) {
30 | return NO;
31 | }
32 | return YES;
33 | }
34 |
35 | @end
36 |
--------------------------------------------------------------------------------
/Example/testHotUpdate/android/app/src/main/java/com/awesomeproject/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.awesomeproject
2 |
3 | import com.facebook.react.ReactActivity
4 | import com.facebook.react.ReactActivityDelegate
5 | import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
6 | import com.facebook.react.defaults.DefaultReactActivityDelegate
7 |
8 | class MainActivity : ReactActivity() {
9 |
10 | /**
11 | * Returns the name of the main component registered from JavaScript. This is used to schedule
12 | * rendering of the component.
13 | */
14 | override fun getMainComponentName(): String = "AwesomeProject"
15 |
16 | /**
17 | * Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate]
18 | * which allows you to enable New Architecture with a single boolean flags [fabricEnabled]
19 | */
20 | override fun createReactActivityDelegate(): ReactActivityDelegate =
21 | DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled)
22 | }
23 |
--------------------------------------------------------------------------------
/harmony/pushy/src/main/ets/DownloadTaskParams.ts:
--------------------------------------------------------------------------------
1 | export interface DownloadTaskListener {
2 | onDownloadCompleted(params: DownloadTaskParams): void;
3 | onDownloadFailed(error: Error): void;
4 | }
5 |
6 | /**
7 | * 下载任务参数类
8 | */
9 | export class DownloadTaskParams {
10 | // 任务类型常量
11 | static readonly TASK_TYPE_CLEANUP: number = 0; // 保留hash和originHash
12 | static readonly TASK_TYPE_PATCH_FULL: number = 1; // 全量补丁
13 | static readonly TASK_TYPE_PATCH_FROM_APP: number = 2; // 从APP补丁
14 | static readonly TASK_TYPE_PATCH_FROM_PPK: number = 3; // 从PPK补丁
15 | static readonly TASK_TYPE_PLAIN_DOWNLOAD: number = 4; // 普通下载
16 |
17 | type: number; // 任务类型
18 | url: string; // 下载URL
19 | hash: string; // 文件哈希值
20 | originHash: string; // 原始文件哈希值
21 | targetFile: string; // 目标文件路径
22 | unzipDirectory: string; // 解压目录路径
23 | originDirectory: string; // 原始文件目录路径
24 | listener: DownloadTaskListener; // 下载监听器
25 | }
--------------------------------------------------------------------------------
/harmony/pushy/src/main/ets/Logger.ts:
--------------------------------------------------------------------------------
1 |
2 | import hilog from '@ohos.hilog';
3 |
4 | class Logger {
5 | private domain: number;
6 | private prefix: string;
7 | private format: string = '%{public}s,%{public}s';
8 | private isDebug: boolean;
9 |
10 | constructor(prefix: string = 'MyApp', domain: number = 0xFF00, isDebug = false) {
11 | this.prefix = prefix;
12 | this.domain = domain;
13 | this.isDebug = isDebug;
14 | }
15 |
16 | debug(...args: string[]): void {
17 | if (this.isDebug) {
18 | hilog.debug(this.domain, this.prefix, this.format, args);
19 | }
20 | }
21 |
22 | info(...args: string[]): void {
23 | hilog.info(this.domain, this.prefix, this.format, args);
24 | }
25 |
26 | warn(...args: string[]): void {
27 | hilog.warn(this.domain, this.prefix, this.format, args);
28 | }
29 |
30 | error(...args: string[]): void {
31 | hilog.error(this.domain, this.prefix, this.format, args);
32 | }
33 | }
34 |
35 | export default new Logger('geolocation', 0xFF00, false)
--------------------------------------------------------------------------------
/Example/harmony_use_pushy/harmony/build-profile.json5:
--------------------------------------------------------------------------------
1 | {
2 | app: {
3 | signingConfigs: [],
4 | products: [
5 | {
6 | name: 'default',
7 | signingConfig: 'default',
8 | compatibleSdkVersion: '5.0.0(12)',
9 | runtimeOS: 'HarmonyOS',
10 | buildOption: {
11 | strictMode: {
12 | caseSensitiveCheck: true,
13 | useNormalizedOHMUrl: true
14 | },
15 | "nativeCompiler": "BiSheng"
16 | },
17 | },
18 | ],
19 | buildModeSet: [
20 | {
21 | name: 'debug',
22 | },
23 | {
24 | name: 'release',
25 | },
26 | ],
27 | },
28 | modules: [
29 | {
30 | name: 'entry',
31 | srcPath: './entry',
32 | targets: [
33 | {
34 | name: 'default',
35 | applyToProducts: ['default'],
36 | },
37 | ],
38 | },
39 | {
40 | name: 'pushy',
41 | srcPath: '../node_modules/react-native-update/harmony/pushy',
42 | },
43 | ],
44 | }
45 |
--------------------------------------------------------------------------------
/android/src/main/java/expo/modules/pushy/ExpoPushyPackage.java:
--------------------------------------------------------------------------------
1 | package expo.modules.pushy;
2 |
3 | import android.content.Context;
4 | import android.util.Log;
5 | import androidx.annotation.Nullable;
6 | import java.util.ArrayList;
7 | import java.util.HashMap;
8 | import java.util.List;
9 | import java.util.Map;
10 | import cn.reactnative.modules.update.UpdateContext;
11 | import expo.modules.core.interfaces.Package;
12 | import expo.modules.core.interfaces.ReactNativeHostHandler;
13 |
14 | public class ExpoPushyPackage implements Package {
15 | @Override
16 | public List createReactNativeHostHandlers(Context context) {
17 | List handlers = new ArrayList<>();
18 | handlers.add(new ReactNativeHostHandler() {
19 | @Nullable
20 | @Override
21 | public String getJSBundleFile(boolean useDeveloperSupport) {
22 | return UpdateContext.getBundleUrl(context);
23 | }
24 | });
25 | return handlers;
26 | }
27 | }
--------------------------------------------------------------------------------
/Example/testHotUpdate/android/app/src/androidTest/java/com/awesomeproject/DetoxTest.java:
--------------------------------------------------------------------------------
1 | package com.awesomeproject;
2 |
3 | import com.wix.detox.Detox;
4 | import com.wix.detox.config.DetoxConfig;
5 |
6 | import org.junit.Rule;
7 | import org.junit.Test;
8 | import org.junit.runner.RunWith;
9 |
10 | import androidx.test.ext.junit.runners.AndroidJUnit4;
11 | import androidx.test.filters.LargeTest;
12 | import androidx.test.rule.ActivityTestRule;
13 |
14 | @RunWith(AndroidJUnit4.class)
15 | @LargeTest
16 | public class DetoxTest {
17 | @Rule
18 | public ActivityTestRule mActivityRule = new ActivityTestRule<>(MainActivity.class, false, false);
19 |
20 | @Test
21 | public void runDetoxTests() {
22 | DetoxConfig detoxConfig = new DetoxConfig();
23 | detoxConfig.idlePolicyConfig.masterTimeoutSec = 90;
24 | detoxConfig.idlePolicyConfig.idleResourceTimeoutSec = 60;
25 | detoxConfig.rnContextLoadTimeoutSec = (BuildConfig.DEBUG ? 180 : 60);
26 |
27 | Detox.runTests(mActivityRule, detoxConfig);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Example/testHotUpdate/ios/AwesomeProject/AppDelegate.mm:
--------------------------------------------------------------------------------
1 | #import "AppDelegate.h"
2 | #import "RCTPushy.h"
3 |
4 | #import
5 | #import
6 |
7 | @implementation AppDelegate
8 |
9 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
10 | {
11 | self.moduleName = @"AwesomeProject";
12 | self.dependencyProvider = [RCTAppDependencyProvider new];
13 | // You can add your custom initial props in the dictionary below.
14 | // They will be passed down to the ViewController used by React Native.
15 | self.initialProps = @{};
16 |
17 | return [super application:application didFinishLaunchingWithOptions:launchOptions];
18 | }
19 |
20 | - (NSURL *)bundleURL
21 | {
22 | #if DEBUG
23 | // 原先DEBUG这里的写法不作修改(所以DEBUG模式下不可热更新)
24 | return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"];
25 | #else
26 | return [RCTPushy bundleURL]; // <-- 把这里非DEBUG的情况替换为热更新bundle
27 | #endif
28 | }
29 |
30 | @end
31 |
--------------------------------------------------------------------------------
/ios/RCTPushy/RCTPushyManager.h:
--------------------------------------------------------------------------------
1 | #import
2 |
3 | @interface RCTPushyManager : NSObject
4 |
5 |
6 | - (BOOL)createDir:(NSString *)dir;
7 |
8 | - (void)unzipFileAtPath:(NSString *)path
9 | toDestination:(NSString *)destination
10 | progressHandler:(void (^)(NSString *entry, long entryNumber, long total))progressHandler
11 | completionHandler:(void (^)(NSString *path, BOOL succeeded, NSError *error))completionHandler;
12 |
13 | - (void)hdiffFileAtPath:(NSString *)path
14 | fromOrigin:(NSString *)origin
15 | toDestination:(NSString *)destination
16 | completionHandler:(void (^)(BOOL success))completionHandler;
17 |
18 | - (void)copyFiles:(NSDictionary *)filesDic
19 | fromDir:(NSString *)fromDir
20 | toDir:(NSString *)toDir
21 | deletes:(NSDictionary *)deletes
22 | completionHandler:(void (^)(NSError *error))completionHandler;
23 |
24 | - (void)removeFile:(NSString *)filePath
25 | completionHandler:(void (^)(NSError *error))completionHandler;
26 |
27 | @end
28 |
--------------------------------------------------------------------------------
/Example/testHotUpdate/android/app/src/main/java/com/awesomeproject/MainApplication.kt:
--------------------------------------------------------------------------------
1 | package com.awesomeproject
2 |
3 | import android.app.Application
4 | import com.facebook.react.PackageList
5 | import com.facebook.react.ReactApplication
6 | import com.facebook.react.ReactHost
7 | import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative
8 | import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
9 | import cn.reactnative.modules.update.UpdateContext
10 |
11 | class MainApplication : Application(), ReactApplication {
12 |
13 | override val reactHost: ReactHost by lazy {
14 | getDefaultReactHost(
15 | context = applicationContext,
16 | packageList =
17 | PackageList(this).packages.apply {
18 | // Packages that cannot be auto-linked yet can be added manually here, for example:
19 | // add(MyReactNativePackage())
20 | },
21 | jsBundleFilePath = UpdateContext.getBundleUrl(this),
22 | )
23 | }
24 |
25 | override fun onCreate() {
26 | super.onCreate()
27 | loadReactNative(this)
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/harmony/pushy/src/main/ets/EventHub.ts:
--------------------------------------------------------------------------------
1 | type EventCallback = (data: any) => void;
2 |
3 | export class EventHub {
4 | private static instance: EventHub;
5 | private listeners: Map>;
6 | private rnInstance: any;
7 |
8 | private constructor() {
9 | this.listeners = new Map();
10 | }
11 |
12 | public static getInstance(): EventHub {
13 | if (!EventHub.instance) {
14 | EventHub.instance = new EventHub();
15 | }
16 | return EventHub.instance;
17 | }
18 |
19 | public on(event: string, callback: EventCallback): void {
20 | if (!this.listeners.has(event)) {
21 | this.listeners.set(event, new Set());
22 | }
23 | this.listeners.get(event)?.add(callback);
24 | }
25 |
26 | public off(event: string, callback: EventCallback): void {
27 | this.listeners.get(event)?.delete(callback);
28 | }
29 |
30 | public emit(event: string, data: any): void {
31 | if (this.rnInstance) {
32 | this.rnInstance.emitDeviceEvent(event, data);
33 | }
34 | }
35 |
36 | setRNInstance(instance: any) {
37 | this.rnInstance = instance;
38 | }
39 | }
--------------------------------------------------------------------------------
/Example/harmony_use_pushy/android/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
12 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/Example/harmony_use_pushy/harmony/entry/obfuscation-rules.txt:
--------------------------------------------------------------------------------
1 | # Define project specific obfuscation rules here.
2 | # You can include the obfuscation configuration files in the current module's build-profile.json5.
3 | #
4 | # For more details, see
5 | # https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/source-obfuscation-V5
6 |
7 | # Obfuscation options:
8 | # -disable-obfuscation: disable all obfuscations
9 | # -enable-property-obfuscation: obfuscate the property names
10 | # -enable-toplevel-obfuscation: obfuscate the names in the global scope
11 | # -compact: remove unnecessary blank spaces and all line feeds
12 | # -remove-log: remove all console.* statements
13 | # -print-namecache: print the name cache that contains the mapping from the old names to new names
14 | # -apply-namecache: reuse the given cache file
15 |
16 | # Keep options:
17 | # -keep-property-name: specifies property names that you want to keep
18 | # -keep-global-name: specifies names that you want to keep in the global scope
19 |
20 | -enable-property-obfuscation
21 | -enable-toplevel-obfuscation
22 | -enable-filename-obfuscation
23 | -enable-export-obfuscation
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .vscode
2 | android/build
3 | android/obj
4 | *.iml
5 |
6 | # OSX
7 | #
8 | .DS_Store
9 |
10 | # Xcode
11 | #
12 | build/
13 | *.pbxuser
14 | !default.pbxuser
15 | *.mode1v3
16 | !default.mode1v3
17 | *.mode2v3
18 | !default.mode2v3
19 | *.perspectivev3
20 | !default.perspectivev3
21 | xcuserdata
22 | *.xccheckout
23 | *.moved-aside
24 | DerivedData
25 | *.hmap
26 | *.ipa
27 | *.xcuserstate
28 | project.xcworkspace
29 |
30 | # Android/IJ
31 | #
32 | .idea
33 | .gradle
34 | local.properties
35 | **/.project
36 | **/.settings
37 | **/.classpath
38 |
39 | # node.js
40 | #
41 | node_modules/
42 | npm-debug.log
43 |
44 | Example/**/.update
45 | Example/**/.pushy
46 | Example/testHotUpdate/artifacts
47 |
48 | yarn-error.log
49 | Example/testHotUpdate/.yarn
50 | android/bin
51 | Example/testHotUpdate/harmony
52 | Example/testHotUpdate/android/app/.cxx
53 | Example/harmony_use_pushy/libs
54 | **/mcp.json
55 |
56 |
57 | harmony/package
58 | **/oh_modules
59 | harmony/pushy/.preview
60 | Example/harmony_use_pushy/harmony/entry/src/main/resources/rawfile/meta.json
61 | **/.hvigor
62 | Example/harmony_use_pushy/harmony/entry/src/main/cpp/generated
63 |
--------------------------------------------------------------------------------
/Example/testHotUpdate/ios/AwesomeProject/Images.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "scale" : "2x",
6 | "size" : "20x20"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "scale" : "3x",
11 | "size" : "20x20"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "scale" : "2x",
16 | "size" : "29x29"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "scale" : "3x",
21 | "size" : "29x29"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "scale" : "2x",
26 | "size" : "40x40"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "scale" : "3x",
31 | "size" : "40x40"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "scale" : "2x",
36 | "size" : "60x60"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "scale" : "3x",
41 | "size" : "60x60"
42 | },
43 | {
44 | "idiom" : "ios-marketing",
45 | "scale" : "1x",
46 | "size" : "1024x1024"
47 | }
48 | ],
49 | "info" : {
50 | "author" : "xcode",
51 | "version" : 1
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/Example/harmony_use_pushy/ios/harmony_use_pushy/Images.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "scale" : "2x",
6 | "size" : "20x20"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "scale" : "3x",
11 | "size" : "20x20"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "scale" : "2x",
16 | "size" : "29x29"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "scale" : "3x",
21 | "size" : "29x29"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "scale" : "2x",
26 | "size" : "40x40"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "scale" : "3x",
31 | "size" : "40x40"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "scale" : "2x",
36 | "size" : "60x60"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "scale" : "3x",
41 | "size" : "60x60"
42 | },
43 | {
44 | "idiom" : "ios-marketing",
45 | "scale" : "1x",
46 | "size" : "1024x1024"
47 | }
48 | ],
49 | "info" : {
50 | "author" : "xcode",
51 | "version" : 1
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/Example/testHotUpdate/ios/AwesomeProject/PrivacyInfo.xcprivacy:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | NSPrivacyAccessedAPITypes
6 |
7 |
8 | NSPrivacyAccessedAPIType
9 | NSPrivacyAccessedAPICategoryFileTimestamp
10 | NSPrivacyAccessedAPITypeReasons
11 |
12 | C617.1
13 |
14 |
15 |
16 | NSPrivacyAccessedAPIType
17 | NSPrivacyAccessedAPICategoryUserDefaults
18 | NSPrivacyAccessedAPITypeReasons
19 |
20 | CA92.1
21 |
22 |
23 |
24 | NSPrivacyAccessedAPIType
25 | NSPrivacyAccessedAPICategorySystemBootTime
26 | NSPrivacyAccessedAPITypeReasons
27 |
28 | 35F9.1
29 |
30 |
31 |
32 | NSPrivacyCollectedDataTypes
33 |
34 | NSPrivacyTracking
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/Example/testHotUpdate/ios/Podfile:
--------------------------------------------------------------------------------
1 | # Resolve react_native_pods.rb with node to allow for hoisting
2 | require Pod::Executable.execute_command('node', ['-p',
3 | 'require.resolve(
4 | "react-native/scripts/react_native_pods.rb",
5 | {paths: [process.argv[1]]},
6 | )', __dir__]).strip
7 |
8 | platform :ios, min_ios_version_supported
9 | prepare_react_native_project!
10 |
11 |
12 | linkage = ENV['USE_FRAMEWORKS']
13 | if linkage != nil
14 | Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green
15 | use_frameworks! :linkage => linkage.to_sym
16 | end
17 |
18 | target 'AwesomeProject' do
19 | config = use_native_modules!
20 |
21 | use_react_native!(
22 | :path => config[:reactNativePath],
23 | # An absolute path to your application root.
24 | :app_path => "#{Pod::Config.instance.installation_root}/.."
25 | )
26 |
27 | post_install do |installer|
28 | # https://github.com/facebook/react-native/blob/main/packages/react-native/scripts/react_native_pods.rb#L197-L202
29 | react_native_post_install(
30 | installer,
31 | config[:reactNativePath],
32 | :mac_catalyst_enabled => false,
33 | # :ccache_enabled => true
34 | )
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Update Plugin for React Native
2 |
3 | Powered by ReactNative.cn
4 |
5 | Copyright (c) Wuhan Charmlot Network Technology Co., Ltd.
6 |
7 | All rights reserved.
8 |
9 | MIT License
10 |
11 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
16 |
--------------------------------------------------------------------------------
/Example/harmony_use_pushy/harmony/entry/oh-package-lock.json5:
--------------------------------------------------------------------------------
1 | {
2 | "meta": {
3 | "stableOrder": true,
4 | "enableUnifiedLockfile": false
5 | },
6 | "lockfileVersion": 3,
7 | "ATTENTION": "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.",
8 | "specifiers": {
9 | "@rnoh/react-native-openharmony@0.72.96": "@rnoh/react-native-openharmony@0.72.96",
10 | "pushy@../../node_modules/react-native-update/harmony/pushy": "pushy@../../node_modules/react-native-update/harmony/pushy"
11 | },
12 | "packages": {
13 | "@rnoh/react-native-openharmony@0.72.96": {
14 | "name": "",
15 | "version": "0.72.96",
16 | "integrity": "sha512-gBbm8LLyqi5UE7qHWdZYeQnjyncfEpCczKZUP/9M2U1Z7exR0Kya8PMKMwr1ta5ujy7w/hZVC2LomEV4QvBeqA==",
17 | "resolved": "https://ohpm.openharmony.cn/ohpm/@rnoh/react-native-openharmony/-/react-native-openharmony-0.72.96.har",
18 | "registryType": "ohpm"
19 | },
20 | "pushy@../../node_modules/react-native-update/harmony/pushy": {
21 | "name": "pushy",
22 | "version": "3.1.0-0.0.7",
23 | "resolved": "",
24 | "registryType": "local",
25 | "dependencies": {
26 | "@rnoh/react-native-openharmony": "^0.72.38"
27 | }
28 | }
29 | }
30 | }
--------------------------------------------------------------------------------
/Example/testHotUpdate/.gitignore:
--------------------------------------------------------------------------------
1 | # OSX
2 | #
3 | .DS_Store
4 |
5 | # Xcode
6 | #
7 | build/
8 | *.pbxuser
9 | !default.pbxuser
10 | *.mode1v3
11 | !default.mode1v3
12 | *.mode2v3
13 | !default.mode2v3
14 | *.perspectivev3
15 | !default.perspectivev3
16 | xcuserdata
17 | *.xccheckout
18 | *.moved-aside
19 | DerivedData
20 | *.hmap
21 | *.ipa
22 | *.xcuserstate
23 | ios/.xcode.env.local
24 |
25 | # Android/IntelliJ
26 | #
27 | build/
28 | .idea
29 | .gradle
30 | local.properties
31 | *.iml
32 | *.hprof
33 | .kotlin/
34 |
35 | # node.js
36 | #
37 | node_modules/
38 | npm-debug.log
39 | yarn-error.log
40 |
41 | # BUCK
42 | buck-out/
43 | \.buckd/
44 | *.keystore
45 | !debug.keystore
46 |
47 | # fastlane
48 | #
49 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
50 | # screenshots whenever they are needed.
51 | # For more information about the recommended setup visit:
52 | # https://docs.fastlane.tools/best-practices/source-control/
53 |
54 | **/fastlane/report.xml
55 | **/fastlane/Preview.html
56 | **/fastlane/screenshots
57 | **/fastlane/test_output
58 |
59 | # Bundle artifact
60 | *.jsbundle
61 |
62 | # Ruby / CocoaPods
63 | /ios/Pods/
64 | /vendor/bundle/
65 |
66 | # react-native-update
67 | .update
68 | .pushy
--------------------------------------------------------------------------------
/harmony/pushy/src/main/cpp/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | cmake_minimum_required(VERSION 3.13)
2 | project(rnupdate)
3 |
4 | # Point to android/jni directory for shared source code
5 | set(ANDROID_JNI_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../../node_modules/react-native-update/android/jni)
6 | set(HDIFFPATCH_DIR ${ANDROID_JNI_DIR}/HDiffPatch)
7 | set(LZMA_DIR ${ANDROID_JNI_DIR}/lzma)
8 | set(HDP_SOURCES
9 | ${CMAKE_CURRENT_SOURCE_DIR}/pushy.c
10 | ${ANDROID_JNI_DIR}/hpatch.c
11 | ${HDIFFPATCH_DIR}/libHDiffPatch/HPatch/patch.c
12 | ${HDIFFPATCH_DIR}/file_for_patch.c
13 | ${LZMA_DIR}/C/LzmaDec.c
14 | ${LZMA_DIR}/C/Lzma2Dec.c
15 | )
16 | set(CMAKE_VERBOSE_MAKEFILE on)
17 |
18 |
19 | add_library(rnupdate SHARED
20 | ${HDP_SOURCES}
21 | )
22 |
23 | target_include_directories(rnupdate PRIVATE
24 | ${CMAKE_CURRENT_SOURCE_DIR}
25 | ${ANDROID_JNI_DIR}
26 | ${HDIFFPATCH_DIR}
27 | ${HDIFFPATCH_DIR}/libHDiffPatch/HPatch
28 | ${LZMA_DIR}/C
29 | )
30 |
31 | target_link_libraries(rnupdate PUBLIC
32 | libace_napi.z.so
33 | )
34 |
35 | file(GLOB rnoh_pushy_SRC CONFIGURE_DEPENDS *.cpp)
36 | add_library(rnoh_pushy SHARED ${rnoh_pushy_SRC})
37 | target_include_directories(rnoh_pushy PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
38 | target_link_libraries(rnoh_pushy PUBLIC rnoh)
39 |
40 |
--------------------------------------------------------------------------------
/Example/harmony_use_pushy/android/app/src/main/java/com/harmony_use_pushy/MainActivity.java:
--------------------------------------------------------------------------------
1 | package com.harmony_use_pushy;
2 |
3 | import com.facebook.react.ReactActivity;
4 | import com.facebook.react.ReactActivityDelegate;
5 | import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint;
6 | import com.facebook.react.defaults.DefaultReactActivityDelegate;
7 |
8 | public class MainActivity extends ReactActivity {
9 |
10 | /**
11 | * Returns the name of the main component registered from JavaScript. This is used to schedule
12 | * rendering of the component.
13 | */
14 | @Override
15 | protected String getMainComponentName() {
16 | return "harmony_use_pushy";
17 | }
18 |
19 | /**
20 | * Returns the instance of the {@link ReactActivityDelegate}. Here we use a util class {@link
21 | * DefaultReactActivityDelegate} which allows you to easily enable Fabric and Concurrent React
22 | * (aka React 18) with two boolean flags.
23 | */
24 | @Override
25 | protected ReactActivityDelegate createReactActivityDelegate() {
26 | return new DefaultReactActivityDelegate(
27 | this,
28 | getMainComponentName(),
29 | // If you opted-in for the New Architecture, we enable the Fabric Renderer.
30 | DefaultNewArchitectureEntryPoint.getFabricEnabled());
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/.github/workflows/lint.yml:
--------------------------------------------------------------------------------
1 | name: lint
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - master
7 | push:
8 | branches:
9 | - master
10 |
11 | # Cancel a currently running workflow from the same PR/branch/tag
12 | # when a new workflow is triggered
13 | concurrency:
14 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
15 | cancel-in-progress: true
16 |
17 | jobs:
18 | lint:
19 | runs-on: ubuntu-latest
20 |
21 | strategy:
22 | matrix:
23 | node-version: [20.x]
24 |
25 | steps:
26 | - uses: actions/checkout@v4
27 | - uses: oven-sh/setup-bun@v2
28 | - uses: actions/setup-node@v4
29 | with:
30 | node-version: '20.x'
31 | registry-url: 'https://registry.npmjs.org'
32 |
33 | - name: Get yarn cache directory path
34 | id: yarn-cache-dir-path
35 | run: echo "dir=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT
36 |
37 | - name: Install Dependency
38 | env:
39 | NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
40 | NODE_OPTIONS: '--max_old_space_size=4096'
41 | run: bun install --frozen-lockfile
42 |
43 | - name: Run lint
44 | env:
45 | NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
46 | NODE_OPTIONS: '--max_old_space_size=4096'
47 | run: bun lint
48 |
--------------------------------------------------------------------------------
/harmony/pushy/hvigor-plugin.ts:
--------------------------------------------------------------------------------
1 | import fs from 'fs';
2 | import path from 'path';
3 |
4 | export function reactNativeUpdatePlugin() {
5 | return {
6 | pluginId: 'reactNativeUpdatePlugin',
7 | apply(node) {
8 | node.registerTask({
9 | name: 'reactNativeUpdatePlugin',
10 | run: () => {
11 | const cwd = process.cwd();
12 | const metaFilePath = path.resolve(
13 | cwd,
14 | 'entry/src/main/resources/rawfile/meta.json',
15 | );
16 | fs.mkdirSync(path.dirname(metaFilePath), { recursive: true });
17 |
18 | const moduleJsonPath = path.resolve(cwd, 'AppScope/app.json5');
19 | let versionName = '';
20 | if (fs.existsSync(moduleJsonPath)) {
21 | const content = fs.readFileSync(moduleJsonPath, 'utf-8');
22 | const match = content.match(
23 | /(?:"versionName"|versionName):\s*["']([^"']+)["']/,
24 | );
25 | versionName = match?.[1] || '';
26 | }
27 |
28 | const metaContent = {
29 | pushy_build_time: new Date().toISOString(),
30 | versionName,
31 | };
32 |
33 | fs.writeFileSync(metaFilePath, JSON.stringify(metaContent, null, 2));
34 | console.log(`Build time written to ${metaFilePath}`);
35 | },
36 | });
37 | },
38 | };
39 | }
40 |
--------------------------------------------------------------------------------
/Example/harmony_use_pushy/harmony/entry/src/main/cpp/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | cmake_minimum_required(VERSION 3.16)
2 | project(rnapp)
3 | set(RNOH_APP_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
4 | set(NODE_MODULES "${CMAKE_CURRENT_SOURCE_DIR}/../../../../../node_modules")
5 | set(OH_MODULE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../../oh_modules")
6 | set(RNOH_CPP_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../../oh_modules/@rnoh/react-native-openharmony/src/main/cpp")
7 | set(OH_MODULES "${CMAKE_CURRENT_SOURCE_DIR}/../../../oh_modules")
8 | set(LOG_VERBOSITY_LEVEL 1)
9 | set(CMAKE_ASM_FLAGS "-Wno-error=unused-command-line-argument -Qunused-arguments")
10 | set(CMAKE_CXX_FLAGS "-fstack-protector-strong -Wl,-z,relro,-z,now,-z,noexecstack -s -fPIE -pie")
11 | set(OH_MODULES "${CMAKE_CURRENT_SOURCE_DIR}/../../../oh_modules")
12 | set(WITH_HITRACE_SYSTRACE 1) # for other CMakeLists.txt files to use
13 | add_compile_definitions(WITH_HITRACE_SYSTRACE)
14 |
15 | add_subdirectory("${OH_MODULES}/pushy/src/main/cpp" ./pushy)
16 | add_subdirectory("${RNOH_CPP_DIR}" ./rn)
17 |
18 | file(GLOB GENERATED_CPP_FILES "${CMAKE_CURRENT_SOURCE_DIR}/generated/*.cpp") # this line is needed by codegen v1
19 |
20 | add_library(rnoh_app SHARED
21 | ${GENERATED_CPP_FILES}
22 | "./PackageProvider.cpp"
23 | "${RNOH_CPP_DIR}/RNOHAppNapiBridge.cpp"
24 | )
25 | target_link_libraries(rnoh_app PUBLIC rnoh)
26 | target_link_libraries(rnoh_app PUBLIC rnoh_pushy)
--------------------------------------------------------------------------------
/src/NativePushy.ts:
--------------------------------------------------------------------------------
1 | import { TurboModule, TurboModuleRegistry } from 'react-native';
2 |
3 | export interface Spec extends TurboModule {
4 | getConstants: () => {
5 | downloadRootDir: string;
6 | packageVersion: string;
7 | currentVersion: string;
8 | isFirstTime: boolean;
9 | rolledBackVersion: string;
10 | buildTime: string;
11 | uuid: string;
12 | isUsingBundleUrl: boolean;
13 | currentVersionInfo: string;
14 | };
15 | setLocalHashInfo(hash: string, info: string): Promise;
16 | getLocalHashInfo(hash: string): Promise;
17 | setUuid(uuid: string): Promise;
18 | reloadUpdate(options: { hash: string }): Promise;
19 | restartApp(): Promise;
20 | setNeedUpdate(options: { hash: string }): Promise;
21 | markSuccess(): Promise;
22 | downloadPatchFromPpk(options: {
23 | updateUrl: string;
24 | hash: string;
25 | originHash: string;
26 | }): Promise;
27 | downloadPatchFromPackage(options: {
28 | updateUrl: string;
29 | hash: string;
30 | }): Promise;
31 | downloadFullUpdate(options: {
32 | updateUrl: string;
33 | hash: string;
34 | }): Promise;
35 | downloadAndInstallApk(options: {
36 | url: string;
37 | target: string;
38 | hash: string;
39 | }): Promise;
40 | addListener(eventName: string): void;
41 | removeListeners(count: number): void;
42 | }
43 |
44 | export default TurboModuleRegistry.get('Pushy') as Spec | null;
45 |
--------------------------------------------------------------------------------
/Example/harmony_use_pushy/harmony/hvigor/hvigor-config.json5:
--------------------------------------------------------------------------------
1 | {
2 | "modelVersion": "5.0.0",
3 | "dependencies": {
4 | pushy: 'file:../../node_modules/react-native-update/harmony/pushy'
5 | },
6 | "execution": {
7 | // "analyze": "normal", /* Define the build analyze mode. Value: [ "normal" | "advanced" | false ]. Default: "normal" */
8 | // "daemon": true, /* Enable daemon compilation. Value: [ true | false ]. Default: true */
9 | // "incremental": true, /* Enable incremental compilation. Value: [ true | false ]. Default: true */
10 | // "parallel": true, /* Enable parallel compilation. Value: [ true | false ]. Default: true */
11 | // "typeCheck": false, /* Enable typeCheck. Value: [ true | false ]. Default: false */
12 | },
13 | "logging": {
14 | // "level": "info" /* Define the log level. Value: [ "debug" | "info" | "warn" | "error" ]. Default: "info" */
15 | },
16 | "debugging": {
17 | // "stacktrace": false /* Disable stacktrace compilation. Value: [ true | false ]. Default: false */
18 | },
19 | "nodeOptions": {
20 | // "maxOldSpaceSize": 8192 /* Enable nodeOptions maxOldSpaceSize compilation. Unit M. Used for the daemon process. Default: 8192*/
21 | // "exposeGC": true /* Enable to trigger garbage collection explicitly. Default: true*/
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/harmony/pushy/src/main/ets/PushyFileJSBundleProvider.ets:
--------------------------------------------------------------------------------
1 | import {
2 | FileJSBundle,
3 | HotReloadConfig,
4 | JSBundleProvider,
5 | JSBundleProviderError
6 | } from '@rnoh/react-native-openharmony';
7 | import common from '@ohos.app.ability.common';
8 | import fs from '@ohos.file.fs';
9 | import { UpdateContext } from './UpdateContext';
10 |
11 | export class PushyFileJSBundleProvider extends JSBundleProvider {
12 | private updateContext: UpdateContext;
13 | private path: string = ''
14 |
15 | constructor(context: common.UIAbilityContext) {
16 | super();
17 | this.updateContext = new UpdateContext(context);
18 | this.path = this.updateContext.getBundleUrl();
19 | }
20 |
21 | getURL(): string {
22 | return this.path;
23 | }
24 |
25 | async getBundle(): Promise {
26 | if (!this.path) {
27 | throw new JSBundleProviderError({
28 | whatHappened: 'No pushy bundle found. using default bundle',
29 | howCanItBeFixed: ['']
30 | })
31 | }
32 | try {
33 | await fs.access(this.path, fs.OpenMode.READ_ONLY);
34 | return {
35 | filePath: this.path
36 | }
37 | } catch (error) {
38 | throw new JSBundleProviderError({
39 | whatHappened: `Couldn't load JSBundle from ${this.path}`,
40 | extraData: error,
41 | howCanItBeFixed: [`Check if a bundle exists at "${this.path}" on your device.`]
42 | })
43 | }
44 | }
45 |
46 | getAppKeys(): string[] {
47 | return [];
48 | }
49 | }
--------------------------------------------------------------------------------
/.github/workflows/e2e_android.yml:
--------------------------------------------------------------------------------
1 | name: e2e-android
2 | on: push
3 |
4 | jobs:
5 | e2e-android:
6 | runs-on: macos-latest
7 | steps:
8 | - name: Checkout repository
9 | uses: actions/checkout@v3
10 |
11 | - name: Setup Node.js
12 | uses: actions/setup-node@v3
13 | with:
14 | cache: yarn
15 | node-version-file: .nvmrc
16 |
17 | - name: Install Yarn dependencies
18 | run: yarn --frozen-lockfile --prefer-offline
19 |
20 | - name: Setup Java
21 | uses: actions/setup-java@v3
22 | with:
23 | cache: gradle
24 | distribution: temurin
25 | java-version: 17
26 |
27 | - name: Cache Detox build
28 | id: cache-detox-build
29 | uses: actions/cache@v3
30 | with:
31 | path: android/app/build
32 | key: ${{ runner.os }}-detox-build
33 | restore-keys: |
34 | ${{ runner.os }}-detox-build
35 |
36 | - name: Detox build
37 | run: yarn build:android-debug
38 |
39 | - name: Get device name
40 | id: device
41 | run: node -e "console.log('AVD_NAME=' + require('./Example/testHotUpdate/.detoxrc').devices.emulator.device.avdName)" >> $GITHUB_OUTPUT
42 |
43 | - name: Detox test
44 | uses: reactivecircus/android-emulator-runner@v2
45 | with:
46 | api-level: 31
47 | arch: x86_64
48 | avd-name: ${{ steps.device.outputs.AVD_NAME }}
49 | script: yarn test:android-debug
50 |
--------------------------------------------------------------------------------
/Example/testHotUpdate/README.md:
--------------------------------------------------------------------------------
1 | # react-native-android-detox
2 |
3 | [](https://github.com/remarkablemark/react-native-android-detox/actions/workflows/e2e-android.yml)
4 |
5 | React Native Android Detox. The project has already been patched with the [additional Android configuration](https://wix.github.io/Detox/docs/introduction/project-setup/).
6 |
7 | ## Prerequisites
8 |
9 | Follow the [environment setup](https://wix.github.io/Detox/docs/introduction/getting-started).
10 |
11 | ## Install
12 |
13 | Clone the repository:
14 |
15 | ```sh
16 | git clone https://github.com/remarkablemark/react-native-android-detox.git
17 | cd react-native-android-detox
18 | ```
19 |
20 | Install the dependencies:
21 |
22 | ```sh
23 | yarn
24 | ```
25 |
26 | ## Build
27 |
28 | ### Android (Debug)
29 |
30 | Build the Android debug app:
31 |
32 | ```sh
33 | yarn detox build --configuration android.emu.debug
34 | ```
35 |
36 | ### Android (Release)
37 |
38 | Build the Android release app:
39 |
40 | ```sh
41 | yarn detox build --configuration android.emu.release
42 | ```
43 |
44 | ## Test
45 |
46 | ### Android (Debug)
47 |
48 | Start the app:
49 |
50 | ```sh
51 | yarn start
52 | ```
53 |
54 | Run the test:
55 |
56 | ```sh
57 | yarn detox test --configuration android.emu.debug
58 | ```
59 |
60 | ### Android (Release)
61 |
62 | Run the test:
63 |
64 | ```sh
65 | yarn detox test --configuration android.emu.release
66 | ```
67 |
--------------------------------------------------------------------------------
/Example/testHotUpdate/android/app/_BUCK:
--------------------------------------------------------------------------------
1 | # To learn about Buck see [Docs](https://buckbuild.com/).
2 | # To run your application with Buck:
3 | # - install Buck
4 | # - `npm start` - to start the packager
5 | # - `cd android`
6 | # - `keytool -genkey -v -keystore keystores/debug.keystore -storepass android -alias androiddebugkey -keypass android -dname "CN=Android Debug,O=Android,C=US"`
7 | # - `./gradlew :app:copyDownloadableDepsToLibs` - make all Gradle compile dependencies available to Buck
8 | # - `buck install -r android/app` - compile, install and run application
9 | #
10 |
11 | load(":build_defs.bzl", "create_aar_targets", "create_jar_targets")
12 |
13 | lib_deps = []
14 |
15 | create_aar_targets(glob(["libs/*.aar"]))
16 |
17 | create_jar_targets(glob(["libs/*.jar"]))
18 |
19 | android_library(
20 | name = "all-libs",
21 | exported_deps = lib_deps,
22 | )
23 |
24 | android_library(
25 | name = "app-code",
26 | srcs = glob([
27 | "src/main/java/**/*.java",
28 | ]),
29 | deps = [
30 | ":all-libs",
31 | ":build_config",
32 | ":res",
33 | ],
34 | )
35 |
36 | android_build_config(
37 | name = "build_config",
38 | package = "com.awesomeproject",
39 | )
40 |
41 | android_resource(
42 | name = "res",
43 | package = "com.awesomeproject",
44 | res = "src/main/res",
45 | )
46 |
47 | android_binary(
48 | name = "app",
49 | keystore = "//android/keystores:debug",
50 | manifest = "src/main/AndroidManifest.xml",
51 | package_type = "debug",
52 | deps = [
53 | ":app-code",
54 | ],
55 | )
56 |
--------------------------------------------------------------------------------
/.github/workflows/scripts/start-firebase-emulator.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | if ! [ -x "$(command -v firebase)" ]; then
3 | echo "❌ Firebase-tools CLI is missing. Run 'npm i -g firebase-tools' or the equivalent"
4 | exit 1
5 | fi
6 |
7 | EMU_START_COMMAND="firebase emulators:start --only auth,database,firestore,functions,storage --project react-native-firebase-testing"
8 | #EMU_START_COMMAND="sleep 120"
9 | MAX_RETRIES=3
10 | MAX_CHECKATTEMPTS=60
11 | CHECKATTEMPTS_WAIT=1
12 |
13 | # Make sure functions are ready to go
14 | pushd "$(dirname "$0")/functions" && yarn && yarn build && popd
15 |
16 |
17 | RETRIES=1
18 | while [ $RETRIES -le $MAX_RETRIES ]; do
19 |
20 | if [ "$1" == "--no-daemon" ]; then
21 | echo "Starting Firebase Emulator Suite in foreground."
22 | $EMU_START_COMMAND
23 | exit 0
24 | else
25 | echo "Starting Firebase Emulator Suite in background."
26 | $EMU_START_COMMAND &
27 | CHECKATTEMPTS=1
28 | while [ $CHECKATTEMPTS -le $MAX_CHECKATTEMPTS ]; do
29 | sleep $CHECKATTEMPTS_WAIT
30 | if curl --output /dev/null --silent --fail http://localhost:8080; then
31 | echo "Firebase Emulator Suite is online!"
32 | exit 0;
33 | fi
34 | echo "Waiting for Firebase Emulator Suite to come online, check $CHECKATTEMPTS of $MAX_CHECKATTEMPTS..."
35 | ((CHECKATTEMPTS = CHECKATTEMPTS + 1))
36 | done
37 | fi
38 |
39 | echo "Firebase Emulator Suite did not come online in $MAX_CHECKATTEMPTS checks. Try $RETRIES of $MAX_RETRIES."
40 | ((RETRIES = RETRIES + 1))
41 |
42 | done
43 | echo "Firebase Emulator Suite did not come online after $MAX_RETRIES attempts."
44 | exit 1
45 |
--------------------------------------------------------------------------------
/Example/testHotUpdate/android/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
15 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/android/jni/hpatch.h:
--------------------------------------------------------------------------------
1 | // hpatch.h
2 | // import HDiffPatch, support patchData created by "hdiffz -SD -c-lzma2 oldfile newfile patchfile"
3 | // Copyright 2021 housisong, All rights reserved
4 |
5 | #ifndef HDIFFPATCH_PATCH_H
6 | #define HDIFFPATCH_PATCH_H
7 | # include //for uint8_t
8 | #include "HDiffPatch/libHDiffPatch/HPatch/patch_types.h" //for hpatch_singleCompressedDiffInfo
9 | #ifdef __cplusplus
10 | extern "C" {
11 | #endif
12 |
13 | //result
14 | enum {
15 | kHPatch_ok = 0,
16 | kHPatch_error_malloc =-1,
17 | kHPatch_error_info =-2,
18 | kHPatch_error_compressType =-3,
19 | kHPatch_error_patch =-4,
20 | kHPatch_error_old_fopen =-5,
21 | kHPatch_error_old_fread =-6,
22 | kHPatch_error_old_fclose =-7,
23 | kHPatch_error_pat_fopen =-8,
24 | kHPatch_error_pat_fread =-9,
25 | kHPatch_error_pat_fclose =-10,
26 | kHPatch_error_new_fopen =-11,
27 | kHPatch_error_new_fwrite =-12,
28 | kHPatch_error_new_fclose =-13,
29 | kHPatch_error_old_size =-14,
30 | kHPatch_error_new_size =-15,
31 | };
32 |
33 | int hpatch_getInfo_by_mem(hpatch_singleCompressedDiffInfo* out_patinfo,
34 | const uint8_t* pat,size_t patsize);
35 |
36 | //patInfo can NULL
37 | int hpatch_by_mem(const uint8_t* old,size_t oldsize, uint8_t* newBuf,size_t newsize,
38 | const uint8_t* pat,size_t patsize,const hpatch_singleCompressedDiffInfo* patInfo);
39 | int hpatch_by_file(const char* oldfile, const char* newfile, const char* patchfile);
40 |
41 | #ifdef __cplusplus
42 | }
43 | #endif
44 | #endif //HDIFFPATCH_PATCH_H
45 |
--------------------------------------------------------------------------------
/harmony/pushy/src/main/cpp/PushyTurboModule.h:
--------------------------------------------------------------------------------
1 | /**
2 | * MIT License
3 | *
4 | * Copyright (C) 2023 Huawei Device Co., Ltd.
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 |
25 | #ifndef GEO_LOCATION_TURBOMODULE_H
26 | #define GEO_LOCATION_TURBOMODULE_H
27 |
28 | #include
29 | #include "RNOH/ArkTSTurboModule.h"
30 |
31 | namespace rnoh {
32 | class JSI_EXPORT PushyTurboModule : public ArkTSTurboModule {
33 | public:
34 | PushyTurboModule(const ArkTSTurboModule::Context ctx, const std::string name);
35 | };
36 | } // namespace rnoh
37 |
38 | #endif
--------------------------------------------------------------------------------
/Example/expoUsePushy/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "expousepushy",
3 | "main": "index.js",
4 | "version": "1.0.0",
5 | "scripts": {
6 | "start": "expo start",
7 | "reset-project": "node ./scripts/reset-project.js",
8 | "android": "expo run:android",
9 | "ios": "expo run:ios",
10 | "web": "expo start --web",
11 | "lint": "expo lint"
12 | },
13 | "dependencies": {
14 | "@expo/vector-icons": "^14.1.0",
15 | "@react-navigation/bottom-tabs": "^7.3.10",
16 | "@react-navigation/elements": "^2.3.8",
17 | "@react-navigation/native": "^7.1.6",
18 | "expo": "~53.0.22",
19 | "expo-blur": "~14.1.5",
20 | "expo-constants": "~17.1.7",
21 | "expo-font": "~13.3.2",
22 | "expo-haptics": "~14.1.4",
23 | "expo-image": "~2.4.0",
24 | "expo-linking": "~7.1.7",
25 | "expo-router": "~5.1.5",
26 | "expo-splash-screen": "~0.30.10",
27 | "expo-status-bar": "~2.2.3",
28 | "expo-symbols": "~0.4.5",
29 | "expo-system-ui": "~5.0.11",
30 | "expo-web-browser": "~14.2.0",
31 | "react": "19.0.0",
32 | "react-dom": "19.0.0",
33 | "react-native": "0.79.6",
34 | "react-native-gesture-handler": "~2.24.0",
35 | "react-native-reanimated": "~3.17.4",
36 | "react-native-safe-area-context": "5.4.0",
37 | "react-native-screens": "~4.11.1",
38 | "react-native-update": "^10.34.4",
39 | "react-native-web": "~0.20.0",
40 | "react-native-webview": "13.13.5"
41 | },
42 | "devDependencies": {
43 | "@babel/core": "^7.25.2",
44 | "@types/react": "~19.0.10",
45 | "typescript": "~5.8.3",
46 | "eslint": "^9.25.0",
47 | "eslint-config-expo": "~9.2.0"
48 | },
49 | "private": true
50 | }
51 |
--------------------------------------------------------------------------------
/Example/harmony_use_pushy/harmony/entry/src/test/LocalUnit.test.ets:
--------------------------------------------------------------------------------
1 | import { describe, beforeAll, beforeEach, afterEach, afterAll, it, expect } from '@ohos/hypium';
2 |
3 | export default function localUnitTest() {
4 | describe('localUnitTest', () => {
5 | // Defines a test suite. Two parameters are supported: test suite name and test suite function.
6 | beforeAll(() => {
7 | // Presets an action, which is performed only once before all test cases of the test suite start.
8 | // This API supports only one parameter: preset action function.
9 | });
10 | beforeEach(() => {
11 | // Presets an action, which is performed before each unit test case starts.
12 | // The number of execution times is the same as the number of test cases defined by **it**.
13 | // This API supports only one parameter: preset action function.
14 | });
15 | afterEach(() => {
16 | // Presets a clear action, which is performed after each unit test case ends.
17 | // The number of execution times is the same as the number of test cases defined by **it**.
18 | // This API supports only one parameter: clear action function.
19 | });
20 | afterAll(() => {
21 | // Presets a clear action, which is performed after all test cases of the test suite end.
22 | // This API supports only one parameter: clear action function.
23 | });
24 | it('assertContain', 0, () => {
25 | // Defines a test case. This API supports three parameters: test case name, filter parameter, and test case function.
26 | let a = 'abc';
27 | let b = 'b';
28 | // Defines a variety of assertion methods, which are used to declare expected boolean conditions.
29 | expect(a).assertContain(b);
30 | expect(a).assertEqual(a);
31 | });
32 | });
33 | }
--------------------------------------------------------------------------------
/.github/workflows/scripts/firestore.indexes.json:
--------------------------------------------------------------------------------
1 | {
2 | "indexes": [
3 | {
4 | "collectionGroup": "firestore",
5 | "queryScope": "COLLECTION",
6 | "fields": [
7 | {
8 | "fieldPath": "a",
9 | "order": "ASCENDING"
10 | },
11 | {
12 | "fieldPath": "b",
13 | "order": "ASCENDING"
14 | }
15 | ]
16 | }
17 | ],
18 | "fieldOverrides": [
19 | {
20 | "collectionGroup": "collectionGroup",
21 | "fieldPath": "value",
22 | "indexes": [
23 | {
24 | "order": "ASCENDING",
25 | "queryScope": "COLLECTION"
26 | },
27 | {
28 | "order": "DESCENDING",
29 | "queryScope": "COLLECTION"
30 | },
31 | {
32 | "arrayConfig": "CONTAINS",
33 | "queryScope": "COLLECTION"
34 | },
35 | {
36 | "order": "ASCENDING",
37 | "queryScope": "COLLECTION_GROUP"
38 | },
39 | {
40 | "order": "DESCENDING",
41 | "queryScope": "COLLECTION_GROUP"
42 | }
43 | ]
44 | },
45 | {
46 | "collectionGroup": "collectionGroup",
47 | "fieldPath": "number",
48 | "indexes": [
49 | {
50 | "order": "ASCENDING",
51 | "queryScope": "COLLECTION"
52 | },
53 | {
54 | "order": "DESCENDING",
55 | "queryScope": "COLLECTION"
56 | },
57 | {
58 | "arrayConfig": "CONTAINS",
59 | "queryScope": "COLLECTION"
60 | },
61 | {
62 | "order": "ASCENDING",
63 | "queryScope": "COLLECTION_GROUP"
64 | },
65 | {
66 | "order": "DESCENDING",
67 | "queryScope": "COLLECTION_GROUP"
68 | }
69 | ]
70 | }
71 | ]
72 | }
73 |
--------------------------------------------------------------------------------
/.github/workflows/scripts/functions/src/sample-data.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Testing tools for invertase/react-native-firebase use only.
3 | *
4 | * Copyright (C) 2018-present Invertase Limited
5 | *
6 | * See License file for more information.
7 | */
8 |
9 | const SAMPLE_DATA: { [key: string]: any } = {
10 | number: 1234,
11 | string: 'acde',
12 | boolean: true,
13 | null: null,
14 | object: {
15 | number: 1234,
16 | string: 'acde',
17 | boolean: true,
18 | null: null,
19 | },
20 | array: [1234, 'acde', true, null],
21 | deepObject: {
22 | array: [1234, 'acde', false, null],
23 | object: {
24 | number: 1234,
25 | string: 'acde',
26 | boolean: true,
27 | null: null,
28 | array: [1234, 'acde', true, null],
29 | },
30 | number: 1234,
31 | string: 'acde',
32 | boolean: true,
33 | null: null,
34 | },
35 | deepArray: [
36 | 1234,
37 | 'acde',
38 | true,
39 | null,
40 | [1234, 'acde', true, null],
41 | {
42 | number: 1234,
43 | string: 'acde',
44 | boolean: true,
45 | null: null,
46 | array: [1234, 'acde', true, null],
47 | },
48 | ],
49 | deepMap: {
50 | number: 123,
51 | string: 'foo',
52 | booleanTrue: true,
53 | booleanFalse: false,
54 | null: null,
55 | list: ['1', 2, true, false],
56 | map: {
57 | number: 123,
58 | string: 'foo',
59 | booleanTrue: true,
60 | booleanFalse: false,
61 | null: null,
62 | },
63 | },
64 | deepList: [
65 | '1',
66 | 2,
67 | true,
68 | false,
69 | ['1', 2, true, false],
70 | {
71 | number: 123,
72 | string: 'foo',
73 | booleanTrue: true,
74 | booleanFalse: false,
75 | null: null,
76 | },
77 | ],
78 | };
79 |
80 | export default SAMPLE_DATA;
81 |
--------------------------------------------------------------------------------
/Example/testHotUpdate/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "testHotUpdate",
3 | "version": "0.0.1",
4 | "private": true,
5 | "scripts": {
6 | "android": "react-native run-android",
7 | "ios": "react-native run-ios",
8 | "start": "react-native start",
9 | "test": "jest",
10 | "test:e2e": "detox test --configuration android.emu.debug",
11 | "lint": "eslint .",
12 | "postinstall": "patch-package",
13 | "apk": "cd android && ./gradlew assembleRelease"
14 | },
15 | "dependencies": {
16 | "form-data": "^4.0.5",
17 | "patch-package": "^8.0.1",
18 | "react": "19.1.1",
19 | "react-native": "0.82.1",
20 | "react-native-camera-kit": "^16.1.3",
21 | "react-native-paper": "^5.14.5",
22 | "react-native-safe-area-context": "^5.6.2",
23 | "react-native-svg": "^15.15.0",
24 | "react-native-update": "^10.36.2",
25 | "react-native-vector-icons": "^10.3.0"
26 | },
27 | "devDependencies": {
28 | "@babel/core": "^7.27.3",
29 | "@babel/preset-env": "^7.27.2",
30 | "@babel/runtime": "^7.27.3",
31 | "@react-native-community/cli": "20.0.0",
32 | "@react-native-community/cli-platform-android": "20.0.0",
33 | "@react-native-community/cli-platform-ios": "20.0.0",
34 | "@react-native/babel-preset": "0.82.1",
35 | "@react-native/eslint-config": "0.82.1",
36 | "@react-native/metro-config": "0.82.1",
37 | "@react-native/typescript-config": "0.82.1",
38 | "@types/react": "^19.1.13",
39 | "@types/react-test-renderer": "^19.1.0",
40 | "detox": "^20.41.2",
41 | "eslint": "^9.35.0",
42 | "jest": "^29.6.3",
43 | "prettier": "2.8.8",
44 | "react-test-renderer": "19.1.1",
45 | "typescript": "5.9.3"
46 | },
47 | "engines": {
48 | "node": ">=20"
49 | },
50 | "trustedDependencies": [
51 | "detox",
52 | "dtrace-provider"
53 | ]
54 | }
55 |
--------------------------------------------------------------------------------
/Example/harmony_use_pushy/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "harmony_use_pushy",
3 | "version": "0.0.1",
4 | "private": true,
5 | "scripts": {
6 | "android": "react-native run-android",
7 | "ios": "react-native run-ios",
8 | "lint": "eslint .",
9 | "start": "npm run codegen && hdc rport tcp:8081 tcp:8081 && react-native start",
10 | "codegen": "react-native codegen-harmony --rnoh-module-path ./harmony/react_native_openharmony",
11 | "build": "pushy bundle --platform harmony --no-interactive",
12 | "test": "jest",
13 | "hdiffFromPPK": "pushy hdiffFromPPK .pushy/output/harmony.1735052610653.ppk .pushy/output/harmony.1735052678646.ppk .pushy/output/hdiff.ppk-patch",
14 | "hdiffFromApp": "pushy hdiffFromApp .pushy/output/version-1.0.0.app .pushy/output/harmony.1735052610653.ppk .pushy/output/hdiff.app-patch",
15 | "hash": "pushy hash /Users/yanbo.he/Desktop/HarmonyOS/react-native-pushy/Example/harmony_use_pushy/.pushy/output/harmony.1735048297258.ppk"
16 | },
17 | "dependencies": {
18 | "@react-native-oh/react-native-harmony": "^0.72.59",
19 | "react": "18.2.0",
20 | "react-native": "0.72.5",
21 | "react-native-update": "^10.35.4"
22 | },
23 | "devDependencies": {
24 | "@babel/core": "^7.20.0",
25 | "@babel/preset-env": "^7.20.0",
26 | "@babel/runtime": "^7.20.0",
27 | "@react-native/eslint-config": "^0.72.2",
28 | "@react-native/metro-config": "^0.72.11",
29 | "@tsconfig/react-native": "^3.0.0",
30 | "@types/react": "^18.0.24",
31 | "@types/react-test-renderer": "^18.0.0",
32 | "babel-jest": "^29.2.1",
33 | "eslint": "^8.19.0",
34 | "jest": "^29.2.1",
35 | "metro-react-native-babel-preset": "0.76.8",
36 | "prettier": "^2.4.1",
37 | "react-test-renderer": "18.2.0",
38 | "typescript": "4.8.4"
39 | },
40 | "engines": {
41 | "node": ">=16"
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/Example/harmony_use_pushy/ios/harmony_use_pushy/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleDisplayName
8 | harmony_use_pushy
9 | CFBundleExecutable
10 | $(EXECUTABLE_NAME)
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | APPL
19 | CFBundleShortVersionString
20 | $(MARKETING_VERSION)
21 | CFBundleSignature
22 | ????
23 | CFBundleVersion
24 | $(CURRENT_PROJECT_VERSION)
25 | LSRequiresIPhoneOS
26 |
27 | NSAppTransportSecurity
28 |
29 | NSExceptionDomains
30 |
31 | localhost
32 |
33 | NSExceptionAllowsInsecureHTTPLoads
34 |
35 |
36 |
37 |
38 | NSLocationWhenInUseUsageDescription
39 |
40 | UILaunchStoryboardName
41 | LaunchScreen
42 | UIRequiredDeviceCapabilities
43 |
44 | armv7
45 |
46 | UISupportedInterfaceOrientations
47 |
48 | UIInterfaceOrientationPortrait
49 | UIInterfaceOrientationLandscapeLeft
50 | UIInterfaceOrientationLandscapeRight
51 |
52 | UIViewControllerBasedStatusBarAppearance
53 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/Example/harmony_use_pushy/harmony/entry/src/ohosTest/ets/test/Ability.test.ets:
--------------------------------------------------------------------------------
1 | import { hilog } from '@kit.PerformanceAnalysisKit';
2 | import { describe, beforeAll, beforeEach, afterEach, afterAll, it, expect } from '@ohos/hypium';
3 |
4 | export default function abilityTest() {
5 | describe('ActsAbilityTest', () => {
6 | // Defines a test suite. Two parameters are supported: test suite name and test suite function.
7 | beforeAll(() => {
8 | // Presets an action, which is performed only once before all test cases of the test suite start.
9 | // This API supports only one parameter: preset action function.
10 | })
11 | beforeEach(() => {
12 | // Presets an action, which is performed before each unit test case starts.
13 | // The number of execution times is the same as the number of test cases defined by **it**.
14 | // This API supports only one parameter: preset action function.
15 | })
16 | afterEach(() => {
17 | // Presets a clear action, which is performed after each unit test case ends.
18 | // The number of execution times is the same as the number of test cases defined by **it**.
19 | // This API supports only one parameter: clear action function.
20 | })
21 | afterAll(() => {
22 | // Presets a clear action, which is performed after all test cases of the test suite end.
23 | // This API supports only one parameter: clear action function.
24 | })
25 | it('assertContain', 0, () => {
26 | // Defines a test case. This API supports three parameters: test case name, filter parameter, and test case function.
27 | hilog.info(0x0000, 'testTag', '%{public}s', 'it begin');
28 | let a = 'abc';
29 | let b = 'b';
30 | // Defines a variety of assertion methods, which are used to declare expected boolean conditions.
31 | expect(a).assertContain(b);
32 | expect(a).assertEqual(a);
33 | })
34 | })
35 | }
--------------------------------------------------------------------------------
/.github/workflows/scripts/functions/src/testFunctionDefaultRegion.ts:
--------------------------------------------------------------------------------
1 | /*
2 | *
3 | * Testing tools for invertase/react-native-firebase use only.
4 | *
5 | * Copyright (C) 2018-present Invertase Limited
6 | *
7 | * See License file for more information.
8 | */
9 |
10 | import * as assert from 'assert';
11 | import { FirebaseError } from 'firebase-admin';
12 | import * as functions from 'firebase-functions';
13 | import SAMPLE_DATA from './sample-data';
14 |
15 | export const testFunctionDefaultRegion = functions.https.onCall(data => {
16 | console.log(Date.now(), data);
17 |
18 | if (typeof data === 'undefined') {
19 | return 'undefined';
20 | }
21 |
22 | if (typeof data === 'string') {
23 | return 'string';
24 | }
25 |
26 | if (typeof data === 'number') {
27 | return 'number';
28 | }
29 |
30 | if (typeof data === 'boolean') {
31 | return 'boolean';
32 | }
33 |
34 | if (data === null) {
35 | return 'null';
36 | }
37 |
38 | if (Array.isArray(data)) {
39 | return 'array';
40 | }
41 |
42 | const { type, asError, inputData } = data;
43 | if (!Object.hasOwnProperty.call(SAMPLE_DATA, type)) {
44 | throw new functions.https.HttpsError(
45 | 'invalid-argument',
46 | 'Invalid test requested.',
47 | );
48 | }
49 |
50 | const outputData = SAMPLE_DATA[type];
51 |
52 | try {
53 | assert.deepEqual(outputData, inputData);
54 | } catch (e: any) {
55 | console.error(e);
56 | throw new functions.https.HttpsError(
57 | 'invalid-argument',
58 | 'Input and Output types did not match.',
59 | (e as FirebaseError).message,
60 | );
61 | }
62 |
63 | // all good
64 | if (asError) {
65 | throw new functions.https.HttpsError(
66 | 'cancelled',
67 | 'Response data was requested to be sent as part of an Error payload, so here we are!',
68 | outputData,
69 | );
70 | }
71 |
72 | return outputData;
73 | });
74 |
--------------------------------------------------------------------------------
/ios/Expo/ExpoPushyReactDelegateHandler.swift:
--------------------------------------------------------------------------------
1 | import ExpoModulesCore
2 | import React
3 |
4 | public final class ExpoPushyReactDelegateHandler: ExpoReactDelegateHandler {
5 |
6 | #if EXPO_SUPPORTS_BUNDLEURL
7 | // This code block compiles only if EXPO_SUPPORTS_BUNDLEURL is defined
8 | // For expo-modules-core >= 1.12.0
9 |
10 | // Override bundleURL, which is the primary mechanism for these versions.
11 | // Expo's default createBridge implementation should respect this.
12 | override public func bundleURL(reactDelegate: ExpoReactDelegate) -> URL? {
13 | let bundleURL = RCTPushy.bundleURL()
14 | print("PushyHandler: Using bundleURL: \(bundleURL?.absoluteString ?? "nil")")
15 | return bundleURL
16 | }
17 |
18 | // No createBridge override needed here, rely on default behavior using the bundleURL override.
19 |
20 | #else
21 | // This code block compiles only if EXPO_SUPPORTS_BUNDLEURL is NOT defined
22 | // For expo-modules-core < 1.12.0
23 |
24 | // No bundleURL override possible here.
25 |
26 | // createBridge is the mechanism to customize the URL here.
27 | // We completely override it and do not call super.
28 | override public func createBridge(reactDelegate: ExpoReactDelegate, bridgeDelegate: RCTBridgeDelegate, launchOptions: [AnyHashable: Any]?) -> RCTBridge? {
29 | let bundleURL = RCTPushy.bundleURL()
30 | // Print the URL being provided to the initializer
31 | print("PushyHandler: createBridge bundleURL: \(bundleURL?.absoluteString ?? "nil")")
32 |
33 | // Directly create the bridge using the bundleURL initializer.
34 | // Pass nil for moduleProvider, assuming default behavior is sufficient.
35 | // WARNING: If bundleURL is nil, this initialization might fail silently or crash.
36 | return RCTBridge(bundleURL: bundleURL, moduleProvider: nil, launchOptions: launchOptions)
37 | }
38 |
39 | #endif // EXPO_SUPPORTS_BUNDLEURL
40 | }
41 |
--------------------------------------------------------------------------------
/Example/testHotUpdate/android/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | # Default value: -Xmx512m -XX:MaxMetaspaceSize=256m
13 | org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
14 |
15 | # When configured, Gradle will run in incubating parallel mode.
16 | # This option should only be used with decoupled projects. More details, visit
17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
18 | # org.gradle.parallel=true
19 |
20 | # AndroidX package structure to make it clearer which packages are bundled with the
21 | # Android operating system, and which are packaged with your app's APK
22 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
23 | android.useAndroidX=true
24 |
25 | # Use this property to specify which architecture you want to build.
26 | # You can also override it from the CLI using
27 | # ./gradlew -PreactNativeArchitectures=x86_64
28 | # reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
29 | reactNativeArchitectures=arm64-v8a
30 |
31 | # Use this property to enable support to the new architecture.
32 | # This will allow you to use TurboModules and the Fabric render in
33 | # your application. You should enable this flag either if you want
34 | # to write custom TurboModules/Fabric components OR use libraries that
35 | # are providing them.
36 | newArchEnabled=true
37 |
38 | # Use this property to enable or disable the Hermes JS engine.
39 | # If set to false, you will be using JSC instead.
40 | hermesEnabled=true
41 |
--------------------------------------------------------------------------------
/android/src/main/java/cn/reactnative/modules/update/UpdatePackage.java:
--------------------------------------------------------------------------------
1 | package cn.reactnative.modules.update;
2 |
3 | import androidx.annotation.Nullable;
4 | import com.facebook.react.TurboReactPackage;
5 | import com.facebook.react.bridge.NativeModule;
6 | import com.facebook.react.bridge.ReactApplicationContext;
7 | import com.facebook.react.module.model.ReactModuleInfo;
8 | import com.facebook.react.module.model.ReactModuleInfoProvider;
9 | import java.util.HashMap;
10 | import java.util.Map;
11 |
12 | public class UpdatePackage extends TurboReactPackage {
13 | @Nullable
14 | @Override
15 | public NativeModule getModule(String name, ReactApplicationContext reactContext) {
16 | if (name.equals(UpdateModuleImpl.NAME)) {
17 | return new UpdateModule(reactContext);
18 | } else {
19 | return null;
20 | }
21 | }
22 |
23 | @Override
24 | public ReactModuleInfoProvider getReactModuleInfoProvider() {
25 | return new ReactModuleInfoProvider() {
26 | @Override
27 | public Map getReactModuleInfos() {
28 | final Map moduleInfos = new HashMap<>();
29 | boolean isTurboModule = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED;
30 | moduleInfos.put(
31 | UpdateModuleImpl.NAME,
32 | new ReactModuleInfo(
33 | UpdateModuleImpl.NAME,
34 | UpdateModuleImpl.NAME,
35 | false, // canOverrideExistingModule
36 | false, // needsEagerInit
37 | true, // hasConstants
38 | false, // isCxxModule
39 | isTurboModule // isTurboModule
40 | ));
41 | return moduleInfos;
42 | }
43 | };
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/Example/harmony_use_pushy/harmony/entry/src/main/module.json5:
--------------------------------------------------------------------------------
1 | {
2 | "module": {
3 | "name": "entry",
4 | "type": "entry",
5 | "description": "$string:module_desc",
6 | "mainElement": "EntryAbility",
7 | "deviceTypes": [
8 | "phone",
9 | "tablet",
10 | "2in1"
11 | ],
12 | "deliveryWithInstall": true,
13 | "installationFree": false,
14 | "pages": "$profile:main_pages",
15 | "abilities": [
16 | {
17 | "name": "EntryAbility",
18 | "srcEntry": "./ets/entryability/EntryAbility.ets",
19 | "description": "$string:EntryAbility_desc",
20 | "icon": "$media:layered_image",
21 | "label": "$string:EntryAbility_label",
22 | "startWindowIcon": "$media:startIcon",
23 | "startWindowBackground": "$color:start_window_background",
24 | "exported": true,
25 | "skills": [
26 | {
27 | "entities": [
28 | "entity.system.home"
29 | ],
30 | "actions": [
31 | "action.system.home"
32 | ]
33 | }
34 | ]
35 | }
36 | ],
37 | "extensionAbilities": [
38 | {
39 | "name": "EntryBackupAbility",
40 | "srcEntry": "./ets/entrybackupability/EntryBackupAbility.ets",
41 | "type": "backup",
42 | "exported": false,
43 | "metadata": [
44 | {
45 | "name": "ohos.extension.backup",
46 | "resource": "$profile:backup_config"
47 | }
48 | ],
49 | }
50 | ],
51 | "requestPermissions": [
52 | {
53 | "name": "ohos.permission.WRITE_MEDIA",
54 | "reason": "$string:reason_write_media",
55 | "usedScene": {
56 | "when": "always",
57 | }
58 | },
59 | {
60 | "name": "ohos.permission.READ_MEDIA",
61 | "reason": "$string:reason_read_media",
62 | "usedScene": {
63 | "when": "always",
64 | }
65 | },
66 | {
67 | "name": "ohos.permission.INTERNET"
68 | }
69 | ]
70 | }
71 | }
--------------------------------------------------------------------------------
/Example/testHotUpdate/ios/AwesomeProject/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleDisplayName
8 | AwesomeProject
9 | CFBundleExecutable
10 | $(EXECUTABLE_NAME)
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | APPL
19 | CFBundleShortVersionString
20 | 1.0
21 | CFBundleSignature
22 | ????
23 | CFBundleVersion
24 | 1
25 | LSRequiresIPhoneOS
26 |
27 | NSAppTransportSecurity
28 |
29 | NSAllowsArbitraryLoads
30 |
31 | NSExceptionDomains
32 |
33 | localhost
34 |
35 | NSExceptionAllowsInsecureHTTPLoads
36 |
37 |
38 |
39 |
40 | NSCameraUsageDescription
41 | For taking photos
42 | NSLocationWhenInUseUsageDescription
43 |
44 | NSPhotoLibraryUsageDescription
45 | For saving photos
46 | RCTNewArchEnabled
47 |
48 | UILaunchStoryboardName
49 | LaunchScreen
50 | UIRequiredDeviceCapabilities
51 |
52 | armv7
53 |
54 | UISupportedInterfaceOrientations
55 |
56 | UIInterfaceOrientationPortrait
57 | UIInterfaceOrientationLandscapeLeft
58 | UIInterfaceOrientationLandscapeRight
59 |
60 | UIViewControllerBasedStatusBarAppearance
61 |
62 |
63 |
64 |
--------------------------------------------------------------------------------
/Example/testHotUpdate/android/app/src/main/res/drawable/rn_edit_text_material.xml:
--------------------------------------------------------------------------------
1 |
2 |
16 |
21 |
22 |
23 |
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/Example/harmony_use_pushy/android/app/src/main/res/drawable/rn_edit_text_material.xml:
--------------------------------------------------------------------------------
1 |
2 |
16 |
21 |
22 |
23 |
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/Example/harmony_use_pushy/android/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | # Default value: -Xmx512m -XX:MaxMetaspaceSize=256m
13 | org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
14 |
15 | # When configured, Gradle will run in incubating parallel mode.
16 | # This option should only be used with decoupled projects. More details, visit
17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
18 | # org.gradle.parallel=true
19 |
20 | # AndroidX package structure to make it clearer which packages are bundled with the
21 | # Android operating system, and which are packaged with your app's APK
22 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
23 | android.useAndroidX=true
24 | # Automatically convert third-party libraries to use AndroidX
25 | android.enableJetifier=true
26 |
27 | # Version of flipper SDK to use with React Native
28 | FLIPPER_VERSION=0.182.0
29 |
30 | # Use this property to specify which architecture you want to build.
31 | # You can also override it from the CLI using
32 | # ./gradlew -PreactNativeArchitectures=x86_64
33 | reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
34 |
35 | # Use this property to enable support to the new architecture.
36 | # This will allow you to use TurboModules and the Fabric render in
37 | # your application. You should enable this flag either if you want
38 | # to write custom TurboModules/Fabric components OR use libraries that
39 | # are providing them.
40 | newArchEnabled=false
41 |
42 | # Use this property to enable or disable the Hermes JS engine.
43 | # If set to false, you will be using JSC instead.
44 | hermesEnabled=true
45 |
--------------------------------------------------------------------------------
/src/context.ts:
--------------------------------------------------------------------------------
1 | import { createContext, useContext } from 'react';
2 | import { CheckResult, ProgressData } from './type';
3 | import { Pushy, Cresc } from './client';
4 | import i18n from './i18n';
5 |
6 | const noop = () => {};
7 | const asyncNoop = () => Promise.resolve();
8 |
9 | export const defaultContext = {
10 | checkUpdate: asyncNoop,
11 | switchVersion: asyncNoop,
12 | switchVersionLater: asyncNoop,
13 | markSuccess: noop,
14 | dismissError: noop,
15 | downloadUpdate: asyncNoop,
16 | downloadAndInstallApk: asyncNoop,
17 | restartApp: asyncNoop,
18 | getCurrentVersionInfo: () => Promise.resolve({}),
19 | parseTestQrCode: () => false,
20 | currentHash: '',
21 | packageVersion: '',
22 | currentVersionInfo: {},
23 | };
24 |
25 | export const UpdateContext = createContext<{
26 | checkUpdate: () => Promise;
27 | switchVersion: () => Promise;
28 | switchVersionLater: () => Promise;
29 | markSuccess: () => void;
30 | dismissError: () => void;
31 | downloadUpdate: () => Promise;
32 | downloadAndInstallApk: (url: string) => Promise;
33 | // @deprecated use currentVersionInfo instead
34 | getCurrentVersionInfo: () => Promise<{
35 | name?: string;
36 | description?: string;
37 | metaInfo?: string;
38 | }>;
39 | currentVersionInfo: {
40 | name?: string;
41 | description?: string;
42 | metaInfo?: string;
43 | } | null;
44 | parseTestQrCode: (code: string) => boolean;
45 | restartApp: () => Promise;
46 | currentHash: string;
47 | packageVersion: string;
48 | client?: Pushy | Cresc;
49 | progress?: ProgressData;
50 | updateInfo?: CheckResult;
51 | lastError?: Error;
52 | }>(defaultContext);
53 |
54 | export const useUpdate = __DEV__ ? () => {
55 | const context = useContext(UpdateContext);
56 |
57 | // 检查是否在 UpdateProvider 内部使用
58 | if (!context.client) {
59 | throw new Error(i18n.t('error_use_update_outside_provider'));
60 | }
61 |
62 | return context;
63 | } : () => useContext(UpdateContext);
64 |
65 | /** @deprecated Please use `useUpdate` instead */
66 | export const usePushy = useUpdate;
67 |
--------------------------------------------------------------------------------
/harmony/pushy/src/main/cpp/PushyPackage.h:
--------------------------------------------------------------------------------
1 | /**
2 | * MIT License
3 | *
4 | * Copyright (C) 2023 Huawei Device Co., Ltd.
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 |
25 | #ifndef GEO_LOCATION_PACKAGE_H
26 | #define GEO_LOCATION_PACKAGE_H
27 |
28 | #include "RNOH/Package.h"
29 | #include "PushyTurboModule.h"
30 |
31 | using namespace rnoh;
32 | using namespace facebook;
33 |
34 | class PushyTurboModuleFactoryDelegate : public TurboModuleFactoryDelegate {
35 | public:
36 | SharedTurboModule createTurboModule(Context ctx, const std::string &name) const override
37 | {
38 | if (name == "Pushy") {
39 | return std::make_shared(ctx, name);
40 | }
41 | return nullptr;
42 | };
43 | };
44 |
45 | namespace rnoh {
46 | class PushyPackage : public Package {
47 | public:
48 | PushyPackage(Package::Context ctx) : Package(ctx){}
49 | std::unique_ptr createTurboModuleFactoryDelegate() override
50 | {
51 | return std::make_unique();
52 | }
53 | };
54 | } // namespace rnoh
55 | #endif
56 |
--------------------------------------------------------------------------------
/src/isInRollout.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-fallthrough */
2 |
3 | import { cInfo } from './core';
4 |
5 | /* eslint-disable no-bitwise */
6 | function murmurhash3_32_gc(key: string, seed = 0) {
7 | let remainder, bytes, h1, h1b, c1, c2, k1, i;
8 |
9 | remainder = key.length & 3; // key.length % 4
10 | bytes = key.length - remainder;
11 | h1 = seed;
12 | c1 = 0xcc9e2d51;
13 | c2 = 0x1b873593;
14 | i = 0;
15 |
16 | while (i < bytes) {
17 | k1 =
18 | (key.charCodeAt(i) & 0xff) |
19 | ((key.charCodeAt(++i) & 0xff) << 8) |
20 | ((key.charCodeAt(++i) & 0xff) << 16) |
21 | ((key.charCodeAt(++i) & 0xff) << 24);
22 | ++i;
23 |
24 | ((k1 & 0xffff) * c1 + ((((k1 >>> 16) * c1) & 0xffff) << 16)) & 0xffffffff;
25 | k1 = (k1 << 15) | (k1 >>> 17);
26 | k1 =
27 | ((k1 & 0xffff) * c2 + ((((k1 >>> 16) * c2) & 0xffff) << 16)) & 0xffffffff;
28 |
29 | h1 ^= k1;
30 | h1 = (h1 << 13) | (h1 >>> 19);
31 | h1b =
32 | ((h1 & 0xffff) * 5 + ((((h1 >>> 16) * 5) & 0xffff) << 16)) & 0xffffffff;
33 | h1 = (h1b & 0xffff) + 0x6b64 + ((((h1b >>> 16) + 0xe654) & 0xffff) << 16);
34 | }
35 |
36 | k1 = 0;
37 |
38 | switch (remainder) {
39 | case 3:
40 | k1 ^= (key.charCodeAt(i + 2) & 0xff) << 16;
41 | case 2:
42 | k1 ^= (key.charCodeAt(i + 1) & 0xff) << 8;
43 | case 1:
44 | k1 ^= key.charCodeAt(i) & 0xff;
45 |
46 | k1 =
47 | ((k1 & 0xffff) * c1 + ((((k1 >>> 16) * c1) & 0xffff) << 16)) &
48 | 0xffffffff;
49 | k1 = (k1 << 15) | (k1 >>> 17);
50 | k1 =
51 | ((k1 & 0xffff) * c2 + ((((k1 >>> 16) * c2) & 0xffff) << 16)) &
52 | 0xffffffff;
53 | h1 ^= k1;
54 | }
55 |
56 | h1 ^= key.length;
57 |
58 | h1 ^= h1 >>> 16;
59 | h1 =
60 | ((h1 & 0xffff) * 0x85ebca6b +
61 | ((((h1 >>> 16) * 0x85ebca6b) & 0xffff) << 16)) &
62 | 0xffffffff;
63 | h1 ^= h1 >>> 13;
64 | h1 =
65 | ((h1 & 0xffff) * 0xc2b2ae35 +
66 | ((((h1 >>> 16) * 0xc2b2ae35) & 0xffff) << 16)) &
67 | 0xffffffff;
68 | h1 ^= h1 >>> 16;
69 |
70 | return h1 >>> 0;
71 | }
72 |
73 | const intForUUID = murmurhash3_32_gc(cInfo.uuid);
74 |
75 | export function isInRollout(rollout: number) {
76 | return intForUUID % 100 < rollout;
77 | }
78 |
--------------------------------------------------------------------------------
/Example/harmony_use_pushy/android/app/src/main/java/com/harmony_use_pushy/MainApplication.java:
--------------------------------------------------------------------------------
1 | package com.harmony_use_pushy;
2 |
3 | import android.app.Application;
4 | import com.facebook.react.PackageList;
5 | import com.facebook.react.ReactApplication;
6 | import com.facebook.react.ReactNativeHost;
7 | import com.facebook.react.ReactPackage;
8 | import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint;
9 | import com.facebook.react.defaults.DefaultReactNativeHost;
10 | import com.facebook.soloader.SoLoader;
11 | import java.util.List;
12 |
13 | public class MainApplication extends Application implements ReactApplication {
14 |
15 | private final ReactNativeHost mReactNativeHost =
16 | new DefaultReactNativeHost(this) {
17 | @Override
18 | public boolean getUseDeveloperSupport() {
19 | return BuildConfig.DEBUG;
20 | }
21 |
22 | @Override
23 | protected List getPackages() {
24 | @SuppressWarnings("UnnecessaryLocalVariable")
25 | List packages = new PackageList(this).getPackages();
26 | // Packages that cannot be autolinked yet can be added manually here, for example:
27 | // packages.add(new MyReactNativePackage());
28 | return packages;
29 | }
30 |
31 | @Override
32 | protected String getJSMainModuleName() {
33 | return "index";
34 | }
35 |
36 | @Override
37 | protected boolean isNewArchEnabled() {
38 | return BuildConfig.IS_NEW_ARCHITECTURE_ENABLED;
39 | }
40 |
41 | @Override
42 | protected Boolean isHermesEnabled() {
43 | return BuildConfig.IS_HERMES_ENABLED;
44 | }
45 | };
46 |
47 | @Override
48 | public ReactNativeHost getReactNativeHost() {
49 | return mReactNativeHost;
50 | }
51 |
52 | @Override
53 | public void onCreate() {
54 | super.onCreate();
55 | SoLoader.init(this, /* native exopackage */ false);
56 | if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
57 | // If you opted-in for the New Architecture, we load the native entry point for this app.
58 | DefaultNewArchitectureEntryPoint.load();
59 | }
60 | ReactNativeFlipper.initializeFlipper(this, getReactNativeHost().getReactInstanceManager());
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/Example/harmony_use_pushy/ios/harmony_use_pushyTests/harmony_use_pushyTests.m:
--------------------------------------------------------------------------------
1 | #import
2 | #import
3 |
4 | #import
5 | #import
6 |
7 | #define TIMEOUT_SECONDS 600
8 | #define TEXT_TO_LOOK_FOR @"Welcome to React"
9 |
10 | @interface harmony_use_pushyTests : XCTestCase
11 |
12 | @end
13 |
14 | @implementation harmony_use_pushyTests
15 |
16 | - (BOOL)findSubviewInView:(UIView *)view matching:(BOOL (^)(UIView *view))test
17 | {
18 | if (test(view)) {
19 | return YES;
20 | }
21 | for (UIView *subview in [view subviews]) {
22 | if ([self findSubviewInView:subview matching:test]) {
23 | return YES;
24 | }
25 | }
26 | return NO;
27 | }
28 |
29 | - (void)testRendersWelcomeScreen
30 | {
31 | UIViewController *vc = [[[RCTSharedApplication() delegate] window] rootViewController];
32 | NSDate *date = [NSDate dateWithTimeIntervalSinceNow:TIMEOUT_SECONDS];
33 | BOOL foundElement = NO;
34 |
35 | __block NSString *redboxError = nil;
36 | #ifdef DEBUG
37 | RCTSetLogFunction(
38 | ^(RCTLogLevel level, RCTLogSource source, NSString *fileName, NSNumber *lineNumber, NSString *message) {
39 | if (level >= RCTLogLevelError) {
40 | redboxError = message;
41 | }
42 | });
43 | #endif
44 |
45 | while ([date timeIntervalSinceNow] > 0 && !foundElement && !redboxError) {
46 | [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
47 | [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
48 |
49 | foundElement = [self findSubviewInView:vc.view
50 | matching:^BOOL(UIView *view) {
51 | if ([view.accessibilityLabel isEqualToString:TEXT_TO_LOOK_FOR]) {
52 | return YES;
53 | }
54 | return NO;
55 | }];
56 | }
57 |
58 | #ifdef DEBUG
59 | RCTSetLogFunction(RCTDefaultLogFunction);
60 | #endif
61 |
62 | XCTAssertNil(redboxError, @"RedBox error: %@", redboxError);
63 | XCTAssertTrue(foundElement, @"Couldn't find element with text '%@' in %d seconds", TEXT_TO_LOOK_FOR, TIMEOUT_SECONDS);
64 | }
65 |
66 | @end
67 |
--------------------------------------------------------------------------------
/harmony/pushy/OAT.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | LICENSE
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/Example/harmony_use_pushy/ios/Podfile:
--------------------------------------------------------------------------------
1 | # Resolve react_native_pods.rb with node to allow for hoisting
2 | require Pod::Executable.execute_command('node', ['-p',
3 | 'require.resolve(
4 | "react-native/scripts/react_native_pods.rb",
5 | {paths: [process.argv[1]]},
6 | )', __dir__]).strip
7 |
8 | platform :ios, min_ios_version_supported
9 | prepare_react_native_project!
10 |
11 | # If you are using a `react-native-flipper` your iOS build will fail when `NO_FLIPPER=1` is set.
12 | # because `react-native-flipper` depends on (FlipperKit,...) that will be excluded
13 | #
14 | # To fix this you can also exclude `react-native-flipper` using a `react-native.config.js`
15 | # ```js
16 | # module.exports = {
17 | # dependencies: {
18 | # ...(process.env.NO_FLIPPER ? { 'react-native-flipper': { platforms: { ios: null } } } : {}),
19 | # ```
20 | flipper_config = ENV['NO_FLIPPER'] == "1" ? FlipperConfiguration.disabled : FlipperConfiguration.enabled
21 |
22 | linkage = ENV['USE_FRAMEWORKS']
23 | if linkage != nil
24 | Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green
25 | use_frameworks! :linkage => linkage.to_sym
26 | end
27 |
28 | target 'harmony_use_pushy' do
29 | config = use_native_modules!
30 |
31 | # Flags change depending on the env values.
32 | flags = get_default_flags()
33 |
34 | use_react_native!(
35 | :path => config[:reactNativePath],
36 | # Hermes is now enabled by default. Disable by setting this flag to false.
37 | :hermes_enabled => flags[:hermes_enabled],
38 | :fabric_enabled => flags[:fabric_enabled],
39 | # Enables Flipper.
40 | #
41 | # Note that if you have use_frameworks! enabled, Flipper will not work and
42 | # you should disable the next line.
43 | :flipper_configuration => flipper_config,
44 | # An absolute path to your application root.
45 | :app_path => "#{Pod::Config.instance.installation_root}/.."
46 | )
47 |
48 | target 'harmony_use_pushyTests' do
49 | inherit! :complete
50 | # Pods for testing
51 | end
52 |
53 | post_install do |installer|
54 | # https://github.com/facebook/react-native/blob/main/packages/react-native/scripts/react_native_pods.rb#L197-L202
55 | react_native_post_install(
56 | installer,
57 | config[:reactNativePath],
58 | :mac_catalyst_enabled => false
59 | )
60 | __apply_Xcode_12_5_M1_post_install_workaround(installer)
61 | end
62 | end
63 |
--------------------------------------------------------------------------------
/android/jni/DownloadTask.c:
--------------------------------------------------------------------------------
1 | //
2 | // Created by DengYun on 3/31/16.
3 | //
4 |
5 | #include "cn_reactnative_modules_update_DownloadTask.h"
6 |
7 | #include "hpatch.h"
8 | #define _check(v,errInfo) do{ if (!(v)) { _isError=hpatch_TRUE; _errInfo=errInfo; goto _clear; } }while(0)
9 |
10 | JNIEXPORT jbyteArray JNICALL Java_cn_reactnative_modules_update_DownloadTask_hdiffPatch
11 | (JNIEnv *env, jobject self, jbyteArray origin, jbyteArray patch){
12 | hpatch_BOOL _isError=hpatch_FALSE;
13 | const char* _errInfo="";
14 |
15 | jbyte* originPtr = (*env)->GetByteArrayElements(env, origin, NULL);
16 | size_t originLength = (*env)->GetArrayLength(env, origin);
17 | jbyte* patchPtr = (*env)->GetByteArrayElements(env, patch, NULL);
18 | size_t patchLength = (*env)->GetArrayLength(env, patch);
19 | jbyteArray ret = NULL;
20 | jbyte* outPtr = NULL;
21 | size_t newsize = 0;
22 | hpatch_singleCompressedDiffInfo patInfo;
23 |
24 | _check(((originLength==0)||originPtr) && patchPtr && (patchLength>0),"Corrupt patch");
25 | _check(kHPatch_ok==hpatch_getInfo_by_mem(&patInfo,(const uint8_t*)patchPtr,patchLength),"Error info in hpatch");
26 | _check(originLength==patInfo.oldDataSize,"Error oldDataSize in hpatch");
27 | newsize=(size_t)patInfo.newDataSize;
28 | if (sizeof(size_t)!=sizeof(hpatch_StreamPos_t))
29 | _check(newsize==patInfo.newDataSize,"Error newDataSize in hpatch");
30 |
31 | ret = (*env)->NewByteArray(env,newsize);
32 | _check(ret,"Error JNIEnv::NewByteArray()");
33 | if (newsize>0) {
34 | outPtr = (*env)->GetByteArrayElements(env, ret, NULL);
35 | _check(outPtr,"Corrupt JNIEnv::GetByteArrayElements");
36 | }
37 |
38 | _check(kHPatch_ok==hpatch_by_mem((const uint8_t*)originPtr,originLength,(uint8_t*)outPtr,newsize,
39 | (const uint8_t*)patchPtr,patchLength,&patInfo),"hpacth");
40 |
41 | _clear:
42 | if (outPtr) (*env)->ReleaseByteArrayElements(env, ret, outPtr, (_isError?JNI_ABORT:0));
43 | if (originPtr) (*env)->ReleaseByteArrayElements(env, origin, originPtr, JNI_ABORT);
44 | if (patchPtr) (*env)->ReleaseByteArrayElements(env, patch, patchPtr, JNI_ABORT);
45 | if (_isError){
46 | jclass newExcCls = NULL;
47 | if (ret){
48 | (*env)->DeleteLocalRef(env, ret);
49 | ret = NULL;
50 | }
51 | newExcCls = (*env)->FindClass(env, "java/lang/Error");
52 | if (newExcCls != NULL) // Unable to find the new exception class, give up.
53 | (*env)->ThrowNew(env, newExcCls, _errInfo);
54 | }
55 | return ret;
56 | }
57 |
--------------------------------------------------------------------------------
/src/locales/zh.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | // Common messages
3 | checking_update: '正在检查更新...',
4 | downloading_update: '正在下载更新包...',
5 | installing_update: '正在安装更新...',
6 | update_available: '发现新版本',
7 | update_downloaded: '更新包下载完成',
8 | update_installed: '更新安装完成',
9 | no_update_available: '已是最新版本',
10 | update_failed: '更新失败',
11 | network_error: '网络连接错误',
12 | download_failed: '下载失败',
13 | install_failed: '安装失败',
14 |
15 | // Progress messages with interpolation
16 | download_progress: '下载进度: {{progress}}%',
17 | download_speed: '下载速度: {{speed}}/s',
18 | file_size: '文件大小: {{size}}',
19 | time_remaining: '剩余时间: {{time}}',
20 |
21 | // Error messages
22 | error_code: '错误代码: {{code}}',
23 | error_message: '错误信息: {{message}}',
24 | retry_count: '重试次数: {{count}}/{{max}}',
25 |
26 | // Update info
27 | version_info: '版本 {{version}} ({{build}})',
28 | release_notes: '更新说明: {{notes}}',
29 | update_size: '更新包大小: {{size}}MB',
30 |
31 | // Alert messages
32 | alert_title: '提示',
33 | alert_update_ready: '下载完毕,是否立即更新?',
34 | alert_next_time: '下次再说',
35 | alert_update_now: '立即更新',
36 | alert_app_updated: '您的应用版本已更新,点击更新下载安装新版本',
37 | alert_update_button: '更新',
38 | alert_cancel: '取消',
39 | alert_confirm: '确定',
40 | alert_info: '信息',
41 | alert_no_update_wait: '未发现更新,请等待10秒让服务器生成补丁包',
42 |
43 | // Error messages
44 | error_appkey_required: '需要提供 appKey',
45 | error_update_check_failed: '更新检查失败',
46 | error_cannot_connect_server: '无法连接到更新服务器。请检查网络连接。',
47 | error_cannot_connect_backup:
48 | '无法连接到更新服务器: {{message}}。正在尝试备用端点。',
49 | error_diff_failed: 'diff 错误: {{message}}',
50 | error_pdiff_failed: 'pdiff 错误: {{message}}',
51 | error_full_patch_failed: '完整补丁错误: {{message}}',
52 | error_all_promises_rejected: '所有请求都被拒绝',
53 | error_ping_failed: 'Ping 失败',
54 | error_ping_timeout: 'Ping 超时',
55 | error_http_status: '{{status}} {{statusText}}',
56 |
57 | // Development messages
58 | dev_debug_disabled:
59 | '您当前处于开发环境且未启用调试模式。{{matter}} 将不会执行。如需在开发环境中调试 {{matter}},请在客户端中将 debug 设为 true。',
60 | dev_log_prefix: 'react-native-update: ',
61 | dev_web_not_supported:
62 | 'react-native-update 不支持 Web 平台,不会执行任何操作',
63 |
64 | // More alert messages
65 | alert_new_version_found:
66 | '检查到新的版本{{name}},是否下载?\n{{description}}',
67 |
68 | // Development environment messages
69 | dev_incremental_update_disabled:
70 | '当前是开发环境,无法执行增量式热更新,重启不会生效。如果需要在开发环境中测试可生效的全量热更新(但也会在再次重启后重新连接 metro),请打开"忽略时间戳"开关再重试。',
71 |
72 | // Context error messages
73 | error_use_update_outside_provider:
74 | 'useUpdate 必须在 UpdateProvider 内部使用。请使用 包裹您的组件树。',
75 | };
76 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-native-update [](http://badge.fury.io/js/react-native-update)
2 |
3 | 本组件是面向 React Native 提供热更新功能的组件,详情请访问我们的官方网站 。
4 |
5 | **现已支持鸿蒙以及新架构**
6 |
7 | ### 快速开始
8 |
9 | 请查看[文档](https://pushy.reactnative.cn/docs/getting-started.html)
10 |
11 | ### 优势
12 |
13 | 1. 对中国用户使用阿里云高速 CDN 分发,对比其他服务器在国外的热更新服务,分发更稳定,更新成功率极高。对国外用户则智能分流至 cloudflare,同样享受高速更新服务。
14 | 2. 基于 bsdiff/hdiff 算法创建的**超小更新包**,通常版本迭代后在几十 KB 级别(其他全量热更新服务所需流量通常在几十 MB 级别)。
15 | 3. 始终跟进 RN 最新正式版本,第一时间提供支持。支持 hermes 字节码格式。支持新架构(注:安卓 0.73.0 ~ 0.76.0 的新架构因官方 bug 不支持,0.73 以下或 0.76.1 以上的新架构可用)。
16 | 4. 跨越多个版本进行更新时,只需要下载**一个更新包**,不需要逐版本依次更新。
17 | 5. 命令行工具 & 网页双端管理,版本发布过程简单便捷,完全可以集成 CI。
18 | 6. 支持崩溃回滚,安全可靠。
19 | 7. meta 信息及开放 API,提供更高扩展性。
20 | 8. 提供付费的专人技术支持。
21 |
22 | ### 与其他热更新库对比
23 |
24 | | 对比维度 | react-native-update | expo-update | react-native-code-push |
25 | |---------|---------------------|-------------|------------------------|
26 | | **价格/成本** | 提供免费额度,多级梯度付费(最低约 66 元/月),流量不单独计费 | 提供免费额度,多级梯度付费(最低约 136 元/月),超出流量额外计费 | ❌ **已停运**(Microsoft App Center 已于 2025 年 3 月 31 日停止服务) |
27 | | **更新包大小** | ⭐⭐⭐⭐⭐ 几十 KB(增量更新) | ⭐⭐⭐ 全量更新(通常几十 MB) | ❌ **已停运** |
28 | | **中国地区访问速度** | ⭐⭐⭐⭐⭐ 阿里云 CDN,速度极快 | ⭐⭐ 国外服务器,可能较慢 | ❌ **已停运** |
29 | | **iOS 支持** | ✅ 支持 | ✅ 支持 | ❌ **已停运** |
30 | | **Android 支持** | ✅ 支持 | ✅ 支持 | ❌ **已停运** |
31 | | **鸿蒙支持** | ✅ 支持 | ❌ 不支持 | ❌ **已停运** |
32 | | **Expo 支持** | ✅ 支持 | ✅ 支持 | ❌ **已停运** |
33 | | **RN 版本支持** | ⭐⭐⭐⭐⭐ 第一时间支持最新版本 | ⭐⭐⭐⭐ 跟随 Expo SDK | ❌ **已停运** |
34 | | **新架构支持** | ✅ 支持 | ✅ 支持 | ❌ **已停运** |
35 | | **Hermes 支持** | ✅ 支持 | ✅ 支持 | ❌ **已停运** |
36 | | **崩溃回滚** | ✅ 自动回滚机制 | ✅ 支持 | ❌ **已停运** |
37 | | **管理界面** | ✅ 命令行工具 + Web 管理界面 | ✅ Expo Dashboard | ❌ **已停运** |
38 | | **CI/CD 集成** | ✅ 支持 | ✅ 支持 | ❌ **已停运** |
39 | | **API 扩展性** | ✅ Meta 信息 + 开放 API | ⚠️ 有限 | ❌ **已停运** |
40 | | **中文文档/支持** | ⭐⭐⭐⭐⭐ 完整中文文档,中文社区支持 | ⭐⭐ 英文为主 | ❌ **已停运** |
41 | | **技术支持** | ✅ 付费专人技术支持 | ⚠️ 社区支持 | ❌ **已停运** |
42 | | **服务器部署** | ✅ 可托管也可付费私有部署 | ✅ Expo 托管(EAS Update) | ❌ **已停运** |
43 | | **更新策略** | 灵活配置(静默/提示/立即/延迟) | 相对固定 | ❌ **已停运** |
44 | | **流量消耗** | ⭐⭐⭐⭐⭐ 极低(增量更新) | ⭐⭐⭐ 较高(全量更新) | ❌ **已停运** |
45 | | **更新成功率** | ⭐⭐⭐⭐⭐ 极高(国内 CDN 优势) | ⭐⭐⭐ 中等 | ❌ **已停运** |
46 |
47 |
48 |
49 | ### 本地开发
50 |
51 | ```
52 | $ git clone git@github.com:reactnativecn/react-native-update.git
53 | $ cd react-native-pushy/Example/testHotUpdate
54 | $ yarn
55 | $ yarn start
56 | ```
57 |
58 | 本地库文件使用 yarn link 链接,因此可直接在源文件中修改,在 testHotUpdate 项目中调试。
59 |
60 | ### 关于我们
61 |
62 | 本组件由[React Native 中文网](https://reactnative.cn/)独家发布,如有定制需求可以[联系我们](https://reactnative.cn/about.html#content)。
63 |
64 | 关于此插件发现任何问题,可以前往[Issues](https://github.com/reactnativecn/react-native-update/issues)发帖提问。
65 |
--------------------------------------------------------------------------------
/src/type.ts:
--------------------------------------------------------------------------------
1 | export interface VersionInfo {
2 | name: string;
3 | hash: string;
4 | description: string;
5 | metaInfo: string;
6 | config: {
7 | rollout: {
8 | [packageVersion: string]: number;
9 | };
10 | [key: string]: any;
11 | };
12 | pdiff?: string;
13 | diff?: string;
14 | full?: string;
15 | }
16 |
17 | interface RootResult {
18 | upToDate?: true;
19 | expired?: true;
20 | downloadUrl?: string;
21 | update?: true;
22 | paused?: 'app' | 'package';
23 | message?: string;
24 | paths?: string[];
25 | }
26 |
27 | export type CheckResult = RootResult &
28 | Partial & {
29 | expVersion?: VersionInfo;
30 | };
31 |
32 | export interface ProgressData {
33 | hash: string;
34 | received: number;
35 | total: number;
36 | }
37 |
38 | export type EventType =
39 | | 'rollback'
40 | | 'errorChecking'
41 | | 'checking'
42 | | 'downloading'
43 | | 'downloadSuccess'
44 | | 'errorUpdate'
45 | | 'markSuccess'
46 | | 'downloadingApk'
47 | | 'rejectStoragePermission'
48 | | 'errorStoragePermission'
49 | | 'errorDownloadAndInstallApk'
50 | | 'errorInstallApk';
51 |
52 | export interface EventData {
53 | currentVersion: string;
54 | cInfo: {
55 | rnu: string;
56 | rn: string;
57 | os: string;
58 | uuid: string;
59 | };
60 | packageVersion: string;
61 | buildTime: string;
62 | message?: string;
63 | rolledBackVersion?: string;
64 | newVersion?: string;
65 | name?: string;
66 | description?: string;
67 | metaInfo?: string;
68 | [key: string]: any;
69 | }
70 |
71 | export type UpdateEventsLogger = ({
72 | type,
73 | data,
74 | }: {
75 | type: EventType;
76 | data: EventData;
77 | }) => void;
78 |
79 | export interface UpdateServerConfig {
80 | main: string;
81 | backups?: string[];
82 | queryUrls?: string[];
83 | }
84 |
85 | export interface ClientOptions {
86 | appKey: string;
87 | server?: UpdateServerConfig;
88 | logger?: UpdateEventsLogger;
89 | locale?: 'zh' | 'en';
90 | updateStrategy?:
91 | | 'alwaysAlert'
92 | | 'alertUpdateAndIgnoreError'
93 | | 'silentAndNow'
94 | | 'silentAndLater'
95 | | null;
96 | checkStrategy?: 'onAppStart' | 'onAppResume' | 'both' | null;
97 | autoMarkSuccess?: boolean;
98 | dismissErrorAfter?: number;
99 | debug?: boolean;
100 | throwError?: boolean;
101 | beforeCheckUpdate?: () => Promise;
102 | beforeDownloadUpdate?: (info: CheckResult) => Promise;
103 | afterDownloadUpdate?: (info: CheckResult) => Promise;
104 | onPackageExpired?: (info: CheckResult) => Promise;
105 | overridePackageVersion?: string;
106 | }
107 |
108 | export interface UpdateTestPayload {
109 | type: '__rnPushyVersionHash' | string | null;
110 | data: any;
111 | }
112 |
--------------------------------------------------------------------------------
/Example/harmony_use_pushy/harmony/entry/src/main/ets/pages/Index.ets:
--------------------------------------------------------------------------------
1 | import { PushyFileJSBundleProvider } from 'pushy/src/main/ets/PushyFileJSBundleProvider';
2 | import { ComponentBuilderContext, RNOHCoreContext,RNAbility,
3 | MetroJSBundleProvider } from '@rnoh/react-native-openharmony';
4 | import {
5 | RNApp,
6 | AnyJSBundleProvider,
7 | ResourceJSBundleProvider,
8 | TraceJSBundleProviderDecorator,
9 | } from '@rnoh/react-native-openharmony'
10 | import { createRNPackages } from '../RNPackagesFactory'
11 | import preferences from '@ohos.data.preferences';
12 |
13 | const arkTsComponentNames: Array = [];
14 |
15 | @Builder
16 | export function buildCustomRNComponent(ctx: ComponentBuilderContext) {
17 | // There seems to be a problem with the placement of ArkTS components in mixed mode. Nested Stack temporarily avoided.
18 | Stack() {
19 | }
20 | .position({ x: 0, y: 0 })
21 | }
22 |
23 |
24 | const wrappedCustomRNComponentBuilder = wrapBuilder(buildCustomRNComponent)
25 |
26 | @Entry
27 | @Component
28 | struct Index {
29 | @StorageLink('RNOHCoreContext') private rnohCoreContext: RNOHCoreContext | undefined = undefined
30 | @State shouldShow: boolean = false
31 |
32 | aboutToAppear(): void {
33 | this.shouldShow = true
34 | }
35 |
36 | onBackPress(): boolean | undefined {
37 | // NOTE: this is required since `Ability`'s `onBackPressed` function always
38 | // terminates or puts the app in the background, but we want Ark to ignore it completely
39 | // when handled by RN
40 | this.rnohCoreContext!.dispatchBackPress()
41 |
42 | // this.preferences = preferences.getPreferencesSync(this.context, {name:'update'});
43 | return true
44 | }
45 |
46 | build() {
47 | Column() {
48 | if (this.rnohCoreContext && this.shouldShow) {
49 | RNApp({
50 | rnInstanceConfig: {
51 | createRNPackages,
52 | enableNDKTextMeasuring: true,
53 | enableBackgroundExecutor: false,
54 | enableCAPIArchitecture: true,
55 | arkTsComponentNames: arkTsComponentNames,
56 | },
57 | appKey: "harmony_use_pushy",
58 | wrappedCustomRNComponentBuilder: wrappedCustomRNComponentBuilder,
59 | onSetUp: (rnInstance) => {
60 | rnInstance.enableFeatureFlag("ENABLE_RN_INSTANCE_CLEAN_UP")
61 | },
62 | jsBundleProvider: new TraceJSBundleProviderDecorator(
63 | new AnyJSBundleProvider([
64 | // local debug mode
65 | new MetroJSBundleProvider(),
66 | // release mode
67 | new PushyFileJSBundleProvider(this.rnohCoreContext.uiAbilityContext),
68 | new ResourceJSBundleProvider(this.rnohCoreContext.uiAbilityContext.resourceManager, 'bundle.harmony.js')
69 | ]),
70 | this.rnohCoreContext.logger),
71 | })
72 | }
73 | }
74 | .height('100%')
75 | .width('100%')
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/ios/RCTPushy/RCTPushyDownloader.mm:
--------------------------------------------------------------------------------
1 | #import "RCTPushyDownloader.h"
2 |
3 | @interface RCTPushyDownloader()
4 |
5 | @property (copy) void (^progressHandler)(long long, long long);
6 | @property (copy) void (^completionHandler)(NSString*, NSError*);
7 | @property (copy) NSString *savePath;
8 | @end
9 |
10 | @implementation RCTPushyDownloader
11 |
12 | + (void)download:(NSString *)downloadPath savePath:(NSString *)savePath
13 | progressHandler:(void (^)(long long receivedBytes, long long totalBytes))progressHandler
14 | completionHandler:(void (^)(NSString *path, NSError *error))completionHandler
15 | {
16 | NSAssert(downloadPath, @"no download path");
17 | NSAssert(savePath, @"no save path");
18 |
19 | RCTPushyDownloader *downloader = [RCTPushyDownloader new];
20 | downloader.progressHandler = progressHandler;
21 | downloader.completionHandler = completionHandler;
22 | downloader.savePath = savePath;
23 |
24 | [downloader download:downloadPath];
25 | }
26 |
27 | - (void)dealloc
28 | {
29 | }
30 |
31 | - (void)download:(NSString *)path
32 | {
33 | NSURL *url = [NSURL URLWithString:path];
34 |
35 | NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
36 | NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfig
37 | delegate:self
38 | delegateQueue:nil];
39 |
40 | NSURLSessionDownloadTask *task = [session downloadTaskWithURL:url];
41 | [session downloadTaskWithURL:url];
42 | [task resume];
43 | }
44 |
45 | #pragma mark - session delegate
46 |
47 | - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
48 | didWriteData:(int64_t)bytesWritten
49 | totalBytesWritten:(int64_t)totalBytesWritten
50 | totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
51 | {
52 | #ifdef DEBUG
53 | NSLog(@"download progress, %lld, %lld, %lld", bytesWritten, totalBytesWritten, totalBytesExpectedToWrite);
54 | #endif
55 |
56 | if (self.progressHandler) {
57 | self.progressHandler(totalBytesWritten ,totalBytesExpectedToWrite);
58 | }
59 | }
60 |
61 | - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
62 | didFinishDownloadingToURL:(NSURL *)location
63 | {
64 | NSData *data = [NSData dataWithContentsOfURL:location];
65 | NSError *error;
66 | [data writeToFile:self.savePath options:NSDataWritingAtomic error:&error];
67 | if (error) {
68 | if (self.completionHandler) {
69 | self.completionHandler(nil, error);
70 | self.completionHandler = nil;
71 | }
72 | }
73 | }
74 |
75 | - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
76 | didCompleteWithError:(NSError *)error
77 | {
78 | if (self.completionHandler) {
79 | self.completionHandler(self.savePath, error);
80 | }
81 | }
82 |
83 | @end
84 |
--------------------------------------------------------------------------------
/Example/testHotUpdate/.detoxrc.js:
--------------------------------------------------------------------------------
1 | /** @type {Detox.DetoxConfig} */
2 | module.exports = {
3 | logger: {
4 | level: process.env.CI ? 'debug' : undefined,
5 | },
6 | testRunner: {
7 | args: {
8 | config: 'e2e/jest.config.js',
9 | maxWorkers: process.env.CI ? 2 : undefined,
10 | _: ['e2e'],
11 | },
12 | },
13 | artifacts: {
14 | plugins: {
15 | log: process.env.CI ? 'failing' : undefined,
16 | screenshot: process.env.CI ? 'failing' : undefined,
17 | },
18 | },
19 | apps: {
20 | 'ios.debug': {
21 | type: 'ios.app',
22 | binaryPath: 'ios/build/Build/Products/Debug-iphonesimulator/AwesomeProject.app',
23 | build: "xcodebuild -workspace ios/AwesomeProject.xcworkspace -UseNewBuildSystem=NO -scheme AwesomeProject -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build",
24 | start: "scripts/start-rn.sh ios",
25 | },
26 | 'ios.release': {
27 | type: 'ios.app',
28 | binaryPath:
29 | 'ios/build/Build/Products/Release-iphonesimulator/AwesomeProject.app',
30 | build:
31 | 'export RCT_NO_LAUNCH_PACKAGER=true && xcodebuild -workspace ios/AwesomeProject.xcworkspace -UseNewBuildSystem=NO -scheme AwesomeProject -configuration Release -sdk iphonesimulator -derivedDataPath ios/build -quiet',
32 | },
33 | 'android.debug': {
34 | type: 'android.apk',
35 | binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk',
36 | build:
37 | 'cd android && ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug',
38 | start: "scripts/start-rn.sh android",
39 | reversePorts: [8081],
40 | },
41 | 'android.release': {
42 | type: 'android.apk',
43 | binaryPath: 'android/app/build/outputs/apk/release/app-release.apk',
44 | build:
45 | 'cd android && ./gradlew assembleRelease assembleAndroidTest -DtestBuildType=release',
46 | },
47 | },
48 | devices: {
49 | simulator: {
50 | type: 'ios.simulator',
51 | device: {
52 | type: 'iPhone 14',
53 | },
54 | },
55 | attached: {
56 | type: 'android.attached',
57 | device: {
58 | adbName: '.*',
59 | },
60 | },
61 | emulator: {
62 | type: 'android.emulator',
63 | device: {
64 | avdName: 'Pixel_3a_API_33_arm64-v8a',
65 | },
66 | },
67 | },
68 | configurations: {
69 | 'ios.sim.debug': {
70 | device: 'simulator',
71 | app: 'ios.debug',
72 | },
73 | 'ios.sim.release': {
74 | device: 'simulator',
75 | app: 'ios.release',
76 | },
77 | 'android.att.debug': {
78 | device: 'attached',
79 | app: 'android.debug',
80 | },
81 | 'android.att.release': {
82 | device: 'attached',
83 | app: 'android.release',
84 | },
85 | 'android.emu.debug': {
86 | device: 'emulator',
87 | app: 'android.debug',
88 | },
89 | 'android.emu.release': {
90 | device: 'emulator',
91 | app: 'android.release',
92 | },
93 | },
94 | };
95 |
--------------------------------------------------------------------------------
/scripts/check-expo-version.js:
--------------------------------------------------------------------------------
1 | const ownPackageJson = require('../package.json');
2 |
3 | if (process.env.npm_package_name === ownPackageJson.name) {
4 | console.log('Skipping postinstall during local development.');
5 | process.exit(0);
6 | }
7 |
8 | const fs = require('fs');
9 | const path = require('path');
10 |
11 | const projectRoot = path.resolve(__dirname, '..'); // react-native-update module root
12 | const expoConfigPath = path.resolve(projectRoot, 'expo-module.config.json');
13 |
14 | function getExpoMajorVersion() {
15 | let resolvedExpoPackagePath;
16 | try {
17 | // Use require.resolve to find expo's package.json from the host project's perspective
18 | resolvedExpoPackagePath = require.resolve('expo/package.json', {
19 | paths: [path.resolve(projectRoot, '..', '..')],
20 | });
21 | } catch (e) {
22 | console.log(
23 | 'Expo not found in project node_modules (via require.resolve).',
24 | );
25 | return null; // Expo not found or resolvable
26 | }
27 |
28 | // Check if the resolved path actually exists (belt-and-suspenders)
29 | if (!fs.existsSync(resolvedExpoPackagePath)) {
30 | console.log(
31 | `Expo package.json path resolved to ${resolvedExpoPackagePath}, but file does not exist.`,
32 | );
33 | return null;
34 | }
35 |
36 | try {
37 | const packageJson = JSON.parse(
38 | fs.readFileSync(resolvedExpoPackagePath, 'utf8'),
39 | );
40 | const version = packageJson.version;
41 | if (!version) {
42 | console.log('Expo package.json does not contain a version.');
43 | return null; // Version not found
44 | }
45 |
46 | // Extract the first number sequence as the major version
47 | const match = version.match(/\d+/);
48 | if (!match) {
49 | console.log(
50 | `Could not parse major version from Expo version string: ${version}`,
51 | );
52 | return null; // Cannot parse version
53 | }
54 |
55 | return parseInt(match[0], 10);
56 | } catch (error) {
57 | console.error('Error reading or parsing Expo package.json:', error);
58 | return null; // Error during processing
59 | }
60 | }
61 |
62 | function checkAndCleanExpoConfig() {
63 | const majorVersion = getExpoMajorVersion();
64 |
65 | // Condition: Expo not found OR major version is less than 50
66 | if (majorVersion === null || majorVersion < 50) {
67 | if (fs.existsSync(expoConfigPath)) {
68 | try {
69 | fs.unlinkSync(expoConfigPath);
70 | console.log(
71 | `Expo version (${
72 | majorVersion !== null ? majorVersion : 'not found'
73 | }) is < 50 or Expo not found. Deleted ${expoConfigPath}`,
74 | );
75 | } catch (error) {
76 | console.error(`Failed to delete ${expoConfigPath}:`, error);
77 | }
78 | } else {
79 | console.log(
80 | `Expo version (${
81 | majorVersion !== null ? majorVersion : 'not found'
82 | }) is < 50 or Expo not found. ${expoConfigPath} does not exist, no action needed.`,
83 | );
84 | }
85 | } else {
86 | console.log(
87 | `Expo version (${majorVersion}) is >= 50. Kept ${expoConfigPath}`,
88 | );
89 | }
90 | }
91 |
92 | checkAndCleanExpoConfig();
93 |
--------------------------------------------------------------------------------
/android/src/main/java/cn/reactnative/modules/update/SafeZipFile.java:
--------------------------------------------------------------------------------
1 | package cn.reactnative.modules.update;
2 |
3 | import android.util.Log;
4 |
5 | import java.io.BufferedInputStream;
6 | import java.io.BufferedOutputStream;
7 | import java.io.File;
8 | import java.io.FileOutputStream;
9 | import java.io.IOException;
10 | import java.io.InputStream;
11 | import java.util.Enumeration;
12 | import java.util.zip.ZipEntry;
13 | import java.util.zip.ZipFile;
14 |
15 |
16 | public class SafeZipFile extends ZipFile {
17 |
18 | public SafeZipFile(File file) throws IOException {
19 | super(file);
20 | }
21 |
22 | private static final int BUFFER_SIZE = 8192;
23 |
24 | @Override
25 | public Enumeration extends ZipEntry> entries() {
26 | return new SafeZipEntryIterator(super.entries());
27 | }
28 |
29 | private static class SafeZipEntryIterator implements Enumeration {
30 |
31 | final private Enumeration extends ZipEntry> delegate;
32 |
33 | private SafeZipEntryIterator(Enumeration extends ZipEntry> delegate) {
34 | this.delegate = delegate;
35 | }
36 |
37 | @Override
38 | public boolean hasMoreElements() {
39 | return delegate.hasMoreElements();
40 | }
41 |
42 | @Override
43 | public ZipEntry nextElement() {
44 | ZipEntry entry = delegate.nextElement();
45 | if (null != entry) {
46 | String name = entry.getName();
47 | /**
48 | * avoid ZipperDown
49 | */
50 | if (null != name && (name.contains("../") || name.contains("..\\"))) {
51 | throw new SecurityException("illegal entry: " + name);
52 | }
53 | }
54 | return entry;
55 | }
56 | }
57 |
58 | public void unzipToPath(ZipEntry ze, File targetPath) throws IOException {
59 | String name = ze.getName();
60 | File target = new File(targetPath, name);
61 |
62 | // Fixing a Zip Path Traversal Vulnerability
63 | // https://support.google.com/faqs/answer/9294009
64 | String canonicalPath = target.getCanonicalPath();
65 | if (!canonicalPath.startsWith(targetPath.getCanonicalPath() + File.separator)) {
66 | throw new SecurityException("Illegal name: " + name);
67 | }
68 |
69 |
70 | Log.d("react-native-update", "Unzipping " + name);
71 |
72 | if (ze.isDirectory()) {
73 | target.mkdirs();
74 | return;
75 | }
76 | unzipToFile(ze, target);
77 | }
78 |
79 | public void unzipToFile(ZipEntry ze, File target) throws IOException {
80 | try (InputStream inputStream = getInputStream(ze)) {
81 | try (BufferedOutputStream output = new BufferedOutputStream(new FileOutputStream(target));
82 | BufferedInputStream input = new BufferedInputStream(inputStream)) {
83 | byte[] buffer = new byte[BUFFER_SIZE];
84 | int n;
85 | while ((n = input.read(buffer, 0, BUFFER_SIZE)) >= 0) {
86 | output.write(buffer, 0, n);
87 | }
88 | }
89 | }
90 | }
91 |
92 | }
--------------------------------------------------------------------------------
/src/core.ts:
--------------------------------------------------------------------------------
1 | import { NativeEventEmitter, NativeModules, Platform } from 'react-native';
2 | import { emptyModule, log } from './utils';
3 | const {
4 | version: v,
5 | } = require('react-native/Libraries/Core/ReactNativeVersion');
6 | const RNVersion = `${v.major}.${v.minor}.${v.patch}`;
7 | const isTurboModuleEnabled =
8 | // https://github.com/facebook/react-native/pull/48362
9 | (global as any).__turboModuleProxy || (global as any).RN$Bridgeless;
10 |
11 | export const PushyModule =
12 | Platform.OS === 'web'
13 | ? emptyModule
14 | : isTurboModuleEnabled
15 | ? require('./NativePushy').default
16 | : NativeModules.Pushy;
17 |
18 | export const UpdateModule = PushyModule;
19 |
20 | if (!PushyModule) {
21 | throw Error(
22 | 'Failed to load react-native-update native module, please try to recompile',
23 | );
24 | }
25 |
26 | const PushyConstants = isTurboModuleEnabled
27 | ? PushyModule.getConstants()
28 | : PushyModule;
29 |
30 | export const downloadRootDir: string = PushyConstants.downloadRootDir;
31 | export const packageVersion: string = PushyConstants.packageVersion;
32 | export const currentVersion: string = PushyConstants.currentVersion;
33 |
34 | export function setLocalHashInfo(hash: string, info: Record) {
35 | PushyModule.setLocalHashInfo(hash, JSON.stringify(info));
36 | }
37 |
38 | const currentVersionInfoString: string = PushyConstants.currentVersionInfo;
39 | let _currentVersionInfo: Record = {};
40 | let isDebugChannel = false;
41 | if (currentVersionInfoString) {
42 | try {
43 | _currentVersionInfo = JSON.parse(currentVersionInfoString);
44 | if (_currentVersionInfo.debugChannel) {
45 | isDebugChannel = true;
46 | delete _currentVersionInfo.debugChannel;
47 | setLocalHashInfo(currentVersion, _currentVersionInfo);
48 | }
49 | } catch (error) {
50 | console.error(
51 | 'Failed to parse currentVersionInfo:',
52 | currentVersionInfoString,
53 | );
54 | }
55 | }
56 | export const currentVersionInfo = _currentVersionInfo;
57 |
58 | export const isFirstTime: boolean = PushyConstants.isFirstTime;
59 | export const isFirstTimeDebug: boolean = isFirstTime && isDebugChannel;
60 | export const rolledBackVersion: string = PushyConstants.rolledBackVersion;
61 | export const isRolledBack: boolean = !!rolledBackVersion;
62 |
63 | export const buildTime: string = PushyConstants.buildTime;
64 | let uuid = PushyConstants.uuid;
65 |
66 |
67 | async function getLocalHashInfo(hash: string) {
68 | return JSON.parse(await PushyModule.getLocalHashInfo(hash));
69 | }
70 |
71 | // @deprecated use currentVersionInfo instead
72 | export async function getCurrentVersionInfo(): Promise<{
73 | name?: string;
74 | description?: string;
75 | metaInfo?: string;
76 | }> {
77 | return currentVersion ? (await getLocalHashInfo(currentVersion)) || {} : {};
78 | }
79 |
80 | export const pushyNativeEventEmitter = new NativeEventEmitter(PushyModule);
81 |
82 | if (!uuid) {
83 | uuid = require('nanoid/non-secure').nanoid();
84 | PushyModule.setUuid(uuid);
85 | }
86 |
87 | log('uuid: ' + uuid);
88 |
89 | export const cInfo = {
90 | rnu: require('../package.json').version,
91 | rn: RNVersion,
92 | os: Platform.OS + ' ' + Platform.Version,
93 | uuid,
94 | };
95 |
--------------------------------------------------------------------------------
/Example/harmony_use_pushy/android/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%"=="" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%"=="" set DIRNAME=.
29 | @rem This is normally unused
30 | set APP_BASE_NAME=%~n0
31 | set APP_HOME=%DIRNAME%
32 |
33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
35 |
36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
38 |
39 | @rem Find java.exe
40 | if defined JAVA_HOME goto findJavaFromJavaHome
41 |
42 | set JAVA_EXE=java.exe
43 | %JAVA_EXE% -version >NUL 2>&1
44 | if %ERRORLEVEL% equ 0 goto execute
45 |
46 | echo.
47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
48 | echo.
49 | echo Please set the JAVA_HOME variable in your environment to match the
50 | echo location of your Java installation.
51 |
52 | goto fail
53 |
54 | :findJavaFromJavaHome
55 | set JAVA_HOME=%JAVA_HOME:"=%
56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
57 |
58 | if exist "%JAVA_EXE%" goto execute
59 |
60 | echo.
61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
62 | echo.
63 | echo Please set the JAVA_HOME variable in your environment to match the
64 | echo location of your Java installation.
65 |
66 | goto fail
67 |
68 | :execute
69 | @rem Setup the command line
70 |
71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
72 |
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if %ERRORLEVEL% equ 0 goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | set EXIT_CODE=%ERRORLEVEL%
85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
87 | exit /b %EXIT_CODE%
88 |
89 | :mainEnd
90 | if "%OS%"=="Windows_NT" endlocal
91 |
92 | :omega
93 |
--------------------------------------------------------------------------------