@hot-updater/react-native 0.17.0 → 0.18.1

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 (138) hide show
  1. package/HotUpdater.podspec +7 -11
  2. package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +200 -0
  3. package/android/src/main/java/com/hotupdater/FileManagerService.kt +104 -0
  4. package/android/src/main/java/com/hotupdater/HotUpdater.kt +62 -305
  5. package/android/src/main/java/com/hotupdater/HotUpdaterFactory.kt +49 -0
  6. package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +176 -0
  7. package/android/src/main/java/com/hotupdater/HttpDownloadService.kt +98 -0
  8. package/android/src/main/java/com/hotupdater/VersionedPreferencesService.kt +69 -0
  9. package/android/src/main/java/com/hotupdater/ZipFileUnzipService.kt +52 -0
  10. package/android/src/newarch/HotUpdaterModule.kt +31 -34
  11. package/android/src/oldarch/HotUpdaterModule.kt +32 -34
  12. package/android/src/oldarch/HotUpdaterSpec.kt +2 -9
  13. package/ios/HotUpdater/Internal/BundleFileStorageService.swift +593 -0
  14. package/ios/HotUpdater/Internal/FileManagerService.swift +97 -0
  15. package/ios/HotUpdater/Internal/HotUpdater-Bridging-Header.h +8 -0
  16. package/ios/HotUpdater/Internal/HotUpdater.mm +241 -0
  17. package/ios/HotUpdater/Internal/HotUpdaterFactory.swift +24 -0
  18. package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +143 -0
  19. package/ios/HotUpdater/Internal/NotificationExtension.swift +6 -0
  20. package/ios/HotUpdater/Internal/SSZipArchiveUnzipService.swift +25 -0
  21. package/ios/HotUpdater/Internal/URLSessionDownloadService.swift +101 -0
  22. package/ios/HotUpdater/Internal/VersionedPreferencesService.swift +82 -0
  23. package/ios/HotUpdater/Public/HotUpdater.h +29 -0
  24. package/lib/commonjs/checkForUpdate.js +70 -0
  25. package/lib/commonjs/checkForUpdate.js.map +1 -0
  26. package/lib/commonjs/error.js +14 -0
  27. package/lib/commonjs/error.js.map +1 -0
  28. package/lib/commonjs/fetchUpdateInfo.js +74 -0
  29. package/lib/commonjs/fetchUpdateInfo.js.map +1 -0
  30. package/lib/commonjs/hooks/useEventCallback.js +17 -0
  31. package/lib/commonjs/hooks/useEventCallback.js.map +1 -0
  32. package/lib/commonjs/index.js +234 -0
  33. package/lib/commonjs/index.js.map +1 -0
  34. package/lib/commonjs/native.js +132 -0
  35. package/lib/commonjs/native.js.map +1 -0
  36. package/lib/commonjs/package.json +1 -0
  37. package/lib/commonjs/runUpdateProcess.js +69 -0
  38. package/lib/commonjs/runUpdateProcess.js.map +1 -0
  39. package/lib/commonjs/specs/NativeHotUpdater.js +9 -0
  40. package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -0
  41. package/lib/commonjs/store.js +48 -0
  42. package/lib/commonjs/store.js.map +1 -0
  43. package/lib/commonjs/wrap.js +98 -0
  44. package/lib/commonjs/wrap.js.map +1 -0
  45. package/lib/module/checkForUpdate.js +64 -0
  46. package/lib/module/checkForUpdate.js.map +1 -0
  47. package/lib/module/error.js +9 -0
  48. package/lib/module/error.js.map +1 -0
  49. package/lib/module/fetchUpdateInfo.js +69 -0
  50. package/lib/module/fetchUpdateInfo.js.map +1 -0
  51. package/lib/module/hooks/useEventCallback.js +13 -0
  52. package/lib/module/hooks/useEventCallback.js.map +1 -0
  53. package/lib/module/index.js +211 -0
  54. package/lib/module/index.js.map +1 -0
  55. package/lib/module/native.js +119 -0
  56. package/lib/module/native.js.map +1 -0
  57. package/lib/module/package.json +1 -0
  58. package/lib/module/runUpdateProcess.js +64 -0
  59. package/lib/module/runUpdateProcess.js.map +1 -0
  60. package/lib/module/specs/NativeHotUpdater.js +5 -0
  61. package/lib/module/specs/NativeHotUpdater.js.map +1 -0
  62. package/lib/module/store.js +42 -0
  63. package/lib/module/store.js.map +1 -0
  64. package/lib/module/wrap.js +94 -0
  65. package/lib/module/wrap.js.map +1 -0
  66. package/lib/typescript/commonjs/checkForUpdate.d.ts +22 -0
  67. package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -0
  68. package/{dist → lib/typescript/commonjs}/error.d.ts +1 -0
  69. package/lib/typescript/commonjs/error.d.ts.map +1 -0
  70. package/lib/typescript/commonjs/fetchUpdateInfo.d.ts +4 -0
  71. package/lib/typescript/commonjs/fetchUpdateInfo.d.ts.map +1 -0
  72. package/{dist → lib/typescript/commonjs}/hooks/useEventCallback.d.ts +1 -0
  73. package/lib/typescript/commonjs/hooks/useEventCallback.d.ts.map +1 -0
  74. package/{dist → lib/typescript/commonjs}/index.d.ts +38 -12
  75. package/lib/typescript/commonjs/index.d.ts.map +1 -0
  76. package/lib/typescript/commonjs/native.d.ts +64 -0
  77. package/lib/typescript/commonjs/native.d.ts.map +1 -0
  78. package/lib/typescript/commonjs/package.json +1 -0
  79. package/{dist → lib/typescript/commonjs}/runUpdateProcess.d.ts +1 -0
  80. package/lib/typescript/commonjs/runUpdateProcess.d.ts.map +1 -0
  81. package/{dist → lib/typescript/commonjs}/specs/NativeHotUpdater.d.ts +8 -9
  82. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -0
  83. package/{dist → lib/typescript/commonjs}/store.d.ts +1 -0
  84. package/lib/typescript/commonjs/store.d.ts.map +1 -0
  85. package/{dist → lib/typescript/commonjs}/wrap.d.ts +3 -2
  86. package/lib/typescript/commonjs/wrap.d.ts.map +1 -0
  87. package/lib/typescript/module/checkForUpdate.d.ts +22 -0
  88. package/lib/typescript/module/checkForUpdate.d.ts.map +1 -0
  89. package/lib/typescript/module/error.d.ts +4 -0
  90. package/lib/typescript/module/error.d.ts.map +1 -0
  91. package/lib/typescript/module/fetchUpdateInfo.d.ts +4 -0
  92. package/lib/typescript/module/fetchUpdateInfo.d.ts.map +1 -0
  93. package/lib/typescript/module/hooks/useEventCallback.d.ts +5 -0
  94. package/lib/typescript/module/hooks/useEventCallback.d.ts.map +1 -0
  95. package/lib/typescript/module/index.d.ts +202 -0
  96. package/lib/typescript/module/index.d.ts.map +1 -0
  97. package/lib/typescript/module/native.d.ts +64 -0
  98. package/lib/typescript/module/native.d.ts.map +1 -0
  99. package/lib/typescript/module/package.json +1 -0
  100. package/lib/typescript/module/runUpdateProcess.d.ts +49 -0
  101. package/lib/typescript/module/runUpdateProcess.d.ts.map +1 -0
  102. package/lib/typescript/module/specs/NativeHotUpdater.d.ts +19 -0
  103. package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -0
  104. package/lib/typescript/module/store.d.ts +11 -0
  105. package/lib/typescript/module/store.d.ts.map +1 -0
  106. package/lib/typescript/module/wrap.d.ts +51 -0
  107. package/lib/typescript/module/wrap.d.ts.map +1 -0
  108. package/package.json +59 -30
  109. package/src/checkForUpdate.ts +59 -9
  110. package/src/fetchUpdateInfo.ts +40 -12
  111. package/src/index.ts +37 -11
  112. package/src/native.ts +87 -41
  113. package/src/runUpdateProcess.ts +2 -2
  114. package/src/specs/NativeHotUpdater.ts +8 -10
  115. package/src/wrap.tsx +9 -13
  116. package/android/generated/java/com/hotupdater/NativeHotUpdaterSpec.java +0 -93
  117. package/android/generated/jni/CMakeLists.txt +0 -36
  118. package/android/generated/jni/HotUpdaterSpec-generated.cpp +0 -68
  119. package/android/generated/jni/HotUpdaterSpec.h +0 -31
  120. package/android/generated/jni/react/renderer/components/HotUpdaterSpec/HotUpdaterSpecJSI-generated.cpp +0 -70
  121. package/android/generated/jni/react/renderer/components/HotUpdaterSpec/HotUpdaterSpecJSI.h +0 -121
  122. package/android/src/main/java/com/hotupdater/HotUpdaterPrefs.kt +0 -42
  123. package/dist/checkForUpdate.d.ts +0 -12
  124. package/dist/fetchUpdateInfo.d.ts +0 -3
  125. package/dist/index.js +0 -341
  126. package/dist/index.mjs +0 -301
  127. package/dist/native.d.ts +0 -41
  128. package/ios/HotUpdater/HotUpdater.h +0 -15
  129. package/ios/HotUpdater/HotUpdater.mm +0 -468
  130. package/ios/HotUpdater/HotUpdater.modulemap +0 -6
  131. package/ios/HotUpdater/HotUpdaterPrefs.h +0 -9
  132. package/ios/HotUpdater/HotUpdaterPrefs.mm +0 -45
  133. package/ios/generated/HotUpdaterSpec/HotUpdaterSpec-generated.mm +0 -81
  134. package/ios/generated/HotUpdaterSpec/HotUpdaterSpec.h +0 -112
  135. package/ios/generated/HotUpdaterSpecJSI-generated.cpp +0 -70
  136. package/ios/generated/HotUpdaterSpecJSI.h +0 -121
  137. package/react-native.config.js +0 -12
  138. package/src/global.d.ts +0 -3
