react-native-pdf-jsi 3.4.0 → 4.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 (40) hide show
  1. package/README.md +32 -1
  2. package/android/.gradle/5.6.1/fileChanges/last-build.bin +0 -0
  3. package/android/.gradle/5.6.1/fileHashes/fileHashes.lock +0 -0
  4. package/android/.gradle/5.6.1/gc.properties +0 -0
  5. package/android/.gradle/8.5/checksums/checksums.lock +0 -0
  6. package/android/.gradle/8.5/checksums/md5-checksums.bin +0 -0
  7. package/android/.gradle/8.5/checksums/sha1-checksums.bin +0 -0
  8. package/android/.gradle/8.5/dependencies-accessors/dependencies-accessors.lock +0 -0
  9. package/android/.gradle/8.5/dependencies-accessors/gc.properties +0 -0
  10. package/android/.gradle/8.5/executionHistory/executionHistory.lock +0 -0
  11. package/android/.gradle/8.5/fileChanges/last-build.bin +0 -0
  12. package/android/.gradle/8.5/fileHashes/fileHashes.lock +0 -0
  13. package/android/.gradle/8.5/gc.properties +0 -0
  14. package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
  15. package/android/.gradle/buildOutputCleanup/cache.properties +2 -0
  16. package/android/.gradle/vcs-1/gc.properties +0 -0
  17. package/android/src/main/java/org/wonday/pdf/PdfManager.java +5 -0
  18. package/fabric/RNPDFPdfNativeComponent.js +4 -3
  19. package/index.d.ts +24 -2
  20. package/ios/PERMISSIONS.md +106 -0
  21. package/ios/RNPDFPdf/FileDownloader.h +15 -0
  22. package/ios/RNPDFPdf/FileDownloader.m +567 -0
  23. package/ios/RNPDFPdf/FileManager.h +12 -0
  24. package/ios/RNPDFPdf/FileManager.m +201 -0
  25. package/ios/RNPDFPdf/ImagePool.h +61 -0
  26. package/ios/RNPDFPdf/ImagePool.m +162 -0
  27. package/ios/RNPDFPdf/LazyMetadataLoader.h +78 -0
  28. package/ios/RNPDFPdf/LazyMetadataLoader.m +184 -0
  29. package/ios/RNPDFPdf/MemoryMappedCache.h +71 -0
  30. package/ios/RNPDFPdf/MemoryMappedCache.m +264 -0
  31. package/ios/RNPDFPdf/PDFExporter.h +1 -1
  32. package/ios/RNPDFPdf/PDFExporter.m +475 -19
  33. package/ios/RNPDFPdf/PDFNativeCacheManager.h +11 -1
  34. package/ios/RNPDFPdf/PDFNativeCacheManager.m +283 -19
  35. package/ios/RNPDFPdf/RNPDFPdfView.h +19 -1
  36. package/ios/RNPDFPdf/RNPDFPdfView.mm +154 -44
  37. package/ios/RNPDFPdf/StreamingPDFProcessor.h +86 -0
  38. package/ios/RNPDFPdf/StreamingPDFProcessor.m +314 -0
  39. package/package.json +5 -3
  40. package/src/managers/ExportManager.js +9 -3
