react-native-update 10.55.0 → 10.56.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/README-CN.md +35 -0
  2. package/README.md +37 -0
  3. package/android/build.gradle +53 -27
  4. package/android/jni/Application.mk +6 -1
  5. package/android/jni/hpatch.c +59 -4
  6. package/android/lib/arm64-v8a/librnupdate.so +0 -0
  7. package/android/lib/armeabi-v7a/librnupdate.so +0 -0
  8. package/android/lib/x86/librnupdate.so +0 -0
  9. package/android/lib/x86_64/librnupdate.so +0 -0
  10. package/android/proguard.pro +70 -14
  11. package/android/src/main/java/cn/reactnative/modules/update/ApkInstaller.java +15 -8
  12. package/android/src/main/java/cn/reactnative/modules/update/ArchiveLimits.java +1 -0
  13. package/android/src/main/java/cn/reactnative/modules/update/BundledResourceCopier.java +1 -2
  14. package/android/src/main/java/cn/reactnative/modules/update/CleanupKeepNames.java +25 -0
  15. package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +98 -34
  16. package/android/src/main/java/cn/reactnative/modules/update/DownloadTaskParams.java +29 -0
  17. package/android/src/main/java/cn/reactnative/modules/update/FileOperationException.java +15 -0
  18. package/android/src/main/java/cn/reactnative/modules/update/HttpUtils.java +59 -0
  19. package/android/src/main/java/cn/reactnative/modules/update/InstallRecord.java +27 -1
  20. package/android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java +57 -16
  21. package/android/src/main/java/cn/reactnative/modules/update/ReactReloadManager.java +38 -46
  22. package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +279 -183
  23. package/android/src/main/java/cn/reactnative/modules/update/UpdateFileUtils.java +14 -0
  24. package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java +222 -215
  25. package/android/src/main/java/cn/reactnative/modules/update/UpdatePackage.java +5 -0
  26. package/android/src/newarch/cn/reactnative/modules/update/UpdateModule.java +23 -25
  27. package/android/src/oldarch/cn/reactnative/modules/update/UpdateModule.java +29 -29
  28. package/cpp/patch_core/archive_limits.h +6 -0
  29. package/cpp/patch_core/jni_util.h +6 -2
  30. package/cpp/patch_core/patch_core.cpp +237 -84
  31. package/cpp/patch_core/patch_core.h +11 -0
  32. package/cpp/patch_core/patch_core_android.cpp +49 -33
  33. package/cpp/patch_core/update_core_android.cpp +134 -103
  34. package/cpp/update_flow_core/flow_json.cpp +150 -17
  35. package/cpp/update_flow_core/flow_json.h +13 -1
  36. package/cpp/update_flow_core/update_flow_core.cpp +78 -8
  37. package/cpp/update_flow_core/update_flow_core.h +9 -0
  38. package/cpp/update_flow_core/update_flow_jni.cpp +44 -25
  39. package/harmony/hvigor-plugin.ts +168 -21
  40. package/harmony/pushy/src/README.md +74 -0
  41. package/harmony/pushy/src/main/cpp/CMakeLists.txt +13 -3
  42. package/harmony/pushy/src/main/cpp/PushyTurboModule.cpp +11 -5
  43. package/harmony/pushy/src/main/cpp/pushy.cpp +103 -18
  44. package/harmony/pushy/src/main/ets/ArchiveLimits.ts +61 -5
  45. package/harmony/pushy/src/main/ets/DownloadTask.ts +255 -166
  46. package/harmony/pushy/src/main/ets/ErrorCodes.ts +75 -0
  47. package/harmony/pushy/src/main/ets/EventHub.ts +22 -20
  48. package/harmony/pushy/src/main/ets/InstallRecord.ts +79 -21
  49. package/harmony/pushy/src/main/ets/Logger.ts +42 -16
  50. package/harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts +98 -17
  51. package/harmony/pushy/src/main/ets/NativePatchCore.ts +10 -3
  52. package/harmony/pushy/src/main/ets/PathUtils.ts +9 -3
  53. package/harmony/pushy/src/main/ets/PushyTurboModule.ts +139 -53
  54. package/harmony/pushy/src/main/ets/UpdateContext.ts +269 -159
  55. package/harmony/pushy/src/main/module.json5 +5 -1
  56. package/harmony/pushy.har +0 -0
  57. package/ios/PrivacyInfo.xcprivacy +79 -0
  58. package/ios/RCTPushy/RCTPushy.mm +406 -170
  59. package/ios/RCTPushy/RCTPushyDownloader.h +14 -0
  60. package/ios/RCTPushy/RCTPushyDownloader.mm +136 -11
  61. package/package.json +14 -24
  62. package/react-native-update.podspec +36 -3
  63. package/src/client.ts +244 -86
  64. package/src/context.ts +40 -27
  65. package/src/core.ts +41 -16
  66. package/src/endpoint.ts +2 -1
  67. package/src/error.ts +49 -11
  68. package/src/index.ts +14 -0
  69. package/src/locales/en.ts +1 -23
  70. package/src/locales/zh.ts +1 -22
  71. package/src/provider.tsx +159 -111
  72. package/src/type.ts +0 -2
  73. package/src/updateFlowCore.ts +10 -2
  74. package/src/utils.ts +61 -4
  75. package/android/src/main/java/cn/reactnative/modules/update/ReactNativeHostHandler.java +0 -13
  76. package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleSupport.java +0 -45
