Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/actions/setup/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ runs:
using: composite
steps:
- name: Setup Node.js
uses: actions/setup-node@v3
uses: actions/setup-node@v5
with:
node-version: '20.x'
node-version: '24.x'
- name: Cache dependencies
id: yarn-cache
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: |
**/node_modules
Expand Down
15 changes: 12 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,20 @@ on:
jobs:
build-and-publish:
runs-on: ubuntu-latest
# Must match the Environment on npm's trusted publisher for this package
environment: prod
# Prevent the app's own release commit from re-triggering this workflow
if: github.actor != 'stallion-release-bot[bot]'
permissions: write-all
# id-token: write mints the OIDC token npm's trusted publisher verifies
permissions:
contents: write
issues: write
pull-requests: write
id-token: write
steps:
- name: Generate app token
id: app-token
uses: actions/create-github-app-token@v1
uses: actions/create-github-app-token@v2
with:
app-id: ${{ secrets.RELEASE_APP_ID }}
private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
Expand Down Expand Up @@ -42,11 +49,13 @@ jobs:
- name: Prep release
run: yarn prepare-release

# Publishes to npm via trusted publishing, so no NPM_TOKEN is needed.
# npm binds the trusted publisher to this workflow's file name: renaming
# ci.yml means updating the publisher on npmjs.com in the same change.
- name: Release package
run: yarn semantic-release
env:
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
GIT_AUTHOR_EMAIL: ${{ secrets.GH_DEPLOY_EMAIL }}
GIT_AUTHOR_NAME: ${{ secrets.GH_DEPLOY_NAME }}
GIT_COMMITTER_EMAIL: ${{ secrets.GH_DEPLOY_EMAIL }}
Expand Down
94 changes: 52 additions & 42 deletions CHANGELOG.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,10 @@ public static void applyPatch(String baseBundlePath, String diffPath, boolean is
// Create a temporary directory for the patched bundle
File tempPatchedDir = new File(diffDir.getParent(), diffDir.getName() + "_patched_temp");
try {
// Copy base bundle to temporary location
// Clear any leftover temp dir, then copy base bundle
if (tempPatchedDir.exists()) {
StallionFileManager.deleteFileOrFolderSilently(tempPatchedDir);
}
StallionFileManager.copyDirectory(baseBundleDir, tempPatchedDir);

// Read and parse manifest.json
Expand Down
12 changes: 2 additions & 10 deletions android/src/main/java/com/stallion/storage/StallionConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.content.res.Resources;
import android.provider.Settings;

import org.json.JSONException;
import org.json.JSONObject;
Expand Down Expand Up @@ -53,22 +52,15 @@ public StallionConfig(Context context, SharedPreferences sharedPreferences) {
);
this.publicSigningKey = stallionPublicKeyRes != 0 ? context.getString(stallionPublicKeyRes) : "";

// get or generate UID
// get or generate UID (random install-scoped UUID)
String cachedUniqueId = sharedPreferences.getString(
StallionConfigConstants.UNIQUE_ID_IDENTIFIER,
""
);
if(!cachedUniqueId.isEmpty()) {
this.uid = cachedUniqueId;
} else {
try {
this.uid = Settings.Secure.getString(
context.getContentResolver(),
Settings.Secure.ANDROID_ID
);
} catch (Exception ignored) {
this.uid = UUID.randomUUID().toString();
}
this.uid = UUID.randomUUID().toString();
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString(StallionConfigConstants.UNIQUE_ID_IDENTIFIER, this.uid);
editor.apply();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ public static void stabilizeProd() {
File newSlot = new File(baseFolderPath, StallionConfigConstants.PROD_DIRECTORY + StallionConfigConstants.NEW_FOLDER_SLOT);
File stableSlot = new File(baseFolderPath, StallionConfigConstants.PROD_DIRECTORY + StallionConfigConstants.STABLE_FOLDER_SLOT);

StallionFileManager.deleteFileOrFolderSilently(stableSlot);
StallionFileManager.copyDirectory(newSlot, stableSlot);

String newReleaseHash = stateManager.stallionMeta.getProdNewHash();
Expand Down
4 changes: 2 additions & 2 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ PODS:
- React-jsinspector (0.71.10)
- React-logger (0.71.10):
- glog
- react-native-stallion (2.4.0-alpha.4):
- react-native-stallion (2.4.2):
- RCT-Folly (= 2021.07.22.00)
- React-Core
- ZIPFoundation
Expand Down Expand Up @@ -497,7 +497,7 @@ SPEC CHECKSUMS:
React-jsiexecutor: 4bb480a183a354e4dbfb1012936b1a2bb9357de7
React-jsinspector: cdc854f8b13abd202afa54bc12578e5afb9cfae1
React-logger: ef2269b3afa6ba868da90496c3e17a4ec4f4cee0
react-native-stallion: 24b0975e63e09e3437a86cbfcf665c7643d0a647
react-native-stallion: 5723bb5ff2923719fad7c0cc6f1d3578c6dc601b
React-perflogger: 217095464d5c4bb70df0742fa86bf2a363693468
React-RCTActionSheet: 8deae9b85a4cbc6a2243618ea62a374880a2c614
React-RCTAnimation: 59c62353a8b59ce206044786c5d30e4754bffa64
Expand Down
10 changes: 1 addition & 9 deletions example/ios/StallionExample/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>StallionPublicSigningKey</key>
<string>MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApm9SgaDU9lXLi5OD6sd9
WpNP8bWeGRzLYOReqH5yuu21kYTDaDt1/+vTpCBNuyX2ZjtKNvKujnAXuWf8Zfj4
JcWb/r4clDQK0sNy8G7x7s+4Mup6W73lW5AtUnREQZX8IA00lp76r9ii6HYxXTpO
CgHpXyRyPLkMNc+HxaTmDHbldZtRCREcLldI7NnQkFMyZlHK2r1QxiQLpTIEK0vI
IJyXeHOeJUROebZn/UI7UDKiSCvlaHMsJ4pnIAyLrtqFQ8w9rf3aljmYX52m6Rbc
df8fwbWtBCH8OPW5SEDTMDzvE1siAvYwRNxbeXHMrAypPg/DaATdXoKMObjJWRm1
owIDAQAB</string>
<key>StallionAppToken</key>
<string>spb_qLFBKtdR9TBZtsKPDqMXvFD_ebcs-Tdjyc7F4-dX7q</string>
<string>spb_ap_Ss_cRvqX-B6iMGxIxKaUqTn9K-RgSmf4ZNTPhx4zjd</string>
<key>StallionProjectId</key>
<string>67210b6f3e19d894c6a1a4fb</string>
<key>CFBundleDevelopmentRegion</key>
Expand Down
50 changes: 34 additions & 16 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,38 @@
import * as React from 'react';

import { StyleSheet, View, Button, Text, Alert } from 'react-native';
import {
StyleSheet,
View,
Button,
Text,
Alert,
Modal,
SafeAreaView,
} from 'react-native';
import {
withStallion,
useStallionModal,
StallionDevMenu,
useStallionUpdate,
addEventListener,
restart,
ACTIVE_RELEASE_HASH,
} from 'react-native-stallion';

const App: React.FC = () => {
const { showModal } = useStallionModal();
const [isDevMenuVisible, setIsDevMenuVisible] = React.useState(false);
const { isRestartRequired, newReleaseBundle } = useStallionUpdate();

// console.log(isRestartRequired, 'isRestartRequired', newReleaseBundle);

React.useEffect(() => {
if (isRestartRequired) {
Alert.alert('New Release installed', JSON.stringify(newReleaseBundle), [
{
text: 'Restart',
onPress: restart,
},
]);
Alert.alert(
'New Release installed',
JSON.stringify(newReleaseBundle),
[
{ text: 'Cancel', style: 'cancel' },
{ text: 'Restart', onPress: restart },
],
{ cancelable: true }
);
}
}, [isRestartRequired, newReleaseBundle]);

Expand All @@ -37,9 +46,20 @@ const App: React.FC = () => {
return (
<View style={styles.container}>
<Text>Hello world</Text>
<Button title="OpenModal" onPress={showModal} />
<Button title="OpenModal" onPress={() => setIsDevMenuVisible(true)} />
<Text>Active Bundle Hash: {ACTIVE_RELEASE_HASH}</Text>
{isRestartRequired ? <Text>Restart the app</Text> : null}

<Modal
animationType="slide"
presentationStyle="fullScreen"
visible={isDevMenuVisible}
onRequestClose={() => setIsDevMenuVisible(false)}
>
<SafeAreaView style={styles.devMenuSafeArea}>
<StallionDevMenu onClosePress={() => setIsDevMenuVisible(false)} />
</SafeAreaView>
</Modal>
</View>
);
};
Expand All @@ -53,9 +73,7 @@ const styles = StyleSheet.create({
justifyContent: 'center',
backgroundColor: 'white',
},
box: {
width: 60,
height: 60,
marginVertical: 20,
devMenuSafeArea: {
flex: 1,
},
});
8 changes: 2 additions & 6 deletions ios/main/StallionConfig.m
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,12 @@ - (instancetype)initWithDefaults:(NSUserDefaults *)defaults {
_lastUnverifiedHash = [defaults stringForKey:LAST_UNVERIFIED_KEY_IDENTIFIER] ?: @"";
_baseUrl = [defaults stringForKey:BASE_URL_IDENTIFIER] ?: @"";

// get or generate UID (random install-scoped UUID)
NSString *cachedUid = [defaults stringForKey:UNIQUE_ID_IDENTIFIER];
if (cachedUid && ![cachedUid isEqualToString:@""]) {
_uid = cachedUid;
} else {
NSString *newUid = @"";
if ([UIDevice currentDevice].identifierForVendor) {
newUid = [UIDevice currentDevice].identifierForVendor.UUIDString;
} else {
newUid = [[NSUUID UUID] UUIDString];
}
NSString *newUid = [[NSUUID UUID] UUIDString];
[defaults setObject:newUid forKey:UNIQUE_ID_IDENTIFIER];
[defaults synchronize];
_uid = newUid;
Expand Down
5 changes: 4 additions & 1 deletion ios/main/StallionPatchHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,10 @@ class StallionPatchHandler {
}
}

// Copy base bundle to temporary location
// Clear any leftover temp dir, then copy base bundle
if FileManager.default.fileExists(atPath: tempPatchedDir.path) {
try? FileManager.default.removeItem(at: tempPatchedDir)
}
StallionFileManager.copyFileOrDirectory(from: baseBundlePath, to: tempPatchedDir.path)

// Verify copy succeeded
Expand Down
9 changes: 5 additions & 4 deletions ios/main/StallionSlotManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,9 @@ + (void)fallbackProd {
StallionStateManager *stateManager = [StallionStateManager sharedInstance];
NSString *baseFolderPath = stateManager.stallionConfig.filesDirectory;

[StallionFileManager deleteFileOrFolderSilently:[NSString stringWithFormat:@"%@%@%@", baseFolderPath, StallionObjConstants.prod_directory, StallionObjConstants.new_folder_slot]];
[StallionFileManager deleteFileOrFolderSilently:[NSString stringWithFormat:@"%@%@%@", baseFolderPath, StallionObjConstants.prod_directory, StallionObjConstants.stable_folder_slot]];
[StallionFileManager deleteFileOrFolderSilently:[NSString stringWithFormat:@"%@%@%@", baseFolderPath, StallionObjConstants.prod_directory, StallionObjConstants.temp_folder_slot]];
[StallionFileManager deleteFileOrFolderSilently:[NSString stringWithFormat:@"%@/%@/%@", baseFolderPath, StallionObjConstants.prod_directory, StallionObjConstants.new_folder_slot]];
[StallionFileManager deleteFileOrFolderSilently:[NSString stringWithFormat:@"%@/%@/%@", baseFolderPath, StallionObjConstants.prod_directory, StallionObjConstants.stable_folder_slot]];
[StallionFileManager deleteFileOrFolderSilently:[NSString stringWithFormat:@"%@/%@/%@", baseFolderPath, StallionObjConstants.prod_directory, StallionObjConstants.temp_folder_slot]];

[stateManager clearStallionMeta];
}
Expand All @@ -74,7 +74,8 @@ + (void)stabilizeProd {
NSString *newSlotPath = [NSString stringWithFormat:@"%@/%@/%@", baseFolderPath, StallionObjConstants.prod_directory, StallionObjConstants.new_folder_slot];
NSString *stableSlotPath = [NSString stringWithFormat:@"%@/%@/%@", baseFolderPath, StallionObjConstants.prod_directory, StallionObjConstants.stable_folder_slot];

[StallionFileManager moveFileFrom:newSlotPath to:stableSlotPath];
[StallionFileManager deleteFileOrFolderSilently:stableSlotPath];
[StallionFileManager copyFileOrDirectoryFrom:newSlotPath to:stableSlotPath];

NSString *newReleaseHash = stateManager.stallionMeta.prodNewHash;
[stateManager.stallionMeta setProdStableHash:newReleaseHash];
Expand Down
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "react-native-stallion",
"version": "2.4.2",
"version": "2.4.3",
"description": "Official React Native OTA updates SDK — CodePush alternative with patch updates, rollback, and bundle signing.",
"main": "src/index",
"types": "types/index.d.ts",
Expand Down Expand Up @@ -60,8 +60,8 @@
"@commitlint/config-conventional": "^17.0.2",
"@evilmartians/lefthook": "^1.2.2",
"@react-native-community/eslint-config": "^3.0.2",
"@semantic-release/changelog": "6.0.0",
"@semantic-release/git": "10.0.0",
"@semantic-release/changelog": "7.0.0",
"@semantic-release/git": "11.0.1",
"@types/jest": "^28.1.2",
"@types/react": "~17.0.21",
"@types/react-native": "0.70.0",
Expand All @@ -79,7 +79,7 @@
"react-native": "0.71.7",
"react-native-builder-bob": "^0.20.0",
"rimraf": "3.0.2",
"semantic-release": "19.0.3",
"semantic-release": "25.0.9",
"tsconfig-paths": "^4.2.0",
"typescript": "^4.5.2"
},
Expand Down
1 change: 1 addition & 0 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ if (StallionNativeModule?.getStallionConfig) {
useStallionModal = useStallionModalNoop;
}

export { default as StallionDevMenu } from './main/components/StallionDevMenu';
export { sync, restart } from './main/utils/StallionNativeUtils';
export { useStallionUpdate } from './main/utils/useStallionUpdate';
export { default as useStallionConfig } from './main/utils/useStallionConfig';
Expand Down
77 changes: 77 additions & 0 deletions src/main/components/StallionDevMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import React, { useContext, useEffect } from 'react';
import { StyleSheet, View } from 'react-native';

import Login from './modules/login';
import DevMenu from './modules/devMenu';
import AppHeader from './modules/devMenu/components/AppHeader';
import useStallionModal from './modules/modal/hooks/useStallionModal';

import { DS_COLORS } from '../constants/designTokens';
import { GlobalContext } from '../state';
import { IStallionDevMenuProps } from '../../types/utils.types';

const StallionDevMenu: React.FC<IStallionDevMenuProps> = ({ onClosePress }) => {
const {
actions: { refreshMeta },
} = useContext(GlobalContext);

const {
onBackPress,
loginRequired,
isTesting,
isBundleHistory,
bucketTitle,
bucketSubtitle,
downloadError,
handleSwitch,
} = useStallionModal();

useEffect(() => {
refreshMeta();
}, [refreshMeta]);

return (
<View style={styles.screen}>
{loginRequired ? (
<View style={styles.loginContainer}>
<AppHeader onClosePress={onClosePress} />
<View style={styles.loginSection}>
<Login />
</View>
</View>
) : (
<DevMenu
isTesting={isTesting}
isBundleHistory={isBundleHistory}
bucketTitle={bucketTitle}
bucketSubtitle={bucketSubtitle}
downloadError={downloadError}
onBackPress={onBackPress}
onClosePress={onClosePress}
onTabChange={handleSwitch}
/>
)}
</View>
);
};

const styles = StyleSheet.create({
// The dev menu takes the whole screen; there is no scrim behind it.
screen: {
flex: 1,
backgroundColor: DS_COLORS.screenBg,
},
// Login is mode-agnostic — you cannot reach the Testing/Production toggle
// until you are signed in — so its header stays untinted.
loginContainer: {
flex: 1,
backgroundColor: DS_COLORS.cardBg,
},
loginSection: {
flex: 1,
borderTopWidth: 1,
borderTopColor: DS_COLORS.headerHairline,
},
});

export default StallionDevMenu;
Loading
Loading