@rejourneyco/react-native 1.0.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 (152) hide show
  1. package/android/build.gradle.kts +135 -0
  2. package/android/consumer-rules.pro +10 -0
  3. package/android/proguard-rules.pro +1 -0
  4. package/android/src/main/AndroidManifest.xml +15 -0
  5. package/android/src/main/java/com/rejourney/RejourneyModuleImpl.kt +2981 -0
  6. package/android/src/main/java/com/rejourney/capture/ANRHandler.kt +206 -0
  7. package/android/src/main/java/com/rejourney/capture/ActivityTracker.kt +98 -0
  8. package/android/src/main/java/com/rejourney/capture/CaptureEngine.kt +1553 -0
  9. package/android/src/main/java/com/rejourney/capture/CaptureHeuristics.kt +375 -0
  10. package/android/src/main/java/com/rejourney/capture/CrashHandler.kt +153 -0
  11. package/android/src/main/java/com/rejourney/capture/MotionEvent.kt +215 -0
  12. package/android/src/main/java/com/rejourney/capture/SegmentUploader.kt +512 -0
  13. package/android/src/main/java/com/rejourney/capture/VideoEncoder.kt +773 -0
  14. package/android/src/main/java/com/rejourney/capture/ViewHierarchyScanner.kt +633 -0
  15. package/android/src/main/java/com/rejourney/capture/ViewSerializer.kt +286 -0
  16. package/android/src/main/java/com/rejourney/core/Constants.kt +117 -0
  17. package/android/src/main/java/com/rejourney/core/Logger.kt +93 -0
  18. package/android/src/main/java/com/rejourney/core/Types.kt +124 -0
  19. package/android/src/main/java/com/rejourney/lifecycle/SessionLifecycleService.kt +162 -0
  20. package/android/src/main/java/com/rejourney/network/DeviceAuthManager.kt +747 -0
  21. package/android/src/main/java/com/rejourney/network/HttpClientProvider.kt +16 -0
  22. package/android/src/main/java/com/rejourney/network/NetworkMonitor.kt +272 -0
  23. package/android/src/main/java/com/rejourney/network/UploadManager.kt +1363 -0
  24. package/android/src/main/java/com/rejourney/network/UploadWorker.kt +492 -0
  25. package/android/src/main/java/com/rejourney/privacy/PrivacyMask.kt +645 -0
  26. package/android/src/main/java/com/rejourney/touch/GestureClassifier.kt +233 -0
  27. package/android/src/main/java/com/rejourney/touch/KeyboardTracker.kt +158 -0
  28. package/android/src/main/java/com/rejourney/touch/TextInputTracker.kt +181 -0
  29. package/android/src/main/java/com/rejourney/touch/TouchInterceptor.kt +591 -0
  30. package/android/src/main/java/com/rejourney/utils/EventBuffer.kt +284 -0
  31. package/android/src/main/java/com/rejourney/utils/OEMDetector.kt +154 -0
  32. package/android/src/main/java/com/rejourney/utils/PerfTiming.kt +235 -0
  33. package/android/src/main/java/com/rejourney/utils/Telemetry.kt +297 -0
  34. package/android/src/main/java/com/rejourney/utils/WindowUtils.kt +84 -0
  35. package/android/src/newarch/java/com/rejourney/RejourneyModule.kt +187 -0
  36. package/android/src/newarch/java/com/rejourney/RejourneyPackage.kt +40 -0
  37. package/android/src/oldarch/java/com/rejourney/RejourneyModule.kt +218 -0
  38. package/android/src/oldarch/java/com/rejourney/RejourneyPackage.kt +23 -0
  39. package/ios/Capture/RJANRHandler.h +42 -0
  40. package/ios/Capture/RJANRHandler.m +328 -0
  41. package/ios/Capture/RJCaptureEngine.h +275 -0
  42. package/ios/Capture/RJCaptureEngine.m +2062 -0
  43. package/ios/Capture/RJCaptureHeuristics.h +80 -0
  44. package/ios/Capture/RJCaptureHeuristics.m +903 -0
  45. package/ios/Capture/RJCrashHandler.h +46 -0
  46. package/ios/Capture/RJCrashHandler.m +313 -0
  47. package/ios/Capture/RJMotionEvent.h +183 -0
  48. package/ios/Capture/RJMotionEvent.m +183 -0
  49. package/ios/Capture/RJPerformanceManager.h +100 -0
  50. package/ios/Capture/RJPerformanceManager.m +373 -0
  51. package/ios/Capture/RJPixelBufferDownscaler.h +42 -0
  52. package/ios/Capture/RJPixelBufferDownscaler.m +85 -0
  53. package/ios/Capture/RJSegmentUploader.h +146 -0
  54. package/ios/Capture/RJSegmentUploader.m +778 -0
  55. package/ios/Capture/RJVideoEncoder.h +247 -0
  56. package/ios/Capture/RJVideoEncoder.m +1036 -0
  57. package/ios/Capture/RJViewControllerTracker.h +73 -0
  58. package/ios/Capture/RJViewControllerTracker.m +508 -0
  59. package/ios/Capture/RJViewHierarchyScanner.h +215 -0
  60. package/ios/Capture/RJViewHierarchyScanner.m +1464 -0
  61. package/ios/Capture/RJViewSerializer.h +119 -0
  62. package/ios/Capture/RJViewSerializer.m +498 -0
  63. package/ios/Core/RJConstants.h +124 -0
  64. package/ios/Core/RJConstants.m +88 -0
  65. package/ios/Core/RJLifecycleManager.h +85 -0
  66. package/ios/Core/RJLifecycleManager.m +308 -0
  67. package/ios/Core/RJLogger.h +61 -0
  68. package/ios/Core/RJLogger.m +211 -0
  69. package/ios/Core/RJTypes.h +176 -0
  70. package/ios/Core/RJTypes.m +66 -0
  71. package/ios/Core/Rejourney.h +64 -0
  72. package/ios/Core/Rejourney.mm +2495 -0
  73. package/ios/Network/RJDeviceAuthManager.h +94 -0
  74. package/ios/Network/RJDeviceAuthManager.m +967 -0
  75. package/ios/Network/RJNetworkMonitor.h +68 -0
  76. package/ios/Network/RJNetworkMonitor.m +267 -0
  77. package/ios/Network/RJRetryManager.h +73 -0
  78. package/ios/Network/RJRetryManager.m +325 -0
  79. package/ios/Network/RJUploadManager.h +267 -0
  80. package/ios/Network/RJUploadManager.m +2296 -0
  81. package/ios/Privacy/RJPrivacyMask.h +163 -0
  82. package/ios/Privacy/RJPrivacyMask.m +922 -0
  83. package/ios/Rejourney.h +63 -0
  84. package/ios/Touch/RJGestureClassifier.h +130 -0
  85. package/ios/Touch/RJGestureClassifier.m +333 -0
  86. package/ios/Touch/RJTouchInterceptor.h +169 -0
  87. package/ios/Touch/RJTouchInterceptor.m +772 -0
  88. package/ios/Utils/RJEventBuffer.h +112 -0
  89. package/ios/Utils/RJEventBuffer.m +358 -0
  90. package/ios/Utils/RJGzipUtils.h +33 -0
  91. package/ios/Utils/RJGzipUtils.m +89 -0
  92. package/ios/Utils/RJKeychainManager.h +48 -0
  93. package/ios/Utils/RJKeychainManager.m +111 -0
  94. package/ios/Utils/RJPerfTiming.h +209 -0
  95. package/ios/Utils/RJPerfTiming.m +264 -0
  96. package/ios/Utils/RJTelemetry.h +92 -0
  97. package/ios/Utils/RJTelemetry.m +320 -0
  98. package/ios/Utils/RJWindowUtils.h +66 -0
  99. package/ios/Utils/RJWindowUtils.m +133 -0
  100. package/lib/commonjs/NativeRejourney.js +40 -0
  101. package/lib/commonjs/components/Mask.js +79 -0
  102. package/lib/commonjs/index.js +1381 -0
  103. package/lib/commonjs/sdk/autoTracking.js +1259 -0
  104. package/lib/commonjs/sdk/constants.js +151 -0
  105. package/lib/commonjs/sdk/errorTracking.js +199 -0
  106. package/lib/commonjs/sdk/index.js +50 -0
  107. package/lib/commonjs/sdk/metricsTracking.js +204 -0
  108. package/lib/commonjs/sdk/navigation.js +151 -0
  109. package/lib/commonjs/sdk/networkInterceptor.js +412 -0
  110. package/lib/commonjs/sdk/utils.js +363 -0
  111. package/lib/commonjs/types/expo-router.d.js +2 -0
  112. package/lib/commonjs/types/index.js +2 -0
  113. package/lib/module/NativeRejourney.js +38 -0
  114. package/lib/module/components/Mask.js +72 -0
  115. package/lib/module/index.js +1284 -0
  116. package/lib/module/sdk/autoTracking.js +1233 -0
  117. package/lib/module/sdk/constants.js +145 -0
  118. package/lib/module/sdk/errorTracking.js +189 -0
  119. package/lib/module/sdk/index.js +12 -0
  120. package/lib/module/sdk/metricsTracking.js +187 -0
  121. package/lib/module/sdk/navigation.js +143 -0
  122. package/lib/module/sdk/networkInterceptor.js +401 -0
  123. package/lib/module/sdk/utils.js +342 -0
  124. package/lib/module/types/expo-router.d.js +2 -0
  125. package/lib/module/types/index.js +2 -0
  126. package/lib/typescript/NativeRejourney.d.ts +147 -0
  127. package/lib/typescript/components/Mask.d.ts +39 -0
  128. package/lib/typescript/index.d.ts +117 -0
  129. package/lib/typescript/sdk/autoTracking.d.ts +204 -0
  130. package/lib/typescript/sdk/constants.d.ts +120 -0
  131. package/lib/typescript/sdk/errorTracking.d.ts +32 -0
  132. package/lib/typescript/sdk/index.d.ts +9 -0
  133. package/lib/typescript/sdk/metricsTracking.d.ts +58 -0
  134. package/lib/typescript/sdk/navigation.d.ts +33 -0
  135. package/lib/typescript/sdk/networkInterceptor.d.ts +47 -0
  136. package/lib/typescript/sdk/utils.d.ts +148 -0
  137. package/lib/typescript/types/index.d.ts +624 -0
  138. package/package.json +102 -0
  139. package/rejourney.podspec +21 -0
  140. package/src/NativeRejourney.ts +165 -0
  141. package/src/components/Mask.tsx +80 -0
  142. package/src/index.ts +1459 -0
  143. package/src/sdk/autoTracking.ts +1373 -0
  144. package/src/sdk/constants.ts +134 -0
  145. package/src/sdk/errorTracking.ts +231 -0
  146. package/src/sdk/index.ts +11 -0
  147. package/src/sdk/metricsTracking.ts +232 -0
  148. package/src/sdk/navigation.ts +157 -0
  149. package/src/sdk/networkInterceptor.ts +440 -0
  150. package/src/sdk/utils.ts +369 -0
  151. package/src/types/expo-router.d.ts +7 -0
  152. package/src/types/index.ts +739 -0