@@ -1,5 +1,19 @@
1
1
  #import <Foundation/Foundation.h>
2
2
 
3
+ // Path of the resume sidecar that records what a partial download at
4
+ // `savePath` belongs to (url + validators + total). Shared with the unzip
5
+ // step, which must drop it together with the archive it vouches for.
6
+ FOUNDATION_EXPORT NSString *RCTPushyResumeSidecarPath(NSString *savePath);
7
+
8
+ // Size of the file at `path` in bytes; -1 when it cannot be read.
9
+ FOUNDATION_EXPORT long long RCTPushyFileSize(NSString *path);
10
+
11
+ // nil when the volume holding `path` can take `bytesToWrite` plus the
12
+ // cpp/patch_core/archive_limits.h safety margin (or the free space is
13
+ // unknown); the reason otherwise. `path` need not exist yet — the check walks
14
+ // up to an existing ancestor.
15
+ FOUNDATION_EXPORT NSString *RCTPushyFreeSpaceShortfall(NSString *path, long long bytesToWrite);
16
+
3
17
  @interface RCTPushyDownloader : NSObject
4
18
 
5
19
  + (void)download:(NSString *)downloadPath savePath:(NSString *)savePath
@@ -1,16 +1,17 @@
1
1
  #import "RCTPushyDownloader.h"
2
+ #include "../../cpp/patch_core/archive_limits.h"
2
3
 
3
4
  static NSString *const RCTPushyDownloaderErrorDomain = @"cn.reactnative.pushy";
4
5
 
5
- // Cross-launch resumable download (NATIVE_CHECKUPDATE_DESIGN §11.4): the
6
- // archive streams straight into savePath (an NSURLSessionDownloadTask's
6
+ // Cross-launch resumable download (NATIVE_CHECKUPDATE_DESIGN §11.4): a data
7
+ // task streams the archive straight into savePath (a download task's
7
8
  // temporary file is discarded on failure, which made every partial byte
8
9
  // worthless), and a sidecar next to it records what the partial belongs to
9
10
  // (url + validators + total). A brick gets a few hundred milliseconds per
10
11
  // launch plus a bounded crash-rescue window, so progress must be monotonic
11
12
  // across process deaths.
12
13
 
13
- static NSString *RCTPushyResumeSidecarPath(NSString *savePath) {
14
+ NSString *RCTPushyResumeSidecarPath(NSString *savePath) {
14
15
  return [savePath stringByAppendingString:@".resume"];
15
16
  }
16
17
 
@@ -35,15 +36,44 @@ static void RCTPushyDeleteResumeSidecar(NSString *savePath) {
35
36
  error:nil];
36
37
  }
37
38
 