@@ -0,0 +1,567 @@
1
+ /**
2
+ * Copyright (c) 2025-present, Punith M (punithm300@gmail.com)
3
+ * FileDownloader for iOS - Downloads files to Documents directory or iCloud Drive
4
+ * All rights reserved.
5
+ *
6
+ * Downloads files to public storage and shows notifications
7
+ */
8
+
9
+ #import "FileDownloader.h"
10
+ #import <React/RCTLog.h>
11
+ #import <React/RCTUtils.h>
12
+ #import <UserNotifications/UserNotifications.h>
13
+ #import <UIKit/UIKit.h>
14
+
15
+ static NSString * const FOLDER_NAME = @"PDFDemoApp";
16
+ static NSString * const NOTIFICATION_IDENTIFIER = @"pdf_exports";
17
+
18
+ @implementation FileDownloader {
19
+ NSURLSession *_downloadSession;
20
+ }
21
+
22
+ RCT_EXPORT_MODULE(FileDownloader);
23
+
24
+ + (BOOL)requiresMainQueueSetup {
25
+ return NO;
26
+ }
27
+
28
+ - (instancetype)init {
29
+ self = [super init];
30
+ if (self) {
31
+ // Configure URL session for downloads
32
+ NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
33
+ config.timeoutIntervalForRequest = 30.0;
34
+ config.timeoutIntervalForResource = 60.0;
35
+ _downloadSession = [NSURLSession sessionWithConfiguration:config];
36
+
37
+ RCTLogInfo(@"📥 FileDownloader initialized for iOS");
38
+ }
39
+ return self;
40
+ }
41
+
42
+ - (NSArray<NSString *> *)supportedEvents {
43
+ return @[@"FileDownloadProgress", @"FileDownloadComplete"];
44
+ }
45
+
46
+ /**
47
+ * Request notification permissions (called when needed, not on init)
48
+ */
49
+ - (void)requestNotificationPermissionsWithCompletion:(void (^)(BOOL granted))completion {
50
+ UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
51
+
52
+ // First check current authorization status
53
+ [center getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings * _Nonnull settings) {
54
+ if (settings.authorizationStatus == UNAuthorizationStatusAuthorized) {
55
+ // Already authorized
56
+ if (completion) {
57
+ completion(YES);
58
+ }
59
+ return;
60
+ }
61
+
62
+ // Request authorization
63
+ [center requestAuthorizationWithOptions:(UNAuthorizationOptionAlert | UNAuthorizationOptionSound | UNAuthorizationOptionBadge)
64
+ completionHandler:^(BOOL granted, NSError * _Nullable error) {
65
+ if (error) {
66
+ RCTLogError(@"❌ Error requesting notification permissions: %@", error.localizedDescription);
67
+ }
68
+
69
+ if (granted) {
70
+ RCTLogInfo(@"📱 [NOTIFICATION] Permission request: GRANTED");
71
+ } else {
72
+ RCTLogInfo(@"⚠️ [NOTIFICATION] Permission request: DENIED");
73
+ if (error) {
74
+ RCTLogError(@"❌ [NOTIFICATION] Permission request error: %@", error.localizedDescription);
75
+ }
76
+ }
77
+
78
+ if (completion) {
79
+ completion(granted);
80
+ }
81
+ }];
82
+ }];
83
+ }
84
+
85
+ /**
86
+ * Download file to Documents/PDFDemoApp folder
87
+ * @param sourcePath Path to source file in app's cache
88
+ * @param fileName Name for the downloaded file
89
+ * @param mimeType MIME type (application/pdf, image/png, image/jpeg)
90
+ * @param promise Promise to resolve with public file path
91
+ */
92
+ RCT_EXPORT_METHOD(downloadToPublicFolder:(NSString *)sourcePath
93
+ fileName:(NSString *)fileName
94
+ mimeType:(NSString *)mimeType
95
+ resolver:(RCTPromiseResolveBlock)resolve
96
+ rejecter:(RCTPromiseRejectBlock)reject) {
97
+
98
+ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
99
+ @try {
100
+ RCTLogInfo(@"📥 [DOWNLOAD] START - file: %@, type: %@", fileName, mimeType);
101
+ RCTLogInfo(@"📁 [SOURCE] %@", sourcePath);
102
+
103
+ // Verify source file exists
104
+ NSFileManager *fileManager = [NSFileManager defaultManager];
105
+ if (![fileManager fileExistsAtPath:sourcePath]) {
106
+ RCTLogError(@"❌ [ERROR] Source file not found: %@", sourcePath);
107
+ reject(@"FILE_NOT_FOUND", @"Source file not found", nil);
108
+ return;
109
+ }
110
+
111
+ NSDictionary *fileAttrs = [fileManager attributesOfItemAtPath:sourcePath error:nil];
112
+ unsigned long long fileSize = [fileAttrs fileSize];
113
+ RCTLogInfo(@"📁 [SOURCE] File exists, size: %llu bytes", fileSize);
114
+
115
+ // Get Documents directory
116
+ NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
117
+ NSString *documentsDirectory = [paths firstObject];
118
+ NSString *appFolder = [documentsDirectory stringByAppendingPathComponent:FOLDER_NAME];
119
+
120
+ // Create folder if needed
121
+ NSError *error;
122
+ if (![fileManager fileExistsAtPath:appFolder]) {
123
+ BOOL created = [fileManager createDirectoryAtPath:appFolder
124
+ withIntermediateDirectories:YES
125
+ attributes:nil
126
+ error:&error];
127
+ if (!created) {
128
+ RCTLogError(@"❌ [ERROR] Failed to create folder: %@", error.localizedDescription);
129
+ reject(@"FOLDER_CREATE_ERROR", @"Failed to create folder", error);
130
+ return;
131
+ }
132
+ RCTLogInfo(@"📁 [FOLDER] Created: %@", appFolder);
133
+ }
134
+
135
+ // Destination path
136
+ NSString *destPath = [appFolder stringByAppendingPathComponent:fileName];
137
+
138
+ // Remove existing file if it exists
139
+ if ([fileManager fileExistsAtPath:destPath]) {
140
+ NSError *removeError;
141
+ BOOL removed = [fileManager removeItemAtPath:destPath error:&removeError];
142
+ if (!removed) {
143
+ RCTLogError(@"❌ [ERROR] Failed to remove existing file: %@", removeError.localizedDescription);
144
+ reject(@"REMOVE_ERROR", @"Failed to remove existing file", removeError);
145
+ return;
146
+ }
147
+ RCTLogInfo(@"📁 [CLEANUP] Removed existing file: %@", fileName);
148
+ }
149
+
150
+ // Emit start event
151
+ @try {
152
+ [self sendEventWithName:@"FileDownloadProgress" body:@{
153
+ @"type": @"downloadStart",
154
+ @"fileName": fileName,
155
+ @"progress": @0.0
156
+ }];
157
+ } @catch (NSException *exception) {
158
+ // Event emitter not ready, continue without event
159
+ }
160
+
161
+ // Copy file
162
+ RCTLogInfo(@"📥 [COPY] Copying file...");
163
+
164
+ // Emit progress event (for file copy, we can estimate)
165
+ @try {
166
+ [self sendEventWithName:@"FileDownloadProgress" body:@{
167
+ @"type": @"downloadProgress",
168
+ @"fileName": fileName,
169
+ @"progress": @0.5,
170
+ @"bytesDownloaded": @(fileSize / 2),
171
+ @"totalBytes": @(fileSize)
172
+ }];
173
+ } @catch (NSException *exception) {
174
+ // Event emitter not ready, continue without event
175
+ }
176
+
177
+ BOOL success = [fileManager copyItemAtPath:sourcePath toPath:destPath error:&error];
178
+
179
+ if (!success) {
180
+ RCTLogError(@"❌ [ERROR] Failed to copy file: %@", error.localizedDescription);
181
+ reject(@"COPY_ERROR", @"Failed to copy file", error);
182
+ return;
183
+ }
184
+
185
+ RCTLogInfo(@"✅ [DOWNLOAD] SUCCESS - %@", destPath);
186
+
187
+ // Emit complete event
188
+ @try {
189
+ [self sendEventWithName:@"FileDownloadComplete" body:@{
190
+ @"type": @"downloadComplete",
191
+ @"fileName": fileName,
192
+ @"path": destPath,
193
+ @"publicPath": destPath,
194
+ @"size": [NSString stringWithFormat:@"%llu", fileSize],
195
+ @"success": @YES
196
+ }];
197
+ } @catch (NSException *exception) {
198
+ // Event emitter not ready, continue without event
199
+ }
200
+
201
+ // Show notification
202
+ [self showDownloadNotification:fileName];
203
+
204
+ resolve(destPath);
205
+
206
+ } @catch (NSException *exception) {
207
+ RCTLogError(@"❌ [DOWNLOAD] ERROR: %@", exception.reason);
208
+ reject(@"DOWNLOAD_ERROR", exception.reason, nil);
209
+ }
210
+ });
211
+ }
212
+
213
+ /**
214
+ * Download file from URL to Documents/PDFDemoApp folder
215
+ * @param url URL to download from
216
+ * @param fileName Name for the downloaded file
217
+ * @param mimeType MIME type
218
+ * @param promise Promise to resolve with downloaded file path
219
+ */
220
+ RCT_EXPORT_METHOD(downloadFile:(NSString *)url
221
+ fileName:(NSString *)fileName
222
+ mimeType:(NSString *)mimeType
223
+ resolver:(RCTPromiseResolveBlock)resolve
224
+ rejecter:(RCTPromiseRejectBlock)reject) {
225
+
226
+ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
227
+ @try {
228
+ RCTLogInfo(@"📥 [DOWNLOAD_URL] START - url: %@", url);
229
+
230
+ // Validate URL
231
+ if (!url || url.length == 0) {
232
+ RCTLogError(@"❌ [DOWNLOAD_URL] Empty URL");
233
+ reject(@"INVALID_URL", @"URL cannot be empty", nil);
234
+ return;
235
+ }
236
+
237
+ // Check for special URL types
238
+ if ([url isEqualToString:@"duplicate-current"]) {
239
+ RCTLogError(@"❌ [DOWNLOAD_URL] Special URL type not handled here");
240
+ reject(@"SPECIAL_URL", @"PDF duplication must be handled in React Native layer", nil);
241
+ return;
242
+ }
243
+
244
+ if ([url isEqualToString:@"custom-url"]) {
245
+ RCTLogError(@"❌ [DOWNLOAD_URL] Custom URL requires user input");
246
+ reject(@"CUSTOM_URL_REQUIRED", @"Please provide a custom URL", nil);
247
+ return;
248
+ }
249
+
250
+ // Validate HTTP/HTTPS URL
251
+ if (![url hasPrefix:@"http://"] && ![url hasPrefix:@"https://"]) {
252
+ RCTLogError(@"❌ [DOWNLOAD_URL] Invalid URL protocol: %@", url);
253
+ reject(@"INVALID_PROTOCOL", @"URL must start with http:// or https://", nil);
254
+ return;
255
+ }
256
+
257
+ // Create cache file first
258
+ NSArray *cachePaths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
259
+ NSString *cacheDirectory = [cachePaths firstObject];
260
+ NSString *cacheFilePath = [cacheDirectory stringByAppendingPathComponent:fileName];
261
+
262
+ // Emit start event
263
+ @try {
264
+ [self sendEventWithName:@"FileDownloadProgress" body:@{
265
+ @"type": @"downloadStart",
266
+ @"fileName": fileName,
267
+ @"url": url,
268
+ @"progress": @0.0
269
+ }];
270
+ } @catch (NSException *exception) {
271
+ // Event emitter not ready, continue without event
272
+ }
273
+
274
+ // Download from URL
275
+ NSURL *downloadURL = [NSURL URLWithString:url];
276
+ NSURLRequest *request = [NSURLRequest requestWithURL:downloadURL];
277
+
278
+ NSURLSessionDownloadTask *downloadTask = [self->_downloadSession downloadTaskWithRequest:request
279
+ completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) {
280
+ if (error) {
281
+ RCTLogError(@"❌ [DOWNLOAD_URL] Network error: %@", error.localizedDescription);
282
+
283
+ // Emit error event
284
+ @try {
285
+ [self sendEventWithName:@"FileDownloadComplete" body:@{
286
+ @"type": @"downloadError",
287
+ @"fileName": fileName,
288
+ @"error": error.localizedDescription,
289
+ @"success": @NO
290
+ }];
291
+ } @catch (NSException *exception) {
292
+ // Event emitter not ready, continue without event
293
+ }
294
+
295
+ reject(@"DOWNLOAD_ERROR", error.localizedDescription, error);
296
+ return;
297
+ }
298
+
299
+ NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
300
+ if (httpResponse.statusCode == 404) {
301
+ RCTLogError(@"❌ [DOWNLOAD_URL] HTTP 404 - File not found");
302
+
303
+ // Emit error event
304
+ @try {
305
+ [self sendEventWithName:@"FileDownloadComplete" body:@{
306
+ @"type": @"downloadError",
307
+ @"fileName": fileName,
308
+ @"error": @"URL not accessible (404)",
309
+ @"success": @NO
310
+ }];
311
+ } @catch (NSException *exception) {
312
+ // Event emitter not ready, continue without event
313
+ }
314
+
315
+ reject(@"FILE_NOT_FOUND", @"URL not accessible (404). The file may have been removed or the URL is incorrect.", nil);
316
+ return;
317
+ } else if (httpResponse.statusCode == 403) {
318
+ RCTLogError(@"❌ [DOWNLOAD_URL] HTTP 403 - Access forbidden");
319
+
320
+ // Emit error event
321
+ @try {
322
+ [self sendEventWithName:@"FileDownloadComplete" body:@{
323
+ @"type": @"downloadError",
324
+ @"fileName": fileName,
325
+ @"error": @"URL not accessible (403)",
326
+ @"success": @NO
327
+ }];
328
+ } @catch (NSException *exception) {
329
+ // Event emitter not ready, continue without event
330
+ }
331
+
332
+ reject(@"ACCESS_FORBIDDEN", @"URL not accessible (403). The server is blocking access.", nil);
333
+ return;
334
+ } else if (httpResponse.statusCode != 200) {
335
+ RCTLogError(@"❌ [DOWNLOAD_URL] HTTP error: %ld", (long)httpResponse.statusCode);
336
+
337
+ // Emit error event
338
+ @try {
339
+ [self sendEventWithName:@"FileDownloadComplete" body:@{
340
+ @"type": @"downloadError",
341
+ @"fileName": fileName,
342
+ @"error": [NSString stringWithFormat:@"HTTP error %ld", (long)httpResponse.statusCode],
343
+ @"success": @NO
344
+ }];
345
+ } @catch (NSException *exception) {
346
+ // Event emitter not ready, continue without event
347
+ }
348
+
349
+ reject(@"DOWNLOAD_FAILED", [NSString stringWithFormat:@"HTTP error %ld", (long)httpResponse.statusCode], nil);
350
+ return;
351
+ }
352
+
353
+ // Emit progress event when download starts
354
+ long long expectedContentLength = httpResponse.expectedContentLength;
355
+ if (expectedContentLength > 0) {
356
+ @try {
357
+ [self sendEventWithName:@"FileDownloadProgress" body:@{
358
+ @"type": @"downloadProgress",
359
+ @"fileName": fileName,
360
+ @"progress": @0.1,
361
+ @"bytesDownloaded": @(expectedContentLength / 10),
362
+ @"totalBytes": @(expectedContentLength)
363
+ }];
364
+ } @catch (NSException *exception) {
365
+ // Event emitter not ready, continue without event
366
+ }
367
+ }
368
+
369
+ // Move downloaded file to cache
370
+ NSFileManager *fileManager = [NSFileManager defaultManager];
371
+ NSError *moveError;
372
+ if ([fileManager fileExistsAtPath:cacheFilePath]) {
373
+ [fileManager removeItemAtPath:cacheFilePath error:nil];
374
+ }
375
+ BOOL moved = [fileManager moveItemAtURL:location toURL:[NSURL fileURLWithPath:cacheFilePath] error:&moveError];
376
+
377
+ if (!moved) {
378
+ RCTLogError(@"❌ [DOWNLOAD_URL] Failed to move file: %@", moveError.localizedDescription);
379
+ reject(@"MOVE_ERROR", @"Failed to move downloaded file", moveError);
380
+ return;
381
+ }
382
+
383
+ RCTLogInfo(@"✅ [DOWNLOAD_URL] Downloaded to cache: %@", cacheFilePath);
384
+
385
+ // Get actual file size
386
+ NSDictionary *fileAttributes = [fileManager attributesOfItemAtPath:cacheFilePath error:nil];
387
+ unsigned long long fileSize = [fileAttributes fileSize];
388
+
389
+ // Emit progress event (download complete, now copying)
390
+ @try {
391
+ [self sendEventWithName:@"FileDownloadProgress" body:@{
392
+ @"type": @"downloadProgress",
393
+ @"fileName": fileName,
394
+ @"progress": @0.9,
395
+ @"bytesDownloaded": @(fileSize),
396
+ @"totalBytes": @(fileSize),
397
+ @"status": @"copying"
398
+ }];
399
+ } @catch (NSException *exception) {
400
+ // Event emitter not ready, continue without event
401
+ }
402
+
403
+ // Now move to Documents/PDFDemoApp folder
404
+ NSArray *documentsPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
405
+ NSString *documentsDirectory = [documentsPaths firstObject];
406
+ NSString *appFolder = [documentsDirectory stringByAppendingPathComponent:FOLDER_NAME];
407
+
408
+ // Create folder if needed
409
+ if (![fileManager fileExistsAtPath:appFolder]) {
410
+ [fileManager createDirectoryAtPath:appFolder
411
+ withIntermediateDirectories:YES
412
+ attributes:nil
413
+ error:nil];
414
+ }
415
+
416
+ NSString *publicPath = [appFolder stringByAppendingPathComponent:fileName];
417
+
418
+ // Copy to public folder
419
+ if ([fileManager fileExistsAtPath:publicPath]) {
420
+ [fileManager removeItemAtPath:publicPath error:nil];
421
+ }
422
+
423
+ BOOL copied = [fileManager copyItemAtPath:cacheFilePath toPath:publicPath error:&moveError];
424
+
425
+ if (!copied) {
426
+ RCTLogError(@"❌ [DOWNLOAD_URL] Failed to copy to public folder: %@", moveError.localizedDescription);
427
+ reject(@"COPY_ERROR", @"Failed to copy to public folder", moveError);
428
+ return;
429
+ }
430
+
431
+ // Reuse fileSize from earlier (same file, just copied)
432
+ NSDictionary *result = @{
433
+ @"path": cacheFilePath,
434
+ @"publicPath": publicPath,
435
+ @"size": [NSString stringWithFormat:@"%llu", fileSize]
436
+ };
437
+
438
+ // Emit complete event
439
+ @try {
440
+ [self sendEventWithName:@"FileDownloadComplete" body:@{
441
+ @"type": @"downloadComplete",
442
+ @"fileName": fileName,
443
+ @"path": cacheFilePath,
444
+ @"publicPath": publicPath,
445
+ @"size": [NSString stringWithFormat:@"%llu", fileSize],
446
+ @"success": @YES
447
+ }];
448
+ } @catch (NSException *exception) {
449
+ // Event emitter not ready, continue without event
450
+ }
451
+
452
+ RCTLogInfo(@"✅ [DOWNLOAD_URL] SUCCESS - Cache: %@, Public: %@", cacheFilePath, publicPath);
453
+ resolve(result);
454
+ }];
455
+
456
+ [downloadTask resume];
457
+
458
+ } @catch (NSException *exception) {
459
+ RCTLogError(@"❌ [DOWNLOAD_URL] ERROR: %@", exception.reason);
460
+ reject(@"DOWNLOAD_ERROR", exception.reason, nil);
461
+ }
462
+ });
463
+ }
464
+
465
+ /**
466
+ * Show notification after successful download
467
+ * Checks permissions before showing
468
+ */
469
+ - (void)showDownloadNotification:(NSString *)fileName {
470
+ UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
471
+
472
+ RCTLogInfo(@"📱 [NOTIFICATION] Checking notification permissions for file: %@", fileName);
473
+
474
+ // Check authorization status before showing notification
475
+ [center getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings * _Nonnull settings) {
476
+ NSString *statusString;
477
+ switch (settings.authorizationStatus) {
478
+ case UNAuthorizationStatusNotDetermined:
479
+ statusString = @"Not Determined";
480
+ break;
481
+ case UNAuthorizationStatusDenied:
482
+ statusString = @"Denied";
483
+ break;
484
+ case UNAuthorizationStatusAuthorized:
485
+ statusString = @"Authorized";
486
+ break;
487
+ case UNAuthorizationStatusProvisional:
488
+ statusString = @"Provisional";
489
+ break;
490
+ case UNAuthorizationStatusEphemeral:
491
+ statusString = @"Ephemeral";
492
+ break;
493
+ default:
494
+ statusString = @"Unknown";
495
+ break;
496
+ }
497
+ RCTLogInfo(@"📱 [NOTIFICATION] Current authorization status: %@", statusString);
498
+
499
+ if (settings.authorizationStatus != UNAuthorizationStatusAuthorized) {
500
+ RCTLogInfo(@"📱 [NOTIFICATION] Permissions not authorized, requesting...");
501
+ // Request permissions if not authorized
502
+ [self requestNotificationPermissionsWithCompletion:^(BOOL granted) {
503
+ if (granted) {
504
+ RCTLogInfo(@"📱 [NOTIFICATION] Permissions granted, displaying notification");
505
+ [self displayNotification:fileName];
506
+ } else {
507
+ RCTLogInfo(@"⚠️ [NOTIFICATION] Cannot show notification - permissions denied by user");
508
+ }
509
+ }];
510
+ } else {
511
+ RCTLogInfo(@"📱 [NOTIFICATION] Permissions already authorized, displaying notification");
512
+ // Already authorized, show notification
513
+ [self displayNotification:fileName];
514
+ }
515
+ }];
516
+ }
517
+
518
+ /**
519
+ * Display the notification (assumes permissions are granted)
520
+ */
521
+ - (void)displayNotification:(NSString *)fileName {
522
+ UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
523
+
524
+ UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init];
525
+ content.title = @"✅ Export Complete";
526
+ content.body = [NSString stringWithFormat:@"1 file(s) saved to Documents/%@", FOLDER_NAME];
527
+ content.sound = [UNNotificationSound defaultSound];
528
+ content.badge = @1;
529
+
530
+ // Add action to open Files app
531
+ UNNotificationAction *openAction = [UNNotificationAction actionWithIdentifier:@"OPEN_FILES"
532
+ title:@"Open Folder"
533
+ options:UNNotificationActionOptionForeground];
534
+ UNNotificationCategory *category = [UNNotificationCategory categoryWithIdentifier:NOTIFICATION_IDENTIFIER
535
+ actions:@[openAction]
536
+ intentIdentifiers:@[]
537
+ options:UNNotificationCategoryOptionNone];
538
+
539
+ [center setNotificationCategories:[NSSet setWithObject:category]];
540
+ content.categoryIdentifier = NOTIFICATION_IDENTIFIER;
541
+
542
+ UNTimeIntervalNotificationTrigger *trigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:1 repeats:NO];
543
+ UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:[NSString stringWithFormat:@"download_%@", fileName]
544
+ content:content
545
+ trigger:trigger];
546
+
547
+ RCTLogInfo(@"📱 [NOTIFICATION] Scheduling notification with identifier: %@", request.identifier);
548
+ RCTLogInfo(@"📱 [NOTIFICATION] Notification content - Title: %@, Body: %@", content.title, content.body);
549
+
550
+ [center addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) {
551
+ if (error) {
552
+ RCTLogError(@"❌ [NOTIFICATION] Failed to show notification: %@", error.localizedDescription);
553
+ RCTLogError(@"❌ [NOTIFICATION] Error domain: %@, code: %ld", error.domain, (long)error.code);
554
+ } else {
555
+ RCTLogInfo(@"✅ [NOTIFICATION] Notification scheduled successfully with identifier: %@", request.identifier);
556
+ RCTLogInfo(@"📱 [NOTIFICATION] Notification should appear in notification center");
557
+ }
558
+ }];
559
+ }
560
+
561
+ - (void)dealloc {
562
+ [_downloadSession invalidateAndCancel];
563
+ RCTLogInfo(@"📥 FileDownloader deallocated");
564
+ }
565
+
566
+ @end
567
+
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Copyright (c) 2025-present, Punith M (punithm300@gmail.com)
3
+ * FileManager for iOS - File operations like opening folders
4
+ * All rights reserved.
5
+ */
6
+
7
+ #import <React/RCTBridgeModule.h>
8
+
9
+ @interface FileManager : NSObject <RCTBridgeModule>
10
+
11
+ @end
12
+