@@ -0,0 +1,112 @@
1
+ //
2
+ // RJEventBuffer.h
3
+ // Rejourney
4
+ //
5
+ // Write-first event buffer that persists events to disk immediately.
6
+ // Industry-standard approach: events are never lost even on force-kill.
7
+ //
8
+ // Copyright (c) 2026 Rejourney
9
+ //
10
+
11
+ #import <Foundation/Foundation.h>
12
+
13
+ NS_ASSUME_NONNULL_BEGIN
14
+
15
+ /**
16
+ * Write-first event buffer for crash-safe event persistence.
17
+ *
18
+ * Events are written to disk immediately when logged, ensuring no data loss
19
+ * even if the app is force-killed. Events are read back for upload on next
20
+ * app launch if not uploaded during the session.
21
+ *
22
+ * File format: JSONL (one JSON object per line) for efficient append
23
+ * operations.
24
+ */
25
+ @interface RJEventBuffer : NSObject
26
+
27
+ /// Session ID this buffer is associated with
28
+ @property(nonatomic, copy, readonly) NSString *sessionId;
29
+
30
+ /// Base directory for pending session data
31
+ @property(nonatomic, copy, readonly) NSString *pendingRootPath;
32
+
33
+ /// Number of events currently buffered on disk
34
+ @property(nonatomic, readonly) NSInteger eventCount;
35
+
36
+ /// Timestamp of the last event written (milliseconds since epoch)
37
+ @property(nonatomic, readonly) NSTimeInterval lastEventTimestamp;
38
+
39
+ #pragma mark - Initialization
40
+
41
+ /**
42
+ * Creates an event buffer for the specified session.
43
+ *
44
+ * @param sessionId The session ID to buffer events for.
45
+ * @param pendingRootPath Base directory for pending session data.
46
+ * @return A new event buffer instance.
47
+ */
48
+ - (instancetype)initWithSessionId:(NSString *)sessionId
49
+ pendingRootPath:(NSString *)pendingRootPath;
50
+
51
+ /// Unavailable. Use initWithSessionId:pendingRootPath: instead.
52
+ - (instancetype)init NS_UNAVAILABLE;
53
+
54
+ #pragma mark - Event Operations
55
+
56
+ /**
57
+ * Appends an event to the buffer, writing immediately to disk.
58
+ * This operation is synchronous and thread-safe.
59
+ *
60
+ * @param event The event dictionary to persist.
61
+ * @return YES if the event was successfully written, NO otherwise.
62
+ */
63
+ - (BOOL)appendEvent:(NSDictionary *)event;
64
+
65
+ /**
66
+ * Appends multiple events to the buffer atomically.
67
+ *
68
+ * @param events Array of event dictionaries to persist.
69
+ * @return YES if all events were successfully written, NO otherwise.
70
+ */
71
+ - (BOOL)appendEvents:(NSArray<NSDictionary *> *)events;
72
+
73
+ /**
74
+ * Reads all buffered events from disk.
75
+ * Used for upload or recovery.
76
+ *
77
+ * @return Array of event dictionaries, or empty array if no events.
78
+ */
79
+ - (NSArray<NSDictionary *> *)readAllEvents;
80
+
81
+ /**
82
+ * Reads events that haven't been uploaded yet (after the given batch number).
83
+ *
84
+ * @param afterBatchNumber Only return events logged after this batch was
85
+ * uploaded.
86
+ * @return Array of event dictionaries.
87
+ */
88
+ - (NSArray<NSDictionary *> *)readEventsAfterBatchNumber:
89
+ (NSInteger)afterBatchNumber;
90
+
91
+ /**
92
+ * Marks events up to the given index as uploaded.
93
+ * This allows incremental uploads without losing state.
94
+ *
95
+ * @param eventIndex The index of the last successfully uploaded event.
96
+ */
97
+ - (void)markEventsUploadedUpToIndex:(NSInteger)eventIndex;
98
+
99
+ /**
100
+ * Clears all buffered events from disk.
101
+ * Call after session is successfully closed.
102
+ */
103
+ - (void)clearAllEvents;
104
+
105
+ /**
106
+ * Returns the timestamp of the last event, useful for session end time.
107
+ */
108
+ - (NSTimeInterval)lastEventTimestampMs;
109
+
110
+ @end
111
+
112
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,358 @@
1
+ //
2
+ // RJEventBuffer.m
3
+ // Rejourney
4
+ //
5
+ // Write-first event buffer implementation.
6
+ // Events are persisted to disk immediately in JSONL format.
7
+ //
8
+ // Copyright (c) 2026 Rejourney
9
+ //
10
+
11
+ #import "RJEventBuffer.h"
12
+ #import "../Core/RJLogger.h"
13
+
14
+ static void *kRJEventBufferQueueKey = &kRJEventBufferQueueKey;
15
+
16
+ @interface RJEventBuffer ()
17
+
18
+ @property(nonatomic, copy, readwrite) NSString *sessionId;
19
+ @property(nonatomic, copy, readwrite) NSString *pendingRootPath;
20
+ @property(nonatomic, strong) NSFileHandle *fileHandle;
21
+ @property(nonatomic, copy) NSString *eventsFilePath;
22
+ @property(nonatomic, assign, readwrite) NSInteger eventCount;
23
+ @property(nonatomic, assign, readwrite) NSTimeInterval lastEventTimestamp;
24
+ @property(nonatomic, strong) dispatch_queue_t writeQueue;
25
+ @property(nonatomic, assign) NSInteger uploadedEventCount;
26
+
27
+ @end
28
+
29
+ @implementation RJEventBuffer
30
+
31
+ #pragma mark - Initialization
32
+
33
+ - (instancetype)initWithSessionId:(NSString *)sessionId
34
+ pendingRootPath:(NSString *)pendingRootPath {
35
+ self = [super init];
36
+ if (self) {
37
+ _sessionId = [sessionId copy];
38
+ if (pendingRootPath.length == 0) {
39
+ NSString *defaultPath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory,
40
+ NSUserDomainMask, YES)
41
+ .firstObject stringByAppendingPathComponent:@"rj_pending"];
42
+ _pendingRootPath = [defaultPath copy];
43
+ } else {
44
+ _pendingRootPath = [pendingRootPath copy];
45
+ }
46
+ _eventCount = 0;
47
+ _lastEventTimestamp = 0;
48
+ _uploadedEventCount = 0;
49
+ _writeQueue = dispatch_queue_create("com.rejourney.eventbuffer",
50
+ DISPATCH_QUEUE_SERIAL);
51
+ dispatch_queue_set_specific(_writeQueue, kRJEventBufferQueueKey,
52
+ kRJEventBufferQueueKey, NULL);
53
+
54
+ [self setupEventsFile];
55
+ }
56
+ return self;
57
+ }
58
+
59
+ - (void)dealloc {
60
+ [self performWriteSync:^{
61
+ [self closeFileHandle];
62
+ }];
63
+ }
64
+
65
+ #pragma mark - File Setup
66
+
67
+ - (void)setupEventsFile {
68
+ NSString *sessionDir =
69
+ [self.pendingRootPath stringByAppendingPathComponent:self.sessionId];
70
+
71
+
72
+ NSFileManager *fm = [NSFileManager defaultManager];
73
+ if (![fm fileExistsAtPath:sessionDir]) {
74
+ NSError *error = nil;
75
+ [fm createDirectoryAtPath:sessionDir
76
+ withIntermediateDirectories:YES
77
+ attributes:nil
78
+ error:&error];
79
+ if (error) {
80
+ RJLogError(@"Failed to create session directory: %@", error);
81
+ return;
82
+ }
83
+ }
84
+
85
+ self.eventsFilePath =
86
+ [sessionDir stringByAppendingPathComponent:@"events.jsonl"];
87
+
88
+ if (![fm fileExistsAtPath:self.eventsFilePath]) {
89
+ NSDictionary *attrs = @{
90
+ NSFileProtectionKey: NSFileProtectionCompleteUntilFirstUserAuthentication
91
+ };
92
+ [fm createFileAtPath:self.eventsFilePath contents:nil attributes:attrs];
93
+ }
94
+
95
+
96
+ NSError *error = nil;
97
+ self.fileHandle =
98
+ [NSFileHandle fileHandleForWritingAtPath:self.eventsFilePath];
99
+ if (!self.fileHandle) {
100
+ RJLogError(@"Failed to open events file for writing: %@",
101
+ self.eventsFilePath);
102
+ return;
103
+ }
104
+
105
+
106
+ if (@available(iOS 13.0, *)) {
107
+ [self.fileHandle seekToEndReturningOffset:nil error:&error];
108
+ } else {
109
+ [self.fileHandle seekToEndOfFile];
110
+ }
111
+
112
+
113
+ [self countExistingEvents];
114
+
115
+ RJLogDebug(@"Event buffer ready: %@ (%ld existing events)",
116
+ self.eventsFilePath, (long)self.eventCount);
117
+ }
118
+
119
+ - (void)countExistingEvents {
120
+ NSError *error = nil;
121
+ NSString *content = [NSString stringWithContentsOfFile:self.eventsFilePath
122
+ encoding:NSUTF8StringEncoding
123
+ error:&error];
124
+ if (error || !content) {
125
+ self.eventCount = 0;
126
+ return;
127
+ }
128
+
129
+ NSArray<NSString *> *lines = [content componentsSeparatedByString:@"\n"];
130
+ NSInteger count = 0;
131
+ NSTimeInterval lastTs = 0;
132
+
133
+ for (NSString *line in lines) {
134
+ if (line.length == 0)
135
+ continue;
136
+
137
+ NSData *data = [line dataUsingEncoding:NSUTF8StringEncoding];
138
+ NSDictionary *event = [NSJSONSerialization JSONObjectWithData:data
139
+ options:0
140
+ error:nil];
141
+ if (event) {
142
+ count++;
143
+ NSNumber *ts = event[@"timestamp"];
144
+ if (ts && [ts doubleValue] > lastTs) {
145
+ lastTs = [ts doubleValue];
146
+ }
147
+ }
148
+ }
149
+
150
+ self.eventCount = count;
151
+ self.lastEventTimestamp = lastTs;
152
+ }
153
+
154
+ - (void)closeFileHandle {
155
+ if (self.fileHandle) {
156
+ if (@available(iOS 13.0, *)) {
157
+ [self.fileHandle closeAndReturnError:nil];
158
+ } else {
159
+ [self.fileHandle closeFile];
160
+ }
161
+ self.fileHandle = nil;
162
+ }
163
+ }
164
+
165
+ #pragma mark - Event Operations
166
+
167
+ - (void)performWriteSync:(dispatch_block_t)block {
168
+ if (!block || !self.writeQueue) {
169
+ return;
170
+ }
171
+
172
+ if (dispatch_get_specific(kRJEventBufferQueueKey)) {
173
+ block();
174
+ } else {
175
+ dispatch_sync(self.writeQueue, block);
176
+ }
177
+ }
178
+
179
+ - (BOOL)appendEvent:(NSDictionary *)event {
180
+ if (!event)
181
+ return NO;
182
+
183
+ __block BOOL success = NO;
184
+
185
+ [self performWriteSync:^{
186
+ success = [self writeEventToDisk:event];
187
+ }];
188
+
189
+ return success;
190
+ }
191
+
192
+ - (BOOL)appendEvents:(NSArray<NSDictionary *> *)events {
193
+ if (!events || events.count == 0)
194
+ return YES;
195
+
196
+ __block BOOL success = YES;
197
+
198
+ [self performWriteSync:^{
199
+ for (NSDictionary *event in events) {
200
+ if (![self writeEventToDisk:event]) {
201
+ success = NO;
202
+ }
203
+ }
204
+ }];
205
+
206
+ return success;
207
+ }
208
+
209
+ - (BOOL)writeEventToDisk:(NSDictionary *)event {
210
+ if (!self.fileHandle) {
211
+ RJLogWarning(@"Event buffer file handle not available");
212
+ return NO;
213
+ }
214
+
215
+ @try {
216
+ NSError *jsonError = nil;
217
+ NSData *jsonData = [NSJSONSerialization dataWithJSONObject:event
218
+ options:0
219
+ error:&jsonError];
220
+ if (jsonError || !jsonData) {
221
+ RJLogWarning(@"Failed to serialize event: %@", jsonError);
222
+ return NO;
223
+ }
224
+
225
+
226
+ NSMutableData *lineData = [jsonData mutableCopy];
227
+ [lineData appendData:[@"\n" dataUsingEncoding:NSUTF8StringEncoding]];
228
+
229
+
230
+ if (@available(iOS 13.4, *)) {
231
+ NSError *writeError = nil;
232
+ [self.fileHandle writeData:lineData error:&writeError];
233
+ if (writeError) {
234
+ RJLogWarning(@"Failed to write event: %@", writeError);
235
+ return NO;
236
+ }
237
+ } else {
238
+ @try {
239
+ [self.fileHandle writeData:lineData];
240
+ } @catch (NSException *e) {
241
+ RJLogWarning(@"Failed to write event: %@", e);
242
+ return NO;
243
+ }
244
+ }
245
+
246
+
247
+ if (@available(iOS 13.0, *)) {
248
+ [self.fileHandle synchronizeAndReturnError:nil];
249
+ } else {
250
+ [self.fileHandle synchronizeFile];
251
+ }
252
+
253
+
254
+ self.eventCount++;
255
+ NSNumber *ts = event[@"timestamp"];
256
+ if (ts) {
257
+ self.lastEventTimestamp = [ts doubleValue];
258
+ }
259
+
260
+ return YES;
261
+
262
+ } @catch (NSException *exception) {
263
+ RJLogError(@"Exception writing event: %@", exception);
264
+ return NO;
265
+ }
266
+ }
267
+
268
+ - (NSArray<NSDictionary *> *)readAllEvents {
269
+ __block NSMutableArray<NSDictionary *> *events = [NSMutableArray new];
270
+
271
+ [self performWriteSync:^{
272
+ NSError *error = nil;
273
+ NSString *content = [NSString stringWithContentsOfFile:self.eventsFilePath
274
+ encoding:NSUTF8StringEncoding
275
+ error:&error];
276
+ if (error || !content) {
277
+ return;
278
+ }
279
+
280
+ NSArray<NSString *> *lines = [content componentsSeparatedByString:@"\n"];
281
+ for (NSString *line in lines) {
282
+ if (line.length == 0)
283
+ continue;
284
+
285
+ NSData *data = [line dataUsingEncoding:NSUTF8StringEncoding];
286
+ NSDictionary *event = [NSJSONSerialization JSONObjectWithData:data
287
+ options:0
288
+ error:nil];
289
+ if (event) {
290
+ [events addObject:event];
291
+ }
292
+ }
293
+ }];
294
+
295
+ return events;
296
+ }
297
+
298
+ - (NSArray<NSDictionary *> *)readEventsAfterBatchNumber:
299
+ (NSInteger)afterBatchNumber {
300
+
301
+ NSArray<NSDictionary *> *allEvents = [self readAllEvents];
302
+
303
+ __block NSInteger uploadedCount = 0;
304
+ [self performWriteSync:^{
305
+ uploadedCount = self.uploadedEventCount;
306
+ }];
307
+
308
+ NSInteger startIndex = MAX(uploadedCount, MAX(0, afterBatchNumber));
309
+ if (startIndex >= allEvents.count) {
310
+ return @[];
311
+ }
312
+
313
+ return [allEvents subarrayWithRange:NSMakeRange(
314
+ startIndex,
315
+ allEvents.count - startIndex)];
316
+ }
317
+
318
+ - (void)markEventsUploadedUpToIndex:(NSInteger)eventIndex {
319
+ [self performWriteSync:^{
320
+ self.uploadedEventCount = eventIndex;
321
+
322
+ NSString *metaPath =
323
+ [[self.eventsFilePath stringByDeletingLastPathComponent]
324
+ stringByAppendingPathComponent:@"buffer_meta.json"];
325
+ NSDictionary *meta = @{
326
+ @"uploadedEventCount" : @(self.uploadedEventCount),
327
+ @"lastEventTimestamp" : @(self.lastEventTimestamp)
328
+ };
329
+ NSData *data = [NSJSONSerialization dataWithJSONObject:meta
330
+ options:0
331
+ error:nil];
332
+ [data writeToFile:metaPath atomically:YES];
333
+ }];
334
+ }
335
+
336
+ - (void)clearAllEvents {
337
+ [self performWriteSync:^{
338
+ [self closeFileHandle];
339
+
340
+ NSFileManager *fm = [NSFileManager defaultManager];
341
+ [fm removeItemAtPath:self.eventsFilePath error:nil];
342
+
343
+ NSString *metaPath =
344
+ [[self.eventsFilePath stringByDeletingLastPathComponent]
345
+ stringByAppendingPathComponent:@"buffer_meta.json"];
346
+ [fm removeItemAtPath:metaPath error:nil];
347
+
348
+ self.eventCount = 0;
349
+ self.uploadedEventCount = 0;
350
+ self.lastEventTimestamp = 0;
351
+ }];
352
+ }
353
+
354
+ - (NSTimeInterval)lastEventTimestampMs {
355
+ return self.lastEventTimestamp;
356
+ }
357
+
358
+ @end
@@ -0,0 +1,33 @@
1
+ //
2
+ // RJGzipUtils.h
3
+ // Rejourney
4
+ //
5
+ // Gzip compression and Base64 decoding utilities.
6
+ //
7
+ // Copyright (c) 2026 Rejourney
8
+ //
9
+
10
+ #import <Foundation/Foundation.h>
11
+
12
+ NS_ASSUME_NONNULL_BEGIN
13
+
14
+ /**
15
+ * Gzip compress data using zlib (with gzip headers).
16
+ *
17
+ * @param input The data to compress.
18
+ * @param error Optional pointer to receive error information.
19
+ * @return The compressed data, or nil on failure.
20
+ */
21
+ NSData *_Nullable RJGzipData(NSData *input, NSError **error);
22
+
23
+ /**
24
+ * Decode base64 data from a data URI or plain base64 string.
25
+ * Handles data URIs (removes "data:...;base64," prefix) and
26
+ * delta prefixes ("delta:...").
27
+ *
28
+ * @param dataString The base64 string to decode.
29
+ * @return The decoded data, or nil on failure.
30
+ */
31
+ NSData *_Nullable RJDecodeBase64Data(NSString *dataString);
32
+
33
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,89 @@
1
+ //
2
+ // RJGzipUtils.m
3
+ // Rejourney
4
+ //
5
+ // Gzip compression and Base64 decoding utilities implementation.
6
+ //
7
+ // Copyright (c) 2026 Rejourney
8
+ //
9
+
10
+ #import "RJGzipUtils.h"
11
+ #import <zlib.h>
12
+
13
+ NSData *_Nullable RJGzipData(NSData *input, NSError **error) {
14
+ if (!input || input.length == 0)
15
+ return input;
16
+
17
+ z_stream stream;
18
+ stream.zalloc = Z_NULL;
19
+ stream.zfree = Z_NULL;
20
+ stream.opaque = Z_NULL;
21
+ stream.next_in = (Bytef *)input.bytes;
22
+ stream.avail_in = (uInt)input.length;
23
+
24
+
25
+ if (deflateInit2(&stream, Z_DEFAULT_COMPRESSION, Z_DEFLATED, 15 + 16, 8,
26
+ Z_DEFAULT_STRATEGY) != Z_OK) {
27
+ if (error) {
28
+ *error =
29
+ [NSError errorWithDomain:@"com.rejourney.gzip"
30
+ code:-1
31
+ userInfo:@{
32
+ NSLocalizedDescriptionKey : @"Failed to init gzip"
33
+ }];
34
+ }
35
+ return nil;
36
+ }
37
+
38
+ NSMutableData *compressed =
39
+ [NSMutableData dataWithLength:input.length * 1.1 + 32];
40
+
41
+ int status;
42
+ do {
43
+ if (stream.total_out >= compressed.length) {
44
+ [compressed increaseLengthBy:input.length / 2];
45
+ }
46
+
47
+ stream.next_out = (Bytef *)compressed.mutableBytes + stream.total_out;
48
+ stream.avail_out = (uInt)(compressed.length - stream.total_out);
49
+
50
+ status = deflate(&stream, Z_FINISH);
51
+ } while (status == Z_OK);
52
+
53
+ if (status != Z_STREAM_END) {
54
+ deflateEnd(&stream);
55
+ if (error) {
56
+ *error =
57
+ [NSError errorWithDomain:@"com.rejourney.gzip"
58
+ code:status
59
+ userInfo:@{
60
+ NSLocalizedDescriptionKey : @"Failed to gzip data"
61
+ }];
62
+ }
63
+ return nil;
64
+ }
65
+
66
+ deflateEnd(&stream);
67
+ [compressed setLength:stream.total_out];
68
+ return compressed;
69
+ }
70
+
71
+ NSData *_Nullable RJDecodeBase64Data(NSString *dataString) {
72
+ if (!dataString || dataString.length == 0)
73
+ return nil;
74
+
75
+ NSString *clean = dataString;
76
+ NSRange comma = [dataString rangeOfString:@","];
77
+ if (comma.location != NSNotFound) {
78
+ clean = [dataString substringFromIndex:comma.location + 1];
79
+ }
80
+
81
+
82
+ if ([clean hasPrefix:@"delta:"]) {
83
+ clean = [clean substringFromIndex:6];
84
+ }
85
+
86
+ return [[NSData alloc]
87
+ initWithBase64EncodedString:clean
88
+ options:NSDataBase64DecodingIgnoreUnknownCharacters];
89
+ }
@@ -0,0 +1,48 @@
1
+ //
2
+ // RJKeychainManager.h
3
+ // Rejourney
4
+ //
5
+ // Secure Keychain storage for sensitive credentials.
6
+ //
7
+ // Licensed under the Apache License, Version 2.0 (the "License");
8
+ // you may not use this file except in compliance with the License.
9
+ // You may obtain a copy of the License at
10
+ //
11
+ // http://www.apache.org/licenses/LICENSE-2.0
12
+ //
13
+ // Unless required by applicable law or agreed to in writing, software
14
+ // distributed under the License is distributed on an "AS IS" BASIS,
15
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ // See the License for the specific language governing permissions and
17
+ // limitations under the License.
18
+ //
19
+ // Copyright (c) 2026 Rejourney
20
+ //
21
+
22
+ #import <Foundation/Foundation.h>
23
+
24
+ NS_ASSUME_NONNULL_BEGIN
25
+
26
+ /**
27
+ * Manages secure storage of sensitive data in iOS Keychain.
28
+ */
29
+ @interface RJKeychainManager : NSObject
30
+
31
+ /// Shared instance
32
+ + (instancetype)sharedManager;
33
+
34
+ /// Store a string value securely
35
+ - (BOOL)setString:(NSString *)value forKey:(NSString *)key;
36
+
37
+ /// Retrieve a string value
38
+ - (nullable NSString *)stringForKey:(NSString *)key;
39
+
40
+ /// Delete a value
41
+ - (BOOL)deleteValueForKey:(NSString *)key;
42
+
43
+ /// Clear all Rejourney keychain items
44
+ - (void)clearAll;
45
+
46
+ @end
47
+
48
+ NS_ASSUME_NONNULL_END