38
- static long long RCTPushyFileSize(NSString *path) {
39
+ long long RCTPushyFileSize(NSString *path) {
39
40
  NSDictionary *attributes =
40
41
  [[NSFileManager defaultManager] attributesOfItemAtPath:path error:nil];
41
42
  NSNumber *size = attributes[NSFileSize];
42
43
  return size == nil ? -1 : size.longLongValue;
43
44
  }
44
45
 
46
+ // Free space on the volume holding `path` (walks up to an existing ancestor);
47
+ // -1 when unknown.
48
+ static long long RCTPushyFreeDiskSpaceForPath(NSString *path) {
49
+ NSFileManager *fileManager = [NSFileManager defaultManager];
50
+ NSString *probe = path;
51
+ while (probe.length > 1 && ![fileManager fileExistsAtPath:probe]) {
52
+ probe = [probe stringByDeletingLastPathComponent];
53
+ }
54
+ NSDictionary *attributes = [fileManager attributesOfFileSystemForPath:probe error:nil];
55
+ NSNumber *free = attributes[NSFileSystemFreeSize];
56
+ return free == nil ? -1 : free.longLongValue;
57
+ }
58
+
59
+ NSString *RCTPushyFreeSpaceShortfall(NSString *path, long long bytesToWrite) {
60
+ long long free = RCTPushyFreeDiskSpaceForPath(path);
61
+ if (free < 0) {
62
+ return nil;
63
+ }
64
+ long long needed = MAX(0LL, bytesToWrite) + pushy::archive_limits::kFreeDiskMarginBytes;
65
+ if (free < needed) {
66
+ return [NSString stringWithFormat:@"insufficient disk space: need %lld bytes, have %lld",
67
+ needed, free];
68
+ }
69
+ return nil;
70
+ }
71
+
45
72
  // "bytes <start>-<end>/<total>". Returns the total (0 when "*"), or -1 when
46
- // missing/malformed or the start does not match the local partial.
73
+ // missing/malformed, the start does not match the local partial, or the
74
+ // range and total contradict each other (end before start, or a numeric
75
+ // total not beyond the end — RFC 9110 §14.4). A total of 0 would otherwise
76
+ // pass as "unknown" and skip the final size check.
47
77
  static long long RCTPushyParseContentRange(NSString *header, long long expectedStart) {
48
78
  if (![header hasPrefix:@"bytes "]) {
49
79
  return -1;
@@ -56,16 +86,35 @@ static long long RCTPushyParseContentRange(NSString *header, long long expectedS
56
86
  || dash.location > slash.location) {
57
87
  return -1;
58
88
  }
59
- long long start = [range substringToIndex:dash.location].longLongValue;
60
- if (start != expectedStart) {
89
+ NSCharacterSet *whitespace = [NSCharacterSet whitespaceCharacterSet];
90
+ NSString *startPart = [[range substringToIndex:dash.location]
91
+ stringByTrimmingCharactersInSet:whitespace];
92
+ NSString *endPart = [[range substringWithRange:
93
+ NSMakeRange(dash.location + 1, slash.location - dash.location - 1)]
94
+ stringByTrimmingCharactersInSet:whitespace];
95
+ NSString *totalPart = [[range substringFromIndex:slash.location + 1]
96
+ stringByTrimmingCharactersInSet:whitespace];
97
+ // longLongValue reads 0 for non-numeric text; require real digits.
98
+ NSCharacterSet *digits = [NSCharacterSet decimalDigitCharacterSet];
99
+ if (startPart.length == 0 || endPart.length == 0
100
+ || [startPart rangeOfCharacterFromSet:digits.invertedSet].location != NSNotFound
101
+ || [endPart rangeOfCharacterFromSet:digits.invertedSet].location != NSNotFound) {
102
+ return -1;
103
+ }
104
+ long long start = startPart.longLongValue;
105
+ long long end = endPart.longLongValue;
106
+ if (start != expectedStart || end < start) {
61
107
  return -1;
62
108
  }
63
- NSString *totalPart = [range substringFromIndex:slash.location + 1];
64
109
  if ([totalPart isEqualToString:@"*"]) {
65
110
  return 0;
66
111
  }
112
+ if (totalPart.length == 0
113
+ || [totalPart rangeOfCharacterFromSet:digits.invertedSet].location != NSNotFound) {
114
+ return -1;
115
+ }
67
116
  long long total = totalPart.longLongValue;
68
- return total > 0 ? total : -1;
117
+ return total > end ? total : -1;
69
118
  }
70
119
 
71
120
  @interface RCTPushyDownloader()<NSURLSessionDataDelegate>
@@ -90,8 +139,15 @@ static long long RCTPushyParseContentRange(NSString *header, long long expectedS
90
139
  // NSURLSession delivers decoded bytes, so length accounting against the
91
140
  // encoded Content-Length is meaningless and resume offsets cannot be trusted.
92
141
  @property (nonatomic, assign) BOOL encodedBody;
142
+ // The archive was rejected outright (over archive_limits::kMaxArchiveBytes):
143
+ // nothing on disk is worth resuming, so completion drops the partial and its
144
+ // sidecar instead of leaving them as resume state.
145
+ @property (nonatomic, assign) BOOL discardPartial;
93
146
  @property (nonatomic, assign) int lastReportedPercentage;
94
147
  @property (nonatomic, assign) long long lastReportedBytes;
148
+ // receivedBytes up to which free disk has been probed for (unknown-length
149
+ // bodies; 0 = probe before the first write)
150
+ @property (nonatomic, assign) long long freeSpaceReservedUntil;
95
151
  @end
96
152
 
97
153
  @implementation RCTPushyDownloader
@@ -221,6 +277,10 @@ completionHandler:(void (^)(NSString *path, NSError *error))completionHandler
221
277
  }
222
278
  self.fileHandle = nil;
223
279
  }
280
+ if (self.discardPartial) {
281
+ [[NSFileManager defaultManager] removeItemAtPath:self.savePath error:nil];
282
+ RCTPushyDeleteResumeSidecar(self.savePath);
283
+ }
224
284
 
225
285
  void (^completionHandler)(NSString *, NSError *) = self.completionHandler;
226
286
  self.progressHandler = nil;
@@ -375,12 +435,44 @@ didReceiveResponse:(NSURLResponse *)response
375
435
  self.expectedTotal = self.contentLength > 0 ? self.contentLength : 0;
376
436
  }
377
437
 
438
+ // Caps from archive_limits.h, applied before the first body byte lands
439
+ // (Android does the same): the announced size up front here, streamed
440
+ // bytes in didReceiveData as the backstop for unknown/chunked lengths.
441
+ long long announcedTotal = self.expectedTotal;
442
+ if (announcedTotal <= 0 && !self.encodedBody && self.contentLength > 0) {
443
+ // A 206 whose Content-Range total is "*" still announces this body.
444
+ announcedTotal = self.baseOffset + self.contentLength;
445
+ }
446
+ if (announcedTotal > pushy::archive_limits::kMaxArchiveBytes) {
447
+ [self failWithDescription:[NSString stringWithFormat:@"archive too large: %lld bytes",
448
+ announcedTotal]
449
+ code:-1];
450
+ self.discardPartial = YES;
451
+ completionHandler(NSURLSessionResponseCancel);
452
+ [self completeWithError:self.fileError];
453
+ return;
454
+ }
378
455
  NSFileManager *fileManager = [NSFileManager defaultManager];
379
456
  if (!append) {
380
- // The server ignored the range (or none was sent): start over.
457
+ // The server ignored the range (or none was sent): the stale partial
458
+ // is dead weight either way, so free it before the space check
459
+ // counts it against this download (a full restart must not fail
460
+ // forever on space the partial itself is holding).
381
461
  [fileManager removeItemAtPath:self.savePath error:nil];
462
+ }
463
+ NSString *shortfall = RCTPushyFreeSpaceShortfall(
464
+ self.savePath, announcedTotal > 0 ? announcedTotal - self.baseOffset : 0);
465
+ if (shortfall != nil) {
466
+ // A resumable partial stays: a later attempt may find the space.
467
+ [self failWithDescription:shortfall code:-1];
468
+ completionHandler(NSURLSessionResponseCancel);
469
+ [self completeWithError:self.fileError];
470
+ return;
471
+ }
472
+ if (!append) {
382
473
  [fileManager createFileAtPath:self.savePath contents:nil attributes:nil];
383
474
  }
475
+ self.freeSpaceReservedUntil = 0;
384
476
  self.fileHandle = [NSFileHandle fileHandleForWritingAtPath:self.savePath];
385
477
  if (self.fileHandle == nil) {
386
478
  [self failWithDescription:@"cannot open download file for writing" code:-1];
@@ -418,6 +510,38 @@ didReceiveResponse:(NSURLResponse *)response
418
510
  if (self.fileHandle == nil || self.fileError != nil) {
419
511
  return;
420
512
  }
513
+ if (self.baseOffset + self.receivedBytes + (long long)data.length
514
+ > pushy::archive_limits::kMaxArchiveBytes) {
515
+ // Unknown/chunked length backstop; an announced size was already
516
+ // checked in didReceiveResponse. Checked before the write so the
517
+ // file on disk never exceeds the cap.
518
+ [self failWithDescription:[NSString stringWithFormat:
519
+ @"archive too large: exceeded %lld bytes",
520
+ pushy::archive_limits::kMaxArchiveBytes]
521
+ code:-1];
522
+ self.discardPartial = YES;
523
+ [dataTask cancel];
524
+ return;
525
+ }
526
+ if (self.expectedTotal <= 0
527
+ && self.receivedBytes + (long long)data.length > self.freeSpaceReservedUntil) {
528
+ // Unknown/encoded length: the response-time check could only
529
+ // reserve the margin, so probe before any write that would run past
530
+ // the bytes reserved so far, each probe reserving at least the next
531
+ // PROBE bytes (more when one callback carries a larger buffer) — no
532
+ // write can ever eat into the margin. The archive cap alone (512
533
+ // MiB) is far more than the margin protects. The partial stays: a
534
+ // later attempt may find the space.
535
+ long long reserve = MAX(pushy::archive_limits::kUnknownLengthFreeSpaceProbeBytes,
536
+ (long long)data.length);
537
+ self.freeSpaceReservedUntil = self.receivedBytes + reserve;
538
+ NSString *shortfall = RCTPushyFreeSpaceShortfall(self.savePath, reserve);
539
+ if (shortfall != nil) {
540
+ [self failWithDescription:shortfall code:-1];
541
+ [dataTask cancel];
542
+ return;
543
+ }
544
+ }
421
545
  @try {
422
546
  [self.fileHandle writeData:data];
423
547
  } @catch (NSException *exception) {
@@ -488,7 +612,8 @@ didCompleteWithError:(NSError *)error
488
612
  }
489
613
  }
490
614
  // On failure the partial + sidecar stay on disk — that is the resume
491
- // state a later launch (or crash-rescue window) picks up.
615
+ // state a later launch (or crash-rescue window) picks up — unless the
616
+ // archive was rejected outright (discardPartial).
492
617
  [self completeWithError:finalError];
493
618
  }
