Skip to content
Merged
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
9 changes: 9 additions & 0 deletions cpp/patch_core/state_core.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,15 @@ State Rollback(const State& state) {
return next;
}

State ForgetPurgedVersions(const State& state) {
State next = state;
next.current_version.clear();
next.last_version.clear();
next.first_time = false;
next.first_time_ok = true;
return next;
}

bool ShouldRollbackForBrokenFirstLoad(const State& state) {
return !state.first_time && !state.first_time_ok;
}
Expand Down
6 changes: 6 additions & 0 deletions cpp/patch_core/state_core.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ State ClearRollbackMark(const State& state);

State Rollback(const State& state);

// The OS deleted the installed versions' files (tvOS purges Caches while the
// app is not running): they were not rejected, so unlike Rollback this leaves
// no rolled-back mark that would make the next check skip reinstalling them.
// An existing mark (a rollback earlier in this launch) is kept.
State ForgetPurgedVersions(const State& state);

bool ShouldRollbackForBrokenFirstLoad(const State& state);

LaunchDecision ResolveLaunchState(
Expand Down
26 changes: 26 additions & 0 deletions cpp/patch_core/tests/patch_core_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -863,6 +863,31 @@ void TestStateCoreRollbackToEmptyVersion() {
Expect(rolled.first_time_ok, "first_time_ok should be true after rollback");
}

void TestStateCoreForgetPurgedVersions() {
State state;
state.package_version = "1.0";
state.build_time = "123";
state.current_version = "current";
state.last_version = "previous";
state.first_time = true;
state.first_time_ok = false;

State forgot = pushy::state::ForgetPurgedVersions(state);
Expect(forgot.current_version.empty(), "forget should clear current_version");
Expect(forgot.last_version.empty(), "forget should clear last_version");
Expect(forgot.rolled_back_version.empty(), "forget must not leave a rollback marker");
Expect(!forgot.first_time, "forget should clear first_time");
Expect(forgot.first_time_ok, "forget should reset first_time_ok");
ExpectEq(forgot.package_version, "1.0", "forget should keep package_version");
ExpectEq(forgot.build_time, "123", "forget should keep build_time");

state.rolled_back_version = "rolled";
ExpectEq(
pushy::state::ForgetPurgedVersions(state).rolled_back_version,
"rolled",
"forget should keep an existing rollback marker");
}

void TestStateCoreResolveLaunchNoCurrentVersion() {
State state;
state.current_version = "";
Expand Down Expand Up @@ -1082,6 +1107,7 @@ int main(int argc, char** argv) {
{"ArchivePatchCoreSupportsCustomBundlePatchEntry", TestArchivePatchCoreSupportsCustomBundlePatchEntry},
{"ArchivePatchCoreHarmonyBundlePatchFromPackage", TestArchivePatchCoreHarmonyBundlePatchFromPackage},
{"StateCoreRollbackToEmptyVersion", TestStateCoreRollbackToEmptyVersion},
{"StateCoreForgetPurgedVersions", TestStateCoreForgetPurgedVersions},
{"StateCoreResolveLaunchNoCurrentVersion", TestStateCoreResolveLaunchNoCurrentVersion},
{"StateCoreSwitchToSameVersion", TestStateCoreSwitchToSameVersion},
{"Sha256KnownVectors", TestSha256KnownVectors},
Expand Down
225 changes: 199 additions & 26 deletions ios/RCTPushy/RCTPushy.mm

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@
},
"peerDependencies": {
"react": ">=16.8.0",
"react-native": ">=0.59.0"
"react-native": "*"
},
"homepage": "https://github.com/reactnativecn/react-native-update#readme",
"dependencies": {
Expand Down
19 changes: 15 additions & 4 deletions react-native-update.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@ Pod::Spec.new do |s|
# Silently ignore errors during check
end

# expo-modules-core is a dependency of expo, not of the app: resolve it from
# expo's own location. A bare require from this podspec's directory misses
# it when npm nests it under expo/node_modules (seen with react-native-tvos),
# which left EXPO_SUPPORTS_BUNDLEURL unset; the createBridge fallback that
# selects no longer exists in ExpoModulesCore (SDK 57), so the build failed.
expo_modules_core_package_json_js =
"require.resolve('expo-modules-core/package.json', " \
"{ paths: [require('path').dirname(require.resolve('expo/package.json'))] })"

# Determine final validity by checking Podfile presence AND Expo version
valid_expo_project = false # Default
if is_expo_in_podfile
Expand Down Expand Up @@ -55,7 +64,7 @@ Pod::Spec.new do |s|
# --- Try to find and parse ExpoModulesCore.podspec only if it's an Expo project ---
parsed_expo_ios_target = nil
expo_modules_core_podspec_path = begin
package_json_path = `node -p "require.resolve('expo-modules-core/package.json')"`.strip
package_json_path = `node -p "#{expo_modules_core_package_json_js}"`.strip
File.join(File.dirname(package_json_path), 'ExpoModulesCore.podspec') if $?.success? && package_json_path && !package_json_path.empty?
rescue
nil
Expand All @@ -82,7 +91,10 @@ Pod::Spec.new do |s|
end
end

s.platforms = { :ios => final_ios_deployment_target }
# tvOS (react-native-tvos) shares the iOS sources; the only platform
# differences live behind TARGET_OS_TV in RCTPushy.mm (download directory,
# purged-cache recovery).
s.platforms = { :ios => final_ios_deployment_target, :tvos => final_ios_deployment_target }

s.name = package['name']
s.version = package['version']
Expand Down Expand Up @@ -178,8 +190,7 @@ Pod::Spec.new do |s|

# 1. Try executing node to get the version string
expo_modules_core_version_str = begin
# Use node to directly require expo-modules-core/package.json and get its version
`node --print \"require('expo-modules-core/package.json').version\"` # Execute, keep raw output
`node --print "require(#{expo_modules_core_package_json_js}).version"` # Execute, keep raw output
rescue
# Node command failed (e.g., node not found, package not found). Return empty string.
''
Expand Down
42 changes: 42 additions & 0 deletions src/__tests__/core.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,48 @@ describe('core info parsing', () => {
});
});

describe('client os label', () => {
const mockPlatform = (platform: Record<string, unknown>) => {
mock.module('react-native', () => ({
Platform: platform,
DeviceEventEmitter: {
addListener: mock(() => ({ remove: mock(() => {}) })),
},
NativeModules: {
Pushy: {
currentVersionInfo: '{}',
downloadRootDir: '/tmp',
packageVersion: '1.0.0',
currentVersion: '',
isFirstTime: false,
rolledBackVersion: '',
buildTime: '1',
uuid: 'existing-uuid',
setLocalHashInfo: mock(() => {}),
getLocalHashInfo: mock(() => Promise.resolve('{}')),
setUuid: mock(() => {}),
},
},
NativeEventEmitter: class {
addListener = mock(() => ({ remove: mock(() => {}) }));
},
}));
};

test('labels tvOS apart from iOS', async () => {
mockPlatform({ OS: 'ios', Version: '18.0', isTV: true });
const { cInfo } = await importFreshCore('os-tvos');
expect(cInfo.os).toBe('tvos 18.0');
});

test('keeps the platform name elsewhere, Android TV included', async () => {
mockPlatform({ OS: 'ios', Version: '17.5', isTV: false });
expect((await importFreshCore('os-ios')).cInfo.os).toBe('ios 17.5');
mockPlatform({ OS: 'android', Version: 34, isTV: true });
expect((await importFreshCore('os-androidtv')).cInfo.os).toBe('android 34');
});
});

describe('web platform', () => {
const origDev = (globalThis as any).__DEV__;
afterEach(() => {
Expand Down
11 changes: 11 additions & 0 deletions src/__tests__/metadata.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,17 @@ describe('getUpdateMetadata', () => {
mockCore({ currentVersionInfo: { forceBootRescue: true } });
const forced = await importFreshMetadata('meta-forceboot');
expect(forced.getUpdateMetadata().rescueSource).toBe('forceBoot');

mockCore({ currentVersionInfo: { purgeRestore: true } });
const restored = await importFreshMetadata('meta-purge-restore');
expect(restored.getUpdateMetadata().rescueSource).toBe('purgeRestore');

// One activation can carry several markers; the highest-priority wins.
mockCore({
currentVersionInfo: { forceBootRescue: true, purgeRestore: true },
});
const both = await importFreshMetadata('meta-forceboot-purge');
expect(both.getUpdateMetadata().rescueSource).toBe('forceBoot');
});

test('tolerates the embedded bundle and a rollback launch', async () => {
Expand Down
4 changes: 3 additions & 1 deletion src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,9 @@ if (!uuid) {
export const cInfo = {
rnu: require('../package.json').version,
rn: RNVersion,
os: `${Platform.OS} ${Platform.Version}`,
// tvOS reports Platform.OS 'ios'; label it apart so the server can bucket
// tvOS devices separately. Must match PushyOsName in RCTPushy.mm.
os: `${Platform.OS === 'ios' && Platform.isTV ? 'tvos' : Platform.OS} ${Platform.Version}`,
uuid,
};

Expand Down
11 changes: 8 additions & 3 deletions src/metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,12 @@ export interface UpdateMetadata {
/**
* How the running version got activated when not by the JS flow:
* 'forceBoot' — the server's per-version override applied by the native
* cold-start check; 'crashRescue' — activated by the crash-time rescue.
* cold-start check; 'crashRescue' — activated by the crash-time rescue;
* 'purgeRestore' — reinstalled at launch after tvOS purged the cache.
* Several can apply to one activation (a purge restore of a forceBoot
* version); this reports the highest-priority one, in the order listed.
*/
rescueSource: 'forceBoot' | 'crashRescue' | null;
rescueSource: 'forceBoot' | 'crashRescue' | 'purgeRestore' | null;
/** Stable per-install client id (gray-release bucketing key). */
uuid: string;
os: string;
Expand All @@ -74,7 +77,9 @@ export function getUpdateMetadata(): UpdateMetadata {
? 'forceBoot'
: info.crashRescue
? 'crashRescue'
: null,
: info.purgeRestore
? 'purgeRestore'
: null,
uuid: cInfo.uuid,
os: cInfo.os,
};
Expand Down
Loading