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.
- package/README.md +32 -1
- package/android/.gradle/5.6.1/fileChanges/last-build.bin +0 -0
- package/android/.gradle/5.6.1/fileHashes/fileHashes.lock +0 -0
- package/android/.gradle/5.6.1/gc.properties +0 -0
- package/android/.gradle/8.5/checksums/checksums.lock +0 -0
- package/android/.gradle/8.5/checksums/md5-checksums.bin +0 -0
- package/android/.gradle/8.5/checksums/sha1-checksums.bin +0 -0
- package/android/.gradle/8.5/dependencies-accessors/dependencies-accessors.lock +0 -0
- package/android/.gradle/8.5/dependencies-accessors/gc.properties +0 -0
- package/android/.gradle/8.5/executionHistory/executionHistory.lock +0 -0
- package/android/.gradle/8.5/fileChanges/last-build.bin +0 -0
- package/android/.gradle/8.5/fileHashes/fileHashes.lock +0 -0
- package/android/.gradle/8.5/gc.properties +0 -0
- package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
- package/android/.gradle/buildOutputCleanup/cache.properties +2 -0
- package/android/.gradle/vcs-1/gc.properties +0 -0
- package/android/src/main/java/org/wonday/pdf/PdfManager.java +5 -0
- package/fabric/RNPDFPdfNativeComponent.js +4 -3
- package/index.d.ts +24 -2
- package/ios/PERMISSIONS.md +106 -0
- package/ios/RNPDFPdf/FileDownloader.h +15 -0
- package/ios/RNPDFPdf/FileDownloader.m +567 -0
- package/ios/RNPDFPdf/FileManager.h +12 -0
- package/ios/RNPDFPdf/FileManager.m +201 -0
- package/ios/RNPDFPdf/ImagePool.h +61 -0
- package/ios/RNPDFPdf/ImagePool.m +162 -0
- package/ios/RNPDFPdf/LazyMetadataLoader.h +78 -0
- package/ios/RNPDFPdf/LazyMetadataLoader.m +184 -0
- package/ios/RNPDFPdf/MemoryMappedCache.h +71 -0
- package/ios/RNPDFPdf/MemoryMappedCache.m +264 -0
- package/ios/RNPDFPdf/PDFExporter.h +1 -1
- package/ios/RNPDFPdf/PDFExporter.m +475 -19
- package/ios/RNPDFPdf/PDFNativeCacheManager.h +11 -1
- package/ios/RNPDFPdf/PDFNativeCacheManager.m +283 -19
- package/ios/RNPDFPdf/RNPDFPdfView.h +19 -1
- package/ios/RNPDFPdf/RNPDFPdfView.mm +154 -44
- package/ios/RNPDFPdf/StreamingPDFProcessor.h +86 -0
- package/ios/RNPDFPdf/StreamingPDFProcessor.m +314 -0
- package/package.json +5 -3
- 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
|
+
|