494
619
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-update",
3
- "version": "10.55.0",
3
+ "version": "10.56.0",
4
4
  "description": "react-native hot update",
5
5
  "main": "src/index",
6
6
  "types": "src/index.ts",
@@ -8,6 +8,7 @@
8
8
  "src",
9
9
  "!src/__tests__",
10
10
  "android/src",
11
+ "!android/src/test",
11
12
  "android/lib",
12
13
  "android/jni",
13
14
  "!android/jni/HDiffPatch/test",
@@ -32,7 +33,7 @@
32
33
  "prepublishOnly": "NODE_ENV=production bun scripts/prepublish.ts",
33
34
  "publish:local": "SKIP_NATIVE_BUILD=1 npm publish",
34
35
  "prepack": "bun submodule && bun lint",
35
- "lint": "biome check src && tsc --noEmit && node scripts/check-harmony-types.js",
36
+ "lint": "biome check src && tsc --noEmit && node scripts/check-harmony-types.js && node scripts/check-native-spec-parity.js",
36
37
  "lint:harmony": "node scripts/check-harmony-types.js",
37
38
  "submodule": "git submodule update --init --recursive",
38
39
  "test": "bun test src/__tests__",
@@ -42,21 +43,17 @@
42
43
  "build:harmony-har": "node scripts/build-harmony-har.js",
43
44
  "build:harmony-e2e": "bash Example/e2etest/scripts/build-harmony-e2e.sh",
44
45
  "build:so": "bun submodule && bash scripts/build-android-so.sh",
45
- "build:ios-debug": "cd Example/e2etest && bun && detox build --configuration ios.sim.debug",
46
- "build:ios-release": "cd Example/e2etest && bun && detox build --configuration ios.sim.release",
47
- "test:ios-debug": "cd Example/e2etest && E2E_PLATFORM=ios detox test --configuration ios.sim.debug",
48
- "test:ios-release": "cd Example/e2etest && E2E_PLATFORM=ios bun detox test --configuration ios.sim.release",
49
- "build:android-debug": "cd Example/e2etest && bun && detox build --configuration android.emu.debug",
50
- "build:android-release": "cd Example/e2etest && bun && detox build --configuration android.emu.release",
51
- "test:android-release": "cd Example/e2etest && E2E_PLATFORM=android bun detox test --configuration android.emu.release --headless --record-logs all",
52
- "test:android-debug": "cd Example/e2etest && E2E_PLATFORM=android detox test --configuration android.emu.debug --headless --record-logs all",
53
- "smoke:android-debug": "cd Example/e2etest && detox test --configuration android.emu.debug --config e2e/smoke/jest.config.js",
46
+ "build:ios-debug": "cd Example/e2etest && bun && bunx detox build --configuration ios.sim.debug",
47
+ "build:ios-release": "cd Example/e2etest && bun && bunx detox build --configuration ios.sim.release",
48
+ "test:ios-debug": "cd Example/e2etest && E2E_PLATFORM=ios bunx detox test --configuration ios.sim.debug",
49
+ "test:ios-release": "cd Example/e2etest && E2E_PLATFORM=ios bunx detox test --configuration ios.sim.release",
50
+ "build:android-debug": "cd Example/e2etest && bun && bunx detox build --configuration android.emu.debug",
51
+ "build:android-release": "cd Example/e2etest && bun && bunx detox build --configuration android.emu.release",
52
+ "test:android-release": "cd Example/e2etest && E2E_PLATFORM=android bunx detox test --configuration android.emu.release --headless --record-logs all",
53
+ "test:android-debug": "cd Example/e2etest && E2E_PLATFORM=android bunx detox test --configuration android.emu.debug --headless --record-logs all",
54
+ "smoke:android-debug": "cd Example/e2etest && bunx detox test --configuration android.emu.debug --config e2e/smoke/jest.config.js",
54
55
  "e2e:ios": "bun build:ios-release && bun test:ios-release",