@@ -0,0 +1,8 @@
1
+ #ifndef HotUpdater_Bridging_Header_h
2
+ #define HotUpdater_Bridging_Header_h
3
+
4
+ #import "React/RCTBridgeModule.h"
5
+ #import "React/RCTEventEmitter.h"
6
+ #import "React/RCTUtils.h" // Needed for RCTPromiseResolveBlock/RejectBlock in Swift
7
+ #import <SSZipArchive/SSZipArchive.h>
8
+ #endif /* HotUpdater_Bridging_Header_h */
@@ -0,0 +1,241 @@
1
+ #import "HotUpdater.h"
2
+ #import <React/RCTReloadCommand.h>
3
+ #import <React/RCTLog.h>
4
+
5
+
6
+ #if __has_include("HotUpdater/HotUpdater-Swift.h")
7
+ #import "HotUpdater/HotUpdater-Swift.h"
8
+ #else
9
+ #import "HotUpdater-Swift.h"
10
+ #endif
11
+
12
+
13
+ // Define Notification names used for observing Swift Core
14
+ NSNotificationName const HotUpdaterDownloadProgressUpdateNotification = @"HotUpdaterDownloadProgressUpdate";
15
+ NSNotificationName const HotUpdaterDownloadDidFinishNotification = @"HotUpdaterDownloadDidFinish";
16
+
17
+ // Create static HotUpdaterImpl instance
18
+ static HotUpdaterImpl *_hotUpdaterImpl = [HotUpdaterFactory.shared create];
19
+
20
+ @implementation HotUpdater {
21
+ bool hasListeners;
22
+ // Keep track of tasks ONLY for removing observers when this ObjC instance is invalidated
23
+ NSMutableSet<NSURLSessionTask *> *observedTasks; // Changed to NSURLSessionTask for broader compatibility if needed
24
+ }
25
+
26
+ + (BOOL)requiresMainQueueSetup {
27
+ return YES;
28
+ }
29
+
30
+ - (instancetype)init {
31
+ self = [super init];
32
+ if (self) {
33
+ observedTasks = [NSMutableSet set];
34
+
35
+ // Start observing notifications needed for cleanup/events
36
+ // Using self as observer
37
+ [[NSNotificationCenter defaultCenter] addObserver:self
38
+ selector:@selector(handleDownloadProgress:)
39
+ name:HotUpdaterDownloadProgressUpdateNotification
40
+ object:nil]; // Observe all tasks from Impl
41
+ [[NSNotificationCenter defaultCenter] addObserver:self
42
+ selector:@selector(handleDownloadCompletion:)
43
+ name:HotUpdaterDownloadDidFinishNotification
44
+ object:nil]; // Observe all tasks from Impl
45
+
46
+ _lastUpdateTime = 0;
47
+ }
48
+ return self;
49
+ }
50
+
51
+ // Clean up observers when module is invalidated or deallocated
52
+ - (void)invalidate {
53
+ RCTLogInfo(@"[HotUpdater.mm] invalidate called, removing observers.");
54
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
55
+ // Swift side should handle KVO observer removal for its tasks
56
+ [super invalidate];
57
+ }
58
+
59
+ - (void)dealloc {
60
+ RCTLogInfo(@"[HotUpdater.mm] dealloc called, removing observers.");
61
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
62
+ }
63
+
64
+
65
+ RCT_EXPORT_MODULE();
66
+
67
+ #pragma mark - React Native Constants (Keep getMinBundleId, delegate others)
68
+
69
+ // Keep local implementation if complex or uses macros
70
+ - (NSString *)getMinBundleId {
71
+ static NSString *uuid = nil;
72
+ static dispatch_once_t onceToken;
73
+ dispatch_once(&onceToken, ^{
74
+ #if DEBUG
75
+ uuid = @"00000000-0000-0000-0000-000000000000";
76
+ #else
77
+ NSString *compileDateStr = [NSString stringWithFormat:@"%s %s", __DATE__, __TIME__];
78
+ NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
79
+ [formatter setLocale:[[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]];
80
+ [formatter setDateFormat:@"MMM d yyyy HH:mm:ss"]; // Correct format for __DATE__ __TIME__
81
+ NSDate *buildDate = [formatter dateFromString:compileDateStr];
82
+ if (!buildDate) {
83
+ RCTLogWarn(@"[HotUpdater.mm] Could not parse build date: %@", compileDateStr);
84
+ uuid = @"00000000-0000-0000-0000-000000000000";
85
+ return;
86
+ }
87
+ uint64_t buildTimestampMs = (uint64_t)([buildDate timeIntervalSince1970] * 1000.0);
88
+ unsigned char bytes[16];
89
+ bytes[0] = (buildTimestampMs >> 40) & 0xFF; // ... rest of UUID logic ...
90
+ bytes[1] = (buildTimestampMs >> 32) & 0xFF;
91
+ bytes[2] = (buildTimestampMs >> 24) & 0xFF;
92
+ bytes[3] = (buildTimestampMs >> 16) & 0xFF;
93
+ bytes[4] = (buildTimestampMs >> 8) & 0xFF;
94
+ bytes[5] = buildTimestampMs & 0xFF;
95
+ bytes[6] = 0x70; bytes[7] = 0x00; bytes[8] = 0x80; bytes[9] = 0x00;
96
+ bytes[10] = 0x00; bytes[11] = 0x00; bytes[12] = 0x00; bytes[13] = 0x00; bytes[14] = 0x00; bytes[15] = 0x00;
97
+ uuid = [NSString stringWithFormat:
98
+ @"%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x",
99
+ bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
100
+ bytes[8], bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15]];
101
+ #endif
102
+ });
103
+ return uuid;
104
+ }
105
+
106
+
107
+ - (NSDictionary *)constantsToExport {
108
+ return @{
109
+ @"MIN_BUNDLE_ID": [self getMinBundleId] ?: [NSNull null], // Local
110
+ @"APP_VERSION": [HotUpdaterImpl appVersion] ?: [NSNull null], // Swift
111
+ @"CHANNEL": [_hotUpdaterImpl getChannel] ?: [NSNull null] // Swift
112
+ };
113
+ }
114
+
115
+ - (NSDictionary *)getConstants {
116
+ return [self constantsToExport];
117
+ }
118
+
119
+
120
+ // Get bundleURL using static instance
121
+ + (NSURL *)bundleURL {
122
+ return [_hotUpdaterImpl bundleURL];
123
+ }
124
+
125
+
126
+ #pragma mark - Progress Updates & Event Emitting (Keep in ObjC Wrapper)
127
+
128
+ - (void)handleDownloadProgress:(NSNotification *)notification {
129
+ if (!hasListeners) return;
130
+
131
+ NSDictionary *userInfo = notification.userInfo;
132
+ NSNumber *progressNum = userInfo[@"progress"];
133
+
134
+ if (progressNum) {
135
+ double progress = [progressNum doubleValue];
136
+ NSTimeInterval currentTime = [[NSDate date] timeIntervalSince1970] * 1000;
137
+ // Throttle events
138
+ if ((currentTime - self.lastUpdateTime) >= 100 || progress >= 1.0) {
139
+ self.lastUpdateTime = currentTime;
140
+ // RCTLogInfo(@"[HotUpdater.mm] Sending progress event: %.2f", progress); // Reduce log noise
141
+ [self sendEventWithName:@"onProgress" body:@{@"progress": @(progress)}];
142
+ }
143
+ }
144
+ }
145
+
146
+ - (void)handleDownloadCompletion:(NSNotification *)notification {
147
+ NSURLSessionTask *task = notification.object; // Task that finished
148
+ RCTLogInfo(@"[HotUpdater.mm] Received download completion notification for task: %@", task.originalRequest.URL);
149
+ // Swift side handles KVO observer removal internally now when task finishes.
150
+ // No specific cleanup needed here based on this notification anymore.
151
+ }
152
+
153
+
154
+ #pragma mark - React Native Events (Keep as is)
155
+
156
+ - (NSArray<NSString *> *)supportedEvents {
157
+ return @[@"onProgress"];
158
+ }
159
+
160
+ - (void)startObserving {
161
+ hasListeners = YES;
162
+ RCTLogInfo(@"[HotUpdater.mm] Start observing JS events.");
163
+ // Observers are added in init now
164
+ }
165
+
166
+ - (void)stopObserving {
167
+ hasListeners = NO;
168
+ RCTLogInfo(@"[HotUpdater.mm] Stop observing JS events.");
169
+ // Observers are removed in invalidate/dealloc
170
+ }
171
+
172
+ - (void)sendEventWithName:(NSString * _Nonnull)name body:(id)body { // Changed body type to id
173
+ if (hasListeners) {
174
+ [super sendEventWithName:name body:body];
175
+ }
176
+ }
177
+
178
+
179
+ #pragma mark - React Native Exports (Slimmed Down)
180
+
181
+ // Keep reload logic here as it interacts with RN Bridge
182
+ RCT_EXPORT_METHOD(reload) {
183
+ RCTLogInfo(@"[HotUpdater.mm] HotUpdater requested a reload");
184
+ dispatch_async(dispatch_get_main_queue(), ^{
185
+ // Get bundleURL using static instance
186
+ NSURL *bundleURL = [_hotUpdaterImpl bundleURL];
187
+ RCTLogInfo(@"[HotUpdater.mm] Reloading with bundle URL: %@", bundleURL);
188
+ if (bundleURL && super.bridge) {
189
+ @try {
190
+ // This method of setting bundleURL might be outdated depending on RN version.
191
+ // Consider alternatives if this doesn't work reliably.
192
+ [super.bridge setValue:bundleURL forKey:@"bundleURL"];
193
+ } @catch (NSException *exception) {
194
+ RCTLogError(@"[HotUpdater.mm] Failed to set bundleURL on bridge: %@", exception);
195
+ }
196
+ } else if (!super.bridge) {
197
+ RCTLogWarn(@"[HotUpdater.mm] Bridge is nil, cannot set bundleURL for reload.");
198
+ }
199
+ RCTTriggerReloadCommandListeners(@"HotUpdater requested a reload");
200
+ });
201
+ }
202
+
203
+ #ifdef RCT_NEW_ARCH_ENABLED
204
+
205
+ RCT_EXPORT_METHOD(updateBundle:(JS::NativeHotUpdater::UpdateBundleParams &)params
206
+ resolve:(RCTPromiseResolveBlock)resolve
207
+ reject:(RCTPromiseRejectBlock)reject) {
208
+ NSLog(@"[HotUpdater.mm] updateBundle called.");
209
+ NSMutableDictionary *paramDict = [NSMutableDictionary dictionary];
210
+ if (params.bundleId()) {
211
+ paramDict[@"bundleId"] = params.bundleId();
212
+ }
213
+ if (params.fileUrl()) {
214
+ paramDict[@"fileUrl"] = params.fileUrl();
215
+ }
216
+
217
+ [_hotUpdaterImpl updateBundle:paramDict resolver:resolve rejecter:reject];
218
+ }
219
+ #else
220
+ RCT_EXPORT_METHOD(updateBundle:(NSDictionary *)params
221
+ resolve:(RCTPromiseResolveBlock)resolve
222
+ reject:(RCTPromiseRejectBlock)reject) {
223
+ NSLog(@"[HotUpdater.mm] updateBundle called. params: %@", params);
224
+ [_hotUpdaterImpl updateBundle:params resolver:resolve rejecter:reject];
225
+ }
226
+ #endif
227
+
228
+
229
+
230
+
231
+ #pragma mark - Turbo Module Support (Keep as is)
232
+
233
+
234
+ #ifdef RCT_NEW_ARCH_ENABLED
235
+ - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const facebook::react::ObjCTurboModule::InitParams &)params {
236
+ return std::make_shared<facebook::react::NativeHotUpdaterSpecJSI>(params);
237
+ }
238
+ #endif
239
+
240
+
241
+ @end
@@ -0,0 +1,24 @@
1
+ import Foundation
2
+
3
+ @objcMembers
4
+ public class HotUpdaterFactory: NSObject {
5
+ public static let shared = HotUpdaterFactory()
6
+
7
+ private override init() {}
8
+
9
+ public func create() -> HotUpdaterImpl {
10
+ let fileSystem = FileManagerService()
11
+ let preferences = VersionedPreferencesService()
12
+ let downloadService = URLSessionDownloadService()
13
+ let unzipService = SSZipArchiveUnzipService()
14
+
15
+ let bundleStorage = BundleFileStorageService(
16
+ fileSystem: fileSystem,
17
+ downloadService: downloadService,
18
+ unzipService: unzipService,
19
+ preferences: preferences
20
+ )
21
+
22
+ return HotUpdaterImpl(bundleStorage: bundleStorage, preferences: preferences)
23
+ }
24
+ }
@@ -0,0 +1,143 @@
1
+ import Foundation
2
+ import React
3
+
4
+ @objcMembers public class HotUpdaterImpl: NSObject {
5
+ private let bundleStorage: BundleStorageService
6
+ private let preferences: PreferencesService
7
+
8
+ // MARK: - Initialization
9
+
10
+ /**
11
+ * Convenience initializer that creates and configures all dependencies.
12
+ */
13
+ public convenience override init() {
14
+ let fileSystem = FileManagerService()
15
+ let preferences = VersionedPreferencesService()
16
+ let downloadService = URLSessionDownloadService()
17
+ let unzipService = SSZipArchiveUnzipService()
18
+
19
+ let bundleStorage = BundleFileStorageService(
20
+ fileSystem: fileSystem,
21
+ downloadService: downloadService,
22
+ unzipService: unzipService,
23
+ preferences: preferences
24
+ )
25
+
26
+ self.init(bundleStorage: bundleStorage, preferences: preferences)
27
+ }
28
+
29
+ /**
30
+ * Primary initializer with dependency injection.
31
+ * @param bundleStorage Service for bundle storage operations
32
+ * @param preferences Service for preference storage
33
+ */
34
+ internal init(bundleStorage: BundleStorageService, preferences: PreferencesService) {
35
+ self.bundleStorage = bundleStorage
36
+ self.preferences = preferences
37
+ super.init()
38
+
39
+ // Configure preferences with app version
40
+ if let appVersion = HotUpdaterImpl.appVersion {
41
+ (preferences as? VersionedPreferencesService)?.configure(appVersion: appVersion)
42
+ }
43
+ }
44
+
45
+ // MARK: - Static Properties
46
+
47
+ /**
48
+ * Returns the app version from main bundle info.
49
+ */
50
+ public static var appVersion: String? {
51
+ return Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String
52
+ }
53
+
54
+ // MARK: - Channel Management
55
+
56
+ /**
57
+ * Gets the current update channel.
58
+ * @return The channel name or nil if not set
59
+ */
60
+ private static let DEFAULT_CHANNEL = "production"
61
+
62
+ public func getChannel() -> String {
63
+ return Bundle.main.object(forInfoDictionaryKey: "HOT_UPDATER_CHANNEL") as? String ?? Self.DEFAULT_CHANNEL
64
+ }
65
+
66
+
67
+ // MARK: - Bundle URL Management
68
+
69
+ /**
70
+ * Gets the URL to the bundle file.
71
+ * @return URL to the bundle or nil
72
+ */
73
+ public func bundleURL() -> URL? {
74
+ return bundleStorage.getBundleURL()
75
+ }
76
+
77
+ // MARK: - Bundle Update
78
+
79
+ /**
80
+ * Updates the bundle from JavaScript bridge.
81
+ * This method acts as the primary error boundary for all bundle operations.
82
+ * @param params Dictionary with bundleId and fileUrl parameters
83
+ * @param resolve Promise resolve callback
84
+ * @param reject Promise reject callback
85
+ */
86
+ public func updateBundle(_ params: NSDictionary?,
87
+ resolver resolve: @escaping RCTPromiseResolveBlock,
88
+ rejecter reject: @escaping RCTPromiseRejectBlock) {
89
+
90
+ do {
91
+ // Validate parameters (this runs on calling thread - typically JS thread)
92
+ guard let data = params else {
93
+ throw NSError(domain: "HotUpdaterError", code: 101,
94
+ userInfo: [NSLocalizedDescriptionKey: "Missing params dictionary"])
95
+ }
96
+
97
+ guard let bundleId = data["bundleId"] as? String, !bundleId.isEmpty else {
98
+ throw NSError(domain: "HotUpdaterError", code: 102,
99
+ userInfo: [NSLocalizedDescriptionKey: "Missing or empty 'bundleId'"])
100
+ }
101
+
102
+ let fileUrlString = data["fileUrl"] as? String ?? ""
103
+
104
+ var fileUrl: URL? = nil
105
+ if !fileUrlString.isEmpty {
106
+ guard let url = URL(string: fileUrlString) else {
107
+ throw NSError(domain: "HotUpdaterError", code: 103,
108
+ userInfo: [NSLocalizedDescriptionKey: "Invalid 'fileUrl' provided: \(fileUrlString)"])
109
+ }
110
+ fileUrl = url
111
+ }
112
+
113
+ NSLog("[HotUpdaterImpl] updateBundle called with bundleId: \(bundleId), fileUrl: \(fileUrl?.absoluteString ?? "nil")")
114
+
115
+ // Heavy work is delegated to bundle storage service with safe error handling
116
+ bundleStorage.updateBundle(bundleId: bundleId, fileUrl: fileUrl) { [weak self] result in
117
+ guard self != nil else {
118
+ let error = NSError(domain: "HotUpdaterError", code: 998,
119
+ userInfo: [NSLocalizedDescriptionKey: "Self deallocated during update"])
120
+ DispatchQueue.main.async {
121
+ reject("UPDATE_ERROR", error.localizedDescription, error)
122
+ }
123
+ return
124
+ }
125
+ // Return results on main thread for React Native bridge
126
+ DispatchQueue.main.async {
127
+ switch result {
128
+ case .success:
129
+ NSLog("[HotUpdaterImpl] Update successful for \(bundleId). Resolving promise.")
130
+ resolve(true)
131
+ case .failure(let error):
132
+ NSLog("[HotUpdaterImpl] Update failed for \(bundleId): \(error.localizedDescription). Rejecting promise.")
133
+ reject("UPDATE_ERROR", error.localizedDescription, error)
134
+ }
135
+ }
136
+ }
137
+ } catch let error {
138
+ // Main error boundary - catch and convert all errors to JS rejection
139
+ NSLog("[HotUpdaterImpl] Error in updateBundleFromJS: \(error.localizedDescription)")
140
+ reject("UPDATE_ERROR", error.localizedDescription, error)
141
+ }
142
+ }
143
+ }
@@ -0,0 +1,6 @@
1
+ import Foundation
2
+
3
+ extension Notification.Name {
4
+ static let downloadProgressUpdate = Notification.Name("HotUpdaterDownloadProgressUpdate")
5
+ static let downloadDidFinish = Notification.Name("HotUpdaterDownloadDidFinish")
6
+ }
@@ -0,0 +1,25 @@
1
+ import Foundation
2
+ import SSZipArchive
3
+
4
+ protocol UnzipService {
5
+ /**
6
+ * Unzips a file to a destination directory.
7
+ * @param file Path to the zip file
8
+ * @param destination Directory to extract to
9
+ * @throws Error if unzipping fails
10
+ */
11
+ func unzip(file: String, to destination: String) throws
12
+ }
13
+
14
+ class SSZipArchiveUnzipService: UnzipService {
15
+ func unzip(file: String, to destination: String) throws {
16
+ var error: Error?
17
+
18
+ do {
19
+ try SSZipArchive.unzipFile(atPath: file, toDestination: destination, overwrite: true, password: nil)
20
+ } catch let caughtError {
21
+ error = caughtError
22
+ throw error!
23
+ }
24
+ }
25
+ }
@@ -0,0 +1,101 @@
1
+ import Foundation
2
+
3
+ protocol DownloadService {
4
+ /**
5
+ * Downloads a file from a URL.
6
+ * @param url The URL to download from
7
+ * @param destination The local path to save to
8
+ * @param progressHandler Callback for download progress updates
9
+ * @param completion Callback with result of the download
10
+ * @return The download task (optional)
11
+ */
12
+ func downloadFile(from url: URL, to destination: String, progressHandler: @escaping (Double) -> Void, completion: @escaping (Result<URL, Error>) -> Void) -> URLSessionDownloadTask?
13
+ }
14
+
15
+
16
+ class URLSessionDownloadService: NSObject, DownloadService {
17
+ private var session: URLSession!
18
+ private var progressHandlers: [URLSessionTask: (Double) -> Void] = [:]
19
+ private var completionHandlers: [URLSessionTask: (Result<URL, Error>) -> Void] = [:]
20
+ private var destinations: [URLSessionTask: String] = [:]
21
+
22
+ override init() {
23
+ super.init()
24
+ let configuration = URLSessionConfiguration.default
25
+ session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
26
+ }
27
+
28
+ func downloadFile(from url: URL, to destination: String, progressHandler: @escaping (Double) -> Void, completion: @escaping (Result<URL, Error>) -> Void) -> URLSessionDownloadTask? {
29
+ let task = session.downloadTask(with: url)
30
+ progressHandlers[task] = progressHandler
31
+ completionHandlers[task] = completion
32
+ destinations[task] = destination
33
+ task.resume()
34
+ return task
35
+ }
36
+ }
37
+
38
+ extension URLSessionDownloadService: URLSessionDownloadDelegate {
39
+ func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
40
+ let completion = completionHandlers[downloadTask]
41
+ let destination = destinations[downloadTask]
42
+
43
+ defer {
44
+ progressHandlers.removeValue(forKey: downloadTask)
45
+ completionHandlers.removeValue(forKey: downloadTask)
46
+ destinations.removeValue(forKey: downloadTask)
47
+
48
+ // 다운로드 완료 알림
49
+ NotificationCenter.default.post(name: .downloadDidFinish, object: downloadTask)
50
+ }
51
+
52
+ guard let destination = destination else {
53
+ completion?(.failure(NSError(domain: "HotUpdaterError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Destination path not found"])))
54
+ return
55
+ }
56
+
57
+ do {
58
+ let destinationURL = URL(fileURLWithPath: destination)
59
+ try FileManager.default.copyItem(at: location, to: destinationURL)
60
+ completion?(.success(destinationURL))
61
+ } catch {
62
+ NSLog("[DownloadService] Failed to copy downloaded file: \(error.localizedDescription)")
63
+ completion?(.failure(error))
64
+ }
65
+ }
66
+
67
+ func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
68
+ let completion = completionHandlers[task]
69
+ defer {
70
+ progressHandlers.removeValue(forKey: task)
71
+ completionHandlers.removeValue(forKey: task)
72
+ destinations.removeValue(forKey: task)
73
+
74
+ NotificationCenter.default.post(name: .downloadDidFinish, object: task)
75
+ }
76
+
77
+ if let error = error {
78
+ completion?(.failure(error))
79
+ }
80
+ }
81
+
82
+ func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
83
+ let progressHandler = progressHandlers[downloadTask]
84
+
85
+ if totalBytesExpectedToWrite > 0 {
86
+ let progress = Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)
87
+ progressHandler?(progress)
88
+
89
+ let progressInfo: [String: Any] = [
90
+ "progress": progress,
91
+ "totalBytesReceived": totalBytesWritten,
92
+ "totalBytesExpected": totalBytesExpectedToWrite
93
+ ]
94
+ NotificationCenter.default.post(name: .downloadProgressUpdate, object: downloadTask, userInfo: progressInfo)
95
+ } else {
96
+ progressHandler?(0)
97
+
98
+ NotificationCenter.default.post(name: .downloadProgressUpdate, object: downloadTask, userInfo: ["progress": 0.0, "totalBytesReceived": 0, "totalBytesExpected": 0])
99
+ }
100
+ }
101
+ }
@@ -0,0 +1,82 @@
1
+ import Foundation
2
+
3
+ enum PreferencesError: Error {
4
+ case configurationError
5
+ case setItemError(String)
6
+ case getItemError(String)
7
+ }
8
+
9
+ protocol PreferencesService {
10
+ func setItem(_ value: String?, forKey key: String) throws
11
+ func getItem(forKey key: String) throws -> String?
12
+ }
13
+
14
+ class VersionedPreferencesService: PreferencesService {
15
+ private let userDefaults: UserDefaults
16
+ private var keyPrefix: String = ""
17
+
18
+ init(userDefaults: UserDefaults = .standard) {
19
+ self.userDefaults = userDefaults
20
+ }
21
+
22
+ /**
23
+ * Configures the service with app version for key prefixing.
24
+ * @param appVersion The app version to use for key prefixing
25
+ */
26
+ func configure(appVersion: String?) {
27
+ self.keyPrefix = "hotupdater_\(appVersion ?? "unknown")_"
28
+ NSLog("[PreferencesService] Configured with appVersion: \(appVersion ?? "nil"). Key prefix: \(self.keyPrefix)")
29
+ }
30
+
31
+ /**
32
+ * Creates a prefixed key for UserDefaults storage.
33
+ * @param key The base key to prefix
34
+ * @return The prefixed key
35
+ * @throws PreferencesError if configuration is missing
36
+ */
37
+ private func prefixedKey(forKey key: String) throws -> String {
38
+ guard !keyPrefix.isEmpty else {
39
+ NSLog("[PreferencesService] Warning: PreferencesService used before configure(appVersion:) was called. Key prefix is empty.")
40
+ throw PreferencesError.configurationError
41
+ }
42
+ return "\(keyPrefix)\(key)"
43
+ }
44
+
45
+ /**
46
+ * Sets a value in preferences.
47
+ * @param value The value to store (or nil to remove)
48
+ * @param key The key to store under
49
+ * @throws PreferencesError if key prefixing fails
50
+ */
51
+ func setItem(_ value: String?, forKey key: String) throws {
52
+ do {
53
+ let fullKey = try prefixedKey(forKey: key)
54
+ if let valueToSet = value {
55
+ userDefaults.set(valueToSet, forKey: fullKey)
56
+ NSLog("[PreferencesService] Set '\(fullKey)' = '\(valueToSet)'")
57
+ } else {
58
+ userDefaults.removeObject(forKey: fullKey)
59
+ NSLog("[PreferencesService] Removed '\(fullKey)'")
60
+ }
61
+ } catch {
62
+ NSLog("[PreferencesService] Error setting key '\(key)': \(error)")
63
+ throw PreferencesError.setItemError(key)
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Gets a value from preferences.
69
+ * @param key The key to retrieve
70
+ * @return The stored value or nil if not found
71
+ * @throws PreferencesError if key prefixing fails
72
+ */
73
+ func getItem(forKey key: String) throws -> String? {
74
+ do {
75
+ let fullKey = try prefixedKey(forKey: key)
76
+ return userDefaults.string(forKey: fullKey)
77
+ } catch {
78
+ NSLog("[PreferencesService] Error getting key '\(key)': \(error)")
79
+ throw PreferencesError.getItemError(key)
80
+ }
81
+ }
82
+ }
@@ -0,0 +1,29 @@
1
+ #import <React/RCTEventEmitter.h>
2
+ #import <React/RCTBundleURLProvider.h>
3
+
4
+ #ifdef RCT_NEW_ARCH_ENABLED
5
+ #import <HotUpdaterSpec/HotUpdaterSpec.h>
6
+ @interface HotUpdater : RCTEventEmitter <NativeHotUpdaterSpec>
7
+ #else
8
+ #import <React/RCTBridgeModule.h>
9
+ @interface HotUpdater : RCTEventEmitter <RCTBridgeModule>
10
+ #endif // RCT_NEW_ARCH_ENABLED
11
+
12
+ /**
13
+ * Returns the currently active bundle URL.
14
+ * Callable from Objective-C (e.g., AppDelegate).
15
+ * This is implemented in HotUpdater.mm and calls the Swift static method.
16
+ */
17
+ + (NSURL *)bundleURL;
18
+
19
+ /**
20
+ * 다운로드 진행 상황 업데이트 시간을 추적하는 속성
21
+ */
22
+ @property (nonatomic, assign) NSTimeInterval lastUpdateTime;
23
+
24
+ // No need to declare the exported methods (reload, etc.) here
25
+ // as RCT_EXPORT_METHOD handles their exposure to JavaScript.
26
+ // We also don't need to declare supportedEvents or requiresMainQueueSetup here
27
+ // as they are implemented in the .mm file (calling Swift).
28
+
29
+ @end