Nitro OTA Package (#560)

Adds internal over-the-air update support

Powered by `react-native-nitro-modules`, this OTA functionality is faster than the previous implementation, and won't slow down the UI when updates are being applied since updates occur off the main thread
This commit is contained in:
Ritesh Shukla
2025-10-22 00:58:53 +05:30
committed by GitHub
parent 741a832998
commit 4d560be350
16 changed files with 229 additions and 71 deletions

View File

@@ -4,7 +4,7 @@ inputs:
xcode-version:
description: 'The xcode version to use'
required: false
default: '16.3.0'
default: '16.4.0'
runs:
using: "composite"
steps:

View File

@@ -31,14 +31,23 @@ jobs:
- name: 🚀 Run fastlane build
run: yarn fastlane:ios:build
env:
# FASTLANE_PASSWORD: ${{ secrets.FASTLANE_PASSWORD }}
APPSTORE_CONNECT_API_KEY_JSON: ${{ secrets.APPSTORE_CONNECT_API_KEY_JSON }}
FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: ${{ secrets.FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD }}
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
MATCH_REPO_PAT: "anultravioletaurora:${{ secrets.SIGNING_REPO_PAT }}"
run: |
cd ios
set -o pipefail
xcodebuild \
CC=clang CPLUSPLUS=clang++ LD=clang LDPLUSPLUS=clang++ \
-derivedDataPath build -UseModernBuildSystem=YES \
-workspace Jellify.xcworkspace \
-scheme Jellify \
-sdk iphonesimulator \
-configuration Release \
-destination 'generic/platform=iOS Simulator' \
build \
CODE_SIGNING_ALLOWED=NO
- name: Package .app for Simulator
run: |
cd ios/build/Build/Products/Release-iphonesimulator
zip -r Jellify-Release-Simulator.zip Jellify.app
- name: 📦 Upload IPA for testing
uses: actions/upload-artifact@v4
@@ -48,5 +57,8 @@ jobs:
path: |
ios/build/*.ipa
ios/*.ipa
Jellify.app
*.zip
ios/build/Build/Products/Release-iphonesimulator/Jellify-Release-Simulator.zip
retention-days: 7
if-no-files-found: warn

View File

@@ -11,7 +11,7 @@ import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
import com.facebook.react.defaults.DefaultReactNativeHost
import com.facebook.react.soloader.OpenSourceMergedSoMapping
import com.facebook.soloader.SoLoader
import com.otahotupdate.OtaHotUpdate
import com.margelo.nitro.nitroota.core.getStoredBundlePath
import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative
@@ -27,7 +27,7 @@ class MainApplication : Application(), ReactApplication {
// Packages that cannot be autolinked yet can be added manually here, for example:
// add(MyReactNativePackage())
},
jsBundleFilePath = OtaHotUpdate.bundleJS(this@MainApplication)
jsBundleFilePath = getStoredBundlePath(applicationContext)
)
}

View File

@@ -6,6 +6,8 @@ import React_RCTAppDelegate
import ReactAppDependencyProvider
import react_native_ota_hot_update
import GoogleCast
import NitroOtaBundleManager
@@ -73,7 +75,12 @@ class ReactNativeDelegate: RCTDefaultReactNativeFactoryDelegate {
#if DEBUG
return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index");
#else
return OtaHotUpdate.getBundle() // -> Add this line
// Check for OTA bundle first
if let bundleURL = NitroOtaBundleManager.shared.getStoredBundleURL() {
return bundleURL
}
// Fallback to main bundle
return Bundle.main.url(forResource: "main", withExtension: "jsbundle")
#endif
}

View File

@@ -187,15 +187,15 @@ GEM
fastlane-sirp (1.0.0)
sysrandom (~> 1.0)
ffi (1.17.2)
ffi (1.17.2-aarch64-linux-gnu)
ffi (1.17.2-aarch64-linux)
ffi (1.17.2-aarch64-linux-musl)
ffi (1.17.2-arm-linux-gnu)
ffi (1.17.2-arm-linux)
ffi (1.17.2-arm-linux-musl)
ffi (1.17.2-arm64-darwin)
ffi (1.17.2-x86-linux-gnu)
ffi (1.17.2-x86-linux)
ffi (1.17.2-x86-linux-musl)
ffi (1.17.2-x86_64-darwin)
ffi (1.17.2-x86_64-linux-gnu)
ffi (1.17.2-x86_64-linux)
ffi (1.17.2-x86_64-linux-musl)
fourflusher (2.3.1)
fuzzy_match (2.0.4)
@@ -334,16 +334,16 @@ GEM
xcpretty (~> 0.2, >= 0.0.7)
PLATFORMS
aarch64-linux-gnu
aarch64-linux
aarch64-linux-musl
arm-linux-gnu
arm-linux
arm-linux-musl
arm64-darwin
ruby
x86-linux-gnu
x86-linux
x86-linux-musl
x86_64-darwin
x86_64-linux-gnu
x86_64-linux
x86_64-linux-musl
DEPENDENCIES

View File

@@ -41,7 +41,7 @@
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
buildConfiguration = "Release"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"

View File

@@ -21,6 +21,7 @@ target 'Jellify' do
config = use_native_modules!
pod 'SDWebImage', :modular_headers => true
pod 'NitroOtaBundleManager', :path => '../node_modules/react-native-nitro-ota'
use_react_native!(
:path => config[:reactNativePath],

View File

@@ -42,7 +42,7 @@ PODS:
- ReactCommon/turbomodule/core
- SocketRocket
- Yoga
- NitroModules (0.29.4):
- NitroModules (0.30.2):
- boost
- DoubleConversion
- fast_float
@@ -71,6 +71,65 @@ PODS:
- ReactCommon/turbomodule/core
- SocketRocket
- Yoga
- NitroOta (0.2.3):
- boost
- DoubleConversion
- fast_float
- fmt
- glog
- hermes-engine
- NitroModules
- RCT-Folly
- RCT-Folly/Fabric
- RCTRequired
- RCTTypeSafety
- React-callinvoker
- React-Core
- React-debug
- React-Fabric
- React-featureflags
- React-graphics
- React-ImageManager
- React-jsi
- React-NativeModulesApple
- React-RCTFabric
- React-renderercss
- React-rendererdebug
- React-utils
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- SocketRocket
- SSZipArchive
- Yoga
- NitroOtaBundleManager (0.2.3):
- boost
- DoubleConversion
- fast_float
- fmt
- glog
- hermes-engine
- RCT-Folly
- RCT-Folly/Fabric
- RCTRequired
- RCTTypeSafety
- React-Core
- React-debug
- React-Fabric
- React-featureflags
- React-graphics
- React-ImageManager
- React-jsi
- React-NativeModulesApple
- React-RCTFabric
- React-renderercss
- React-rendererdebug
- React-utils
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- SocketRocket
- Yoga
- NitroWebImage (0.6.1):
- boost
- DoubleConversion
@@ -3050,9 +3109,9 @@ PODS:
- ReactCommon/turbomodule/core
- SocketRocket
- Yoga
- SDWebImage (5.11.1):
- SDWebImage/Core (= 5.11.1)
- SDWebImage/Core (5.11.1)
- SDWebImage (5.21.2):
- SDWebImage/Core (= 5.21.2)
- SDWebImage/Core (5.21.2)
- Sentry/HybridSDK (8.56.0)
- SocketRocket (0.7.1)
- SSZipArchive (2.4.3)
@@ -3070,6 +3129,8 @@ DEPENDENCIES:
- hermes-engine (from `../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`)
- NitroImage (from `../node_modules/react-native-nitro-image`)
- NitroModules (from `../node_modules/react-native-nitro-modules`)
- NitroOta (from `../node_modules/react-native-nitro-ota`)
- NitroOtaBundleManager (from `../node_modules/react-native-nitro-ota`)
- NitroWebImage (from `../node_modules/react-native-nitro-web-image`)
- RCT-Folly (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`)
- RCTDeprecation (from `../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation`)
@@ -3197,6 +3258,10 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-nitro-image"
NitroModules:
:path: "../node_modules/react-native-nitro-modules"
NitroOta:
:path: "../node_modules/react-native-nitro-ota"
NitroOtaBundleManager:
:path: "../node_modules/react-native-nitro-ota"
NitroWebImage:
:path: "../node_modules/react-native-nitro-web-image"
RCT-Folly:
@@ -3389,7 +3454,9 @@ SPEC CHECKSUMS:
google-cast-sdk: 1fb6724e94cc5ff23b359176e0cf6360586bb97a
hermes-engine: 273e30e7fb618279934b0b95ffab60ecedb7acf5
NitroImage: f2d14e6531629630904d8ceb4037459d6057440a
NitroModules: 8d96528777600e967d371fd62b7eb183e9204530
NitroModules: 72acdf761541be4f8bbb3f0cdca41ae23ce13c50
NitroOta: b2adec40232e3da0cc9eeaa3f7e1ec67313b1f17
NitroOtaBundleManager: 09eeec5c1d7e33b868b2374ce64613f23ad348cd
NitroWebImage: 04de36dd513d350fe3d4d3a9279a95817c1a39f1
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
Protobuf: 164aea2ae380c3951abdc3e195220c01d17400e0
@@ -3481,13 +3548,13 @@ SPEC CHECKSUMS:
RNScreens: 833237c48c756d40764540246a501b47dadb2cac
RNSentry: 60919c9cdac7e4b35e9f5dd0149f551ec12f35cb
RNWorklets: ab618bf7d1c7fd2cb793b9f0f39c3e29274b3ebf
SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d
SDWebImage: 9f177d83116802728e122410fb25ad88f5c7608a
Sentry: 3d82977434c80381cae856c40b99c39e4be6bc11
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
SSZipArchive: fe6a26b2a54d5a0890f2567b5cc6de5caa600aef
SwiftAudioEx: f6aa653770f3a0d3851edaf8d834a30aee4a7646
Yoga: 689c8e04277f3ad631e60fe2a08e41d411daf8eb
PODFILE CHECKSUM: 320ff45ce6a038db4058773abf4993a674de141e
PODFILE CHECKSUM: 05d07b9cff134e4c27345bc2b588e090e4d3431c
COCOAPODS: 1.16.2

View File

@@ -13,6 +13,7 @@ module.exports = {
'./jest/setup/rntp.ts',
'./jest/setup/sentry.ts',
'./jest/setup/nitro-image.ts',
'./jest/setup/nitro-ota.ts',
'./tamagui.config.ts',
'./jest/setup/native-modules.ts',
],

21
jest/setup/nitro-ota.ts Normal file
View File

@@ -0,0 +1,21 @@
// Mock for react-native-nitro-ota
jest.mock('react-native-nitro-ota', () => ({
githubOTA: jest.fn(() => ({
downloadUrl: 'mock://download.url',
versionUrl: 'mock://version.url',
})),
OTAUpdateManager: jest.fn().mockImplementation(() => ({
checkForUpdates: jest.fn().mockResolvedValue(null),
downloadUpdate: jest.fn().mockResolvedValue(undefined),
})),
}))
// Update the existing nitro-modules mock to include createHybridObject
jest.mock('react-native-nitro-modules', () => ({
NitroModules: {
createModule: jest.fn(),
install: jest.fn(),
createHybridObject: jest.fn(() => ({})),
},
createNitroModule: jest.fn(),
}))

View File

@@ -77,7 +77,8 @@
"react-native-linear-gradient": "^2.8.3",
"react-native-mmkv": "3.3.3",
"react-native-nitro-image": "^0.6.1",
"react-native-nitro-modules": "^0.29.4",
"react-native-nitro-modules": "^0.30.2",
"react-native-nitro-ota": "^0.2.3",
"react-native-nitro-web-image": "^0.6.1",
"react-native-ota-hot-update": "2.3.1",
"react-native-pager-view": "^6.9.1",
@@ -143,4 +144,4 @@
"node": ">=18"
},
"packageManager": "yarn@1.22.22"
}
}

View File

@@ -0,0 +1,40 @@
#!/usr/bin/env bash
set -euo pipefail
FILE="ota.version"
# Array of sentences
sentences=(
"Git Blame violet"
"Thank you pikachu"
"Margelo folks are Awesome"
"Pikachu Should have coded this"
"meta sue violet"
)
# Read previous value if file exists
prev=""
if [[ -f "$FILE" ]]; then
prev=$(<"$FILE")
fi
# Function to get a random new sentence (not same as prev)
get_random_sentence() {
local choice
while true; do
choice="${sentences[RANDOM % ${#sentences[@]}]}"
if [[ "$choice" != "$prev" ]]; then
echo "$choice"
return
fi
done
}
new_sentence=$(get_random_sentence)
# Write atomically
tmp="${FILE}.tmp.$$"
echo "$new_sentence" > "$tmp"
mv "$tmp" "$FILE"
echo "✅ Updated $FILE with: \"$new_sentence\""

View File

@@ -1,5 +1,5 @@
version=$(jq -r '.version' "$(dirname "$0")/../package.json")
target_branch="${version}/android"
target_branch="nitro_${version}_android"
cd android
git clone https://github.com/Jellify-Music/App-Bundles.git
cd App-Bundles
@@ -13,6 +13,7 @@ fi
cd ../..
yarn createBundle:android
cd android/App-Bundles
bash ../../scripts/getRandomVersion.sh
git add .
git commit -m "OTA-Update - $(date +'%b %d %H:%M')"
git push https://x-access-token:$SIGNING_REPO_PAT@github.com/Jellify-Music/App-Bundles.git "$target_branch"

View File

@@ -1,6 +1,6 @@
version=$(jq -r '.version' "$(dirname "$0")/../package.json")
target_branch="${version}/ios"
target_branch="nitro_${version}_ios"
cd ios
rm -rf App-Bundles
git clone https://github.com/Jellify-Music/App-Bundles.git
@@ -16,6 +16,7 @@ rm -rf Readme.md
cd ../..
yarn createBundle:ios
cd ios/App-Bundles
bash ../../scripts/getRandomVersion.sh
git add .
git commit -m "OTA-Update - $(date +'%b %d %H:%M')"
git push https://x-access-token:$SIGNING_REPO_PAT@github.com/Jellify-Music/App-Bundles.git "$target_branch"

View File

@@ -13,10 +13,19 @@ import Animated, { useSharedValue, useAnimatedStyle, withTiming } from 'react-na
import hotUpdate from 'react-native-ota-hot-update'
import DeviceInfo from 'react-native-device-info'
import { OTA_UPDATE_ENABLED } from '../../configs/config'
import { githubOTA, OTAUpdateManager } from 'react-native-nitro-ota'
const version = DeviceInfo.getVersion()
const gitBranch = `${version}/${Platform.OS}`
const gitBranch = `nitro_${version}_${Platform.OS}`
const { downloadUrl, versionUrl } = githubOTA({
githubUrl: 'https://github.com/Jellify-Music/App-Bundles',
otaVersionPath: 'ota.version', // optional, defaults to 'ota.version'
ref: gitBranch, // optional, defaults to 'main'
})
const otaManager = new OTAUpdateManager(downloadUrl, versionUrl)
const GitUpdateModal = () => {
const progress = useSharedValue(0)
@@ -35,41 +44,33 @@ const GitUpdateModal = () => {
console.log(isVisible, 'isVisible')
const onCheckGitVersion = () => {
setLoading(true)
hotUpdate.git.checkForGitUpdate({
branch: gitBranch,
bundlePath: Platform.OS === 'ios' ? 'main.jsbundle' : 'index.android.bundle',
url: 'https://github.com/Jellify-Music/App-Bundles',
onCloneFailed(msg: string) {
otaManager
.checkForUpdates()
.then((update) => {
if (update) {
otaManager
.downloadUpdate()
.then(() => {
Alert.alert(
'Jellify has been updated!',
'Restart to apply the changes',
[
{ text: 'OK', onPress: () => hotUpdate.resetApp() },
{ text: 'Cancel', style: 'cancel' },
],
)
})
.catch((error) => {
console.error('Error downloading update:', error)
})
}
})
.catch((error) => {
console.error('Error checking for updates:', error)
})
.finally(() => {
setLoading(false)
// Alert.alert('Clone project faile .d!', msg)
},
onCloneSuccess() {
Alert.alert('Jellify has been updated!', 'Restart to apply the changes', [
{ text: 'OK', onPress: () => hotUpdate.resetApp() },
{ text: 'Cancel', style: 'cancel' },
])
},
onPullFailed(msg: string) {
setLoading(false)
// Alert.alert('Pull project failed!', msg)
},
onPullSuccess() {
Alert.alert('Jellify has been updated!', 'Restart to apply the changes', [
{ text: 'OK', onPress: () => hotUpdate.resetApp() },
{ text: 'Cancel', style: 'cancel' },
])
},
onProgress(received: number, total: number) {
const percent = (+received / +total) * 100
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
progress.value = withTiming(percent, { duration: 300 })
},
onFinishProgress() {
setLoading(false)
},
})
})
}
useEffect(() => {

View File

@@ -8498,10 +8498,15 @@ react-native-nitro-image@^0.6.1:
resolved "https://registry.yarnpkg.com/react-native-nitro-image/-/react-native-nitro-image-0.6.1.tgz#bf4d37ed52d435f751a27017fade4a6d7b7fb9a6"
integrity sha512-LKttegj2fZ6NdmqZG531LB2NteW5TbGy/vbyHFtTX39g73it3R5jss+IMYmuHJHjhbFse/wu2yP+zh3MXAa6Jg==
react-native-nitro-modules@^0.29.4:
version "0.29.4"
resolved "https://registry.yarnpkg.com/react-native-nitro-modules/-/react-native-nitro-modules-0.29.4.tgz#e82a243996f4a85b9194a2e2a89970487638e1c2"
integrity sha512-AfUMcwFtj9FuEDwDLN5eIVo0lBYTQqDaV7meiFzuoZyRmc8ywykFTKfyZwRN2t8Z/WtTlfCj9Y9yaET33IImsg==
react-native-nitro-modules@^0.30.2:
version "0.30.2"
resolved "https://registry.yarnpkg.com/react-native-nitro-modules/-/react-native-nitro-modules-0.30.2.tgz#0a13acbb0bc3032cfa0198059944ddd832bc2dc3"
integrity sha512-+/uVS7FQwOiKYZQERMIvBRv5/X3CVHrFG6Nr/kIhVfVxGeUimHnBz7cgA97lJKIn7AKDRWL+UjLedW8pGOt0dg==
react-native-nitro-ota@^0.2.3:
version "0.2.3"
resolved "https://registry.yarnpkg.com/react-native-nitro-ota/-/react-native-nitro-ota-0.2.3.tgz#555f4922d7dd951a86706016ebdf04dd69ad7b16"
integrity sha512-AB2eycbbRoSPA7UvDCyaHgiSh5QhiI8kq9gpPoDSTbjLEEdFGVb9r+YPTm78bzYC5XqZIP+mPJ7TGWnuRr2c3Q==
react-native-nitro-web-image@^0.6.1:
version "0.6.1"