55
- "e2e:android": "bun build:android-release && bun test:android-release",
56
- "tests:emulator:prepare": "cd .github/workflows/scripts/functions && bun && bun build",
57
- "tests:emulator:start-ci": "bun tests:emulator:prepare && cd ./.github/workflows/scripts && ./start-firebase-emulator.sh",
58
- "tests:packager:jet-ci": "cd Example/e2etest && cross-env TMPDIR=$HOME/.metro REACT_DEBUGGER=\"echo nope\" node_modules/.bin/react-native start --no-interactive",
59
- "tests:ios:pod:install": "cd Example/e2etest && bun && bun pod-install"
56
+ "e2e:android": "bun build:android-release && bun test:android-release"
60
57
  },
61
58
  "repository": {
62
59
  "type": "git",
@@ -79,8 +76,7 @@
79
76
  },
80
77
  "homepage": "https://github.com/reactnativecn/react-native-update#readme",
81
78
  "dependencies": {
82
- "nanoid": "^3.3.3",
83
- "react-native-url-polyfill": "^3.0.0"
79
+ "nanoid": "^3.3.3"
84
80
  },
85
81
  "codegenConfig": {
86
82
  "name": "RCTPushySpec",
@@ -93,18 +89,12 @@
93
89
  "@react-native/babel-preset": "^0.73.21",
94
90
  "@react-native/typescript-config": "0.84.1",
95
91
  "@types/bun": "^1.3.11",
96
- "@types/jest": "^30.0.0",
97
92
  "@types/node": "^25.5.0",
98
93
  "@types/react": "^19.2.14",
99
94
  "@types/react-test-renderer": "18.0.7",
100
- "detox": "^20.50.1",
101
- "firebase-tools": "^15.11.0",
102
- "jest": "^30.3.0",
103
- "pod-install": "^1.0.14",
104
95
  "react": "18.2.0",
105
96
  "react-native": "0.73",
106
97
  "react-test-renderer": "18.2.0",
107
- "ts-jest": "^29.4.6",
108
98
  "typescript": "^7.0.2"
109
99
  }
110
100
  }
@@ -91,16 +91,49 @@ Pod::Spec.new do |s|
91
91
  s.source = { :git => 'https://github.com/reactnativecn/react-native-update.git', :tag => "#{s.version}" }
92
92
 
93
93
  s.libraries = 'bz2', 'z'
94
+ # No search path into the pod source: every include under ios/ resolves
95
+ # relative to the including file (the cpp/ core via ../../cpp/...), and the
96
+ # former absolute "#{podspec_dir}/ios" entry made the generated xcconfig
97
+ # machine-specific.
94
98
  s.pod_target_xcconfig = {
95
- 'USER_HEADER_SEARCH_PATHS' => "#{podspec_dir}/ios \"$(PODS_ROOT)/Headers/Public/SSZipArchive\" \"$(PODS_ROOT)/Headers/Public/React-Codegen/RCTPushySpec\"",
99
+ 'USER_HEADER_SEARCH_PATHS' => "\"$(PODS_ROOT)/Headers/Public/SSZipArchive\" \"$(PODS_ROOT)/Headers/Public/React-Codegen/RCTPushySpec\"",
96
100
  "DEFINES_MODULE" => "YES"
97
101
  }
102
+ # buildTime for binary-rebuild detection (SyncBinaryVersion) and the check
103
+ # request. The stamp is refreshed on every build and ships as a resource.
104
+ # When the pod source is not writable (read-only checkout or cache) the
105
+ # committed value is kept; +buildTime falls back to the main bundle's
106
+ # Info.plist mtime when the file is missing or empty, so it is never blank.
98
107
  s.resource = 'ios/pushy_build_time.txt'
99
- s.script_phase = { :name => 'Generate build time', :script => "set -x;date +%s > \"#{podspec_dir}/ios/pushy_build_time.txt\"", :execution_position => :before_compile }
108
+ build_time_phase = {
109
+ :name => 'Generate build time',
110
+ :script => "set -x; f=\"${PODS_TARGET_SRCROOT:-#{podspec_dir}}/ios/pushy_build_time.txt\"; if [ -w \"$f\" ] || { [ ! -e \"$f\" ] && [ -w \"$(dirname \"$f\")\" ]; }; then date +%s > \"$f\"; else echo \"warning: $f is not writable, keeping the shipped build time\"; fi",
111
+ :execution_position => :before_compile
112
+ }
113
+ # Declaring the output lets Xcode order the resource copy (into the app, or
114
+ # into this pod's framework under use_frameworks!) after the stamp is
115
+ # written and stops the output-less-phase warning. A phase with outputs and
116
+ # no inputs is otherwise skipped once the file exists — and it is committed
117
+ # — so the phase must also be marked always out of date to keep refreshing
118
+ # the stamp every build. CocoaPods < 1.11 has no such key; there the phase
119
+ # stays output-less (runs every build, as before).
120
+ if defined?(Pod::Specification::DSL::SCRIPT_PHASE_OPTIONAL_KEYS) &&
121
+ Pod::Specification::DSL::SCRIPT_PHASE_OPTIONAL_KEYS.include?(:always_out_of_date)
122
+ build_time_phase[:output_files] = ['$(PODS_TARGET_SRCROOT)/ios/pushy_build_time.txt']
123
+ build_time_phase[:always_out_of_date] = '1'
124
+ end
125
+ s.script_phase = build_time_phase
126
+ # Privacy manifest (required-reason APIs: disk space, file timestamps,
127
+ # system boot time, user defaults) delivered as its own resource bundle so
128
+ # static and dynamic integrations both carry it.
129
+ s.resource_bundles = { 'react-native-update_privacy' => ['ios/PrivacyInfo.xcprivacy'] }
100
130
 
101
131
  s.dependency 'React'
102
132
  s.dependency "React-Core"
103
- s.dependency 'SSZipArchive'
133
+ # The unzip guard relies on the 2.x delegate contract
134
+ # (zipArchiveShouldUnzipFileAtIndex:... cancels, _sanitizedPath); a future
135
+ # major could change it silently.
136
+ s.dependency 'SSZipArchive', '~> 2.4'
104
137
 
105
138
  # Conditionally add Expo dependency
106
139
  if valid_expo_project