motorinc-gallery-picker-pro 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.
@@ -0,0 +1,876 @@
1
+ #import "ImagePickerModule.h"
2
+ #import <React/RCTLog.h>
3
+ #import <React/RCTUtils.h>
4
+ #import <React/RCTBridge.h>
5
+ #import <Photos/Photos.h>
6
+ #import <AVFoundation/AVFoundation.h>
7
+ #import <objc/message.h>
8
+ #import <PhotosUI/PhotosUI.h>
9
+
10
+ // Forward declare the method to avoid compiler warnings - remove this as it might be interfering
11
+ // @interface PHPhotoLibrary (LimitedLibraryPicker)
12
+ // - (void)presentLimitedLibraryPickerFromViewController:(UIViewController *)controller API_AVAILABLE(ios(14));
13
+ // @end
14
+
15
+ @interface ImagePickerModule() <PHPhotoLibraryChangeObserver>
16
+ @property (nonatomic, strong) RCTPromiseResolveBlock resolve;
17
+ @property (nonatomic, strong) RCTPromiseRejectBlock reject;
18
+ @property (nonatomic, strong) NSDictionary *options;
19
+ @end
20
+
21
+ @implementation ImagePickerModule
22
+
23
+ RCT_EXPORT_MODULE(ImagePickerModule);
24
+
25
+ + (BOOL)requiresMainQueueSetup
26
+ {
27
+ return YES;
28
+ }
29
+
30
+ - (NSArray<NSString *> *)supportedEvents
31
+ {
32
+ return @[@"PhotoLibraryChanged"];
33
+ }
34
+
35
+ - (instancetype)init
36
+ {
37
+ self = [super init];
38
+ if (self) {
39
+ // Register for photo library changes
40
+ [[PHPhotoLibrary sharedPhotoLibrary] registerChangeObserver:self];
41
+ }
42
+ return self;
43
+ }
44
+
45
+ - (void)dealloc
46
+ {
47
+ [[PHPhotoLibrary sharedPhotoLibrary] unregisterChangeObserver:self];
48
+ }
49
+
50
+ RCT_EXPORT_METHOD(openCamera:(NSDictionary *)options
51
+ resolver:(RCTPromiseResolveBlock)resolve
52
+ rejecter:(RCTPromiseRejectBlock)reject)
53
+ {
54
+ dispatch_async(dispatch_get_main_queue(), ^{
55
+ self.resolve = resolve;
56
+ self.reject = reject;
57
+ self.options = options;
58
+
59
+ [self requestCameraPermissionWithCompletion:^(BOOL granted) {
60
+ if (granted) {
61
+ [self presentImagePickerWithSourceType:UIImagePickerControllerSourceTypeCamera];
62
+ } else {
63
+ reject(@"PERMISSION_DENIED", @"Camera permission not granted", nil);
64
+ }
65
+ }];
66
+ });
67
+ }
68
+
69
+ RCT_EXPORT_METHOD(openGallery:(NSDictionary *)options
70
+ resolver:(RCTPromiseResolveBlock)resolve
71
+ rejecter:(RCTPromiseRejectBlock)reject)
72
+ {
73
+ dispatch_async(dispatch_get_main_queue(), ^{
74
+ self.resolve = resolve;
75
+ self.reject = reject;
76
+ self.options = options;
77
+
78
+ [self requestGalleryPermissionWithCompletion:^(BOOL granted) {
79
+ if (granted) {
80
+ [self presentImagePickerWithSourceType:UIImagePickerControllerSourceTypePhotoLibrary];
81
+ } else {
82
+ reject(@"PERMISSION_DENIED", @"Photo library permission not granted", nil);
83
+ }
84
+ }];
85
+ });
86
+ }
87
+
88
+ RCT_EXPORT_METHOD(openMultiSelectGallery:(NSDictionary *)options
89
+ resolver:(RCTPromiseResolveBlock)resolve
90
+ rejecter:(RCTPromiseRejectBlock)reject)
91
+ {
92
+ dispatch_async(dispatch_get_main_queue(), ^{
93
+ NSInteger maxSelectionLimit = options[@"maxSelectionLimit"] ? [options[@"maxSelectionLimit"] integerValue] : 10;
94
+ BOOL includeBase64 = options[@"includeBase64"] ? [options[@"includeBase64"] boolValue] : YES;
95
+ CGFloat quality = options[@"quality"] ? [options[@"quality"] floatValue] : 0.8;
96
+
97
+ // Store for later use
98
+ NSMutableDictionary *multiSelectOptions = [NSMutableDictionary dictionaryWithDictionary:options];
99
+ multiSelectOptions[@"maxSelectionLimit"] = @(maxSelectionLimit);
100
+ multiSelectOptions[@"includeBase64"] = @(includeBase64);
101
+ multiSelectOptions[@"quality"] = @(quality);
102
+ multiSelectOptions[@"isMultiSelect"] = @(YES);
103
+ self.options = multiSelectOptions;
104
+
105
+ // Store the promise for multi-select
106
+ self.resolve = resolve;
107
+ self.reject = reject;
108
+
109
+ [self requestGalleryPermissionWithCompletion:^(BOOL granted) {
110
+ if (granted) {
111
+ [self presentMultiSelectPicker:maxSelectionLimit];
112
+ } else {
113
+ reject(@"PERMISSION_DENIED", @"Photo library permission not granted", nil);
114
+ }
115
+ }];
116
+ });
117
+ }
118
+
119
+ RCT_EXPORT_METHOD(requestCameraPermission:(RCTPromiseResolveBlock)resolve
120
+ rejecter:(RCTPromiseRejectBlock)reject)
121
+ {
122
+ [self requestCameraPermissionWithCompletion:^(BOOL granted) {
123
+ resolve(@(granted));
124
+ }];
125
+ }
126
+
127
+ RCT_EXPORT_METHOD(requestGalleryPermission:(RCTPromiseResolveBlock)resolve
128
+ rejecter:(RCTPromiseRejectBlock)reject)
129
+ {
130
+ dispatch_async(dispatch_get_main_queue(), ^{
131
+ [self requestGalleryPermissionWithCompletion:^(BOOL granted) {
132
+ resolve(@(granted));
133
+ }];
134
+ });
135
+ }
136
+
137
+ RCT_EXPORT_METHOD(getPhotoLibraryPermissionStatus:(RCTPromiseResolveBlock)resolve
138
+ rejecter:(RCTPromiseRejectBlock)reject)
139
+ {
140
+ if (@available(iOS 14, *)) {
141
+ // For iOS 14+, check both read-write and add-only access levels
142
+ PHAuthorizationStatus readWriteStatus = [PHPhotoLibrary authorizationStatusForAccessLevel:PHAccessLevelReadWrite];
143
+ PHAuthorizationStatus addOnlyStatus = [PHPhotoLibrary authorizationStatusForAccessLevel:PHAccessLevelAddOnly];
144
+
145
+ NSLog(@"📸 ReadWrite status: %ld, AddOnly status: %ld", (long)readWriteStatus, (long)addOnlyStatus);
146
+
147
+ // Use readWrite status as primary
148
+ PHAuthorizationStatus status = readWriteStatus;
149
+ NSString *statusString;
150
+
151
+ switch (status) {
152
+ case PHAuthorizationStatusAuthorized:
153
+ statusString = @"authorized";
154
+ NSLog(@"📸 Status: authorized (full access)");
155
+ break;
156
+ case PHAuthorizationStatusDenied:
157
+ statusString = @"denied";
158
+ NSLog(@"📸 Status: denied");
159
+ break;
160
+ case PHAuthorizationStatusRestricted:
161
+ statusString = @"restricted";
162
+ NSLog(@"📸 Status: restricted");
163
+ break;
164
+ case PHAuthorizationStatusNotDetermined:
165
+ statusString = @"not_determined";
166
+ NSLog(@"📸 Status: not_determined");
167
+ break;
168
+ case PHAuthorizationStatusLimited:
169
+ statusString = @"limited";
170
+ NSLog(@"📸 Status: limited (limited access detected)");
171
+ break;
172
+ default:
173
+ statusString = @"unknown";
174
+ NSLog(@"📸 Status: unknown (%ld)", (long)status);
175
+ break;
176
+ }
177
+
178
+ // Double-check by also testing the legacy API
179
+ PHAuthorizationStatus legacyStatus = [PHPhotoLibrary authorizationStatus];
180
+ NSLog(@"📸 Legacy API status: %ld", (long)legacyStatus);
181
+
182
+ // If legacy API shows limited, override
183
+ if (legacyStatus == PHAuthorizationStatusLimited) {
184
+ NSLog(@"📸 Legacy API detected limited access, overriding to limited");
185
+ statusString = @"limited";
186
+ }
187
+
188
+ NSLog(@"📸 Final status string: %@", statusString);
189
+ resolve(statusString);
190
+ } else {
191
+ // iOS < 14 - use legacy method
192
+ PHAuthorizationStatus status = [PHPhotoLibrary authorizationStatus];
193
+ NSString *statusString;
194
+
195
+ switch (status) {
196
+ case PHAuthorizationStatusAuthorized:
197
+ statusString = @"authorized";
198
+ break;
199
+ case PHAuthorizationStatusDenied:
200
+ statusString = @"denied";
201
+ break;
202
+ case PHAuthorizationStatusRestricted:
203
+ statusString = @"restricted";
204
+ break;
205
+ case PHAuthorizationStatusNotDetermined:
206
+ statusString = @"not_determined";
207
+ break;
208
+ default:
209
+ statusString = @"unknown";
210
+ break;
211
+ }
212
+
213
+ resolve(statusString);
214
+ }
215
+ }
216
+
217
+ RCT_EXPORT_METHOD(fetchPhotoLibraryAssets:(NSDictionary *)options
218
+ resolver:(RCTPromiseResolveBlock)resolve
219
+ rejecter:(RCTPromiseRejectBlock)reject)
220
+ {
221
+ dispatch_async(dispatch_get_main_queue(), ^{
222
+ NSInteger limit = options[@"limit"] ? [options[@"limit"] integerValue] : 20;
223
+ NSInteger offset = options[@"offset"] ? [options[@"offset"] integerValue] : 0;
224
+ NSString *mediaType = options[@"mediaType"] ? options[@"mediaType"] : @"photo";
225
+
226
+ NSLog(@"Fetching assets with limit: %ld, offset: %ld, mediaType: %@", (long)limit, (long)offset, mediaType);
227
+
228
+ // Check current authorization status
229
+ PHAuthorizationStatus status = [PHPhotoLibrary authorizationStatus];
230
+ NSLog(@"Current authorization status: %ld", (long)status);
231
+
232
+ // For limited access, be more careful to avoid triggering popup
233
+ PHFetchOptions *fetchOptions = [[PHFetchOptions alloc] init];
234
+ fetchOptions.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"creationDate" ascending:NO]];
235
+
236
+ // Configure media type based on parameter
237
+ PHAssetMediaType assetMediaType;
238
+ PHFetchResult<PHAsset *> *assets;
239
+
240
+ if ([mediaType isEqualToString:@"video"]) {
241
+ assetMediaType = PHAssetMediaTypeVideo;
242
+ assets = [PHAsset fetchAssetsWithMediaType:assetMediaType options:fetchOptions];
243
+ } else if ([mediaType isEqualToString:@"mixed"]) {
244
+ // For mixed media, fetch all assets without specifying a media type
245
+ assets = [PHAsset fetchAssetsWithOptions:fetchOptions];
246
+ } else {
247
+ // Default to images
248
+ assetMediaType = PHAssetMediaTypeImage;
249
+ assets = [PHAsset fetchAssetsWithMediaType:assetMediaType options:fetchOptions];
250
+ }
251
+
252
+ NSLog(@"Total assets found: %lu", (unsigned long)assets.count);
253
+
254
+ if (assets.count == 0) {
255
+ resolve(@{
256
+ @"assets": @[],
257
+ @"hasMore": @NO,
258
+ @"totalCount": @0
259
+ });
260
+ return;
261
+ }
262
+
263
+ NSInteger startIndex = offset;
264
+ NSInteger endIndex = MIN(offset + limit, assets.count);
265
+ NSInteger totalAssets = endIndex - startIndex;
266
+
267
+ if (totalAssets <= 0) {
268
+ resolve(@{
269
+ @"assets": @[],
270
+ @"hasMore": @(offset < assets.count),
271
+ @"totalCount": @(assets.count)
272
+ });
273
+ return;
274
+ }
275
+
276
+ NSMutableArray *assetArray = [[NSMutableArray alloc] init];
277
+
278
+ // Use simpler approach - just return asset info without processing images
279
+ for (NSInteger i = startIndex; i < endIndex; i++) {
280
+ PHAsset *asset = [assets objectAtIndex:i];
281
+
282
+ // Create a simple placeholder URI using the asset identifier
283
+ NSString *placeholderUri = [NSString stringWithFormat:@"ph://%@", asset.localIdentifier];
284
+
285
+ // Determine asset media type
286
+ NSString *assetMediaTypeString;
287
+ if (asset.mediaType == PHAssetMediaTypeVideo) {
288
+ assetMediaTypeString = @"video";
289
+ } else {
290
+ assetMediaTypeString = @"image";
291
+ }
292
+
293
+ NSMutableDictionary *assetInfo = [[NSMutableDictionary alloc] initWithDictionary:@{
294
+ @"uri": placeholderUri,
295
+ @"width": @(asset.pixelWidth),
296
+ @"height": @(asset.pixelHeight),
297
+ @"id": asset.localIdentifier,
298
+ @"creationDate": @([asset.creationDate timeIntervalSince1970] * 1000),
299
+ @"mediaType": assetMediaTypeString
300
+ }];
301
+
302
+ // Add duration for videos
303
+ if (asset.mediaType == PHAssetMediaTypeVideo) {
304
+ assetInfo[@"duration"] = @(asset.duration);
305
+ }
306
+
307
+ [assetArray addObject:assetInfo];
308
+ }
309
+
310
+ // Sort by creation date
311
+ NSArray *sortedAssets = [assetArray sortedArrayUsingComparator:^NSComparisonResult(NSDictionary *a, NSDictionary *b) {
312
+ NSNumber *dateA = a[@"creationDate"];
313
+ NSNumber *dateB = b[@"creationDate"];
314
+ return [dateB compare:dateA];
315
+ }];
316
+
317
+ BOOL hasMore = endIndex < assets.count;
318
+
319
+ NSLog(@"Returning %lu assets, hasMore: %d, totalCount: %lu",
320
+ (unsigned long)sortedAssets.count, hasMore, (unsigned long)assets.count);
321
+
322
+ resolve(@{
323
+ @"assets": sortedAssets,
324
+ @"hasMore": @(hasMore),
325
+ @"totalCount": @(assets.count)
326
+ });
327
+ });
328
+ }
329
+
330
+ RCT_EXPORT_METHOD(getImageForAsset:(NSString *)assetId
331
+ targetWidth:(NSNumber *)targetWidth
332
+ targetHeight:(NSNumber *)targetHeight
333
+ resolver:(RCTPromiseResolveBlock)resolve
334
+ rejecter:(RCTPromiseRejectBlock)reject)
335
+ {
336
+ dispatch_async(dispatch_get_main_queue(), ^{
337
+ PHFetchResult<PHAsset *> *result = [PHAsset fetchAssetsWithLocalIdentifiers:@[assetId] options:nil];
338
+
339
+ if (result.count == 0) {
340
+ reject(@"ASSET_NOT_FOUND", @"Asset not found", nil);
341
+ return;
342
+ }
343
+
344
+ PHAsset *asset = [result firstObject];
345
+ PHImageManager *imageManager = [PHImageManager defaultManager];
346
+ PHImageRequestOptions *requestOptions = [[PHImageRequestOptions alloc] init];
347
+ requestOptions.deliveryMode = PHImageRequestOptionsDeliveryModeHighQualityFormat;
348
+ requestOptions.networkAccessAllowed = YES;
349
+ requestOptions.synchronous = NO;
350
+
351
+ CGFloat width = targetWidth ? [targetWidth floatValue] : 300;
352
+ CGFloat height = targetHeight ? [targetHeight floatValue] : 300;
353
+
354
+ [imageManager requestImageForAsset:asset
355
+ targetSize:CGSizeMake(width, height)
356
+ contentMode:PHImageContentModeAspectFill
357
+ options:requestOptions
358
+ resultHandler:^(UIImage *image, NSDictionary *info) {
359
+ if (image) {
360
+ NSData *imageData = UIImageJPEGRepresentation(image, 0.8);
361
+ NSString *fileName = [NSString stringWithFormat:@"asset_%@.jpg", [[NSUUID UUID] UUIDString]];
362
+ NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:fileName];
363
+
364
+ if ([imageData writeToFile:filePath atomically:YES]) {
365
+ NSString *fileURI = [NSString stringWithFormat:@"file://%@", filePath];
366
+ resolve(fileURI);
367
+ } else {
368
+ reject(@"FILE_WRITE_ERROR", @"Failed to save image", nil);
369
+ }
370
+ } else {
371
+ reject(@"IMAGE_REQUEST_ERROR", @"Failed to get image", nil);
372
+ }
373
+ }];
374
+ });
375
+ }
376
+
377
+ RCT_EXPORT_METHOD(openPhotoLibraryLimitedPicker:(RCTPromiseResolveBlock)resolve
378
+ rejecter:(RCTPromiseRejectBlock)reject)
379
+ {
380
+ NSLog(@"🚀 openPhotoLibraryLimitedPicker called - Limited photo management");
381
+
382
+ dispatch_async(dispatch_get_main_queue(), ^{
383
+ UIViewController *presentingViewController = RCTPresentedViewController();
384
+ if (!presentingViewController) {
385
+ NSLog(@"❌ No presenting view controller available");
386
+ reject(@"NO_PRESENTING_VC", @"No presenting view controller available", nil);
387
+ return;
388
+ }
389
+
390
+ // Check iOS version first
391
+ NSOperatingSystemVersion version = [[NSProcessInfo processInfo] operatingSystemVersion];
392
+ NSLog(@"📱 Running iOS %ld.%ld.%ld", (long)version.majorVersion, (long)version.minorVersion, (long)version.patchVersion);
393
+
394
+ if (@available(iOS 15, *)) {
395
+ NSLog(@"✅ iOS 15+ detected - using presentLimitedLibraryPicker method");
396
+
397
+ // Use the iOS 15+ method for modern photo library access
398
+ PHPhotoLibrary *photoLibrary = [PHPhotoLibrary sharedPhotoLibrary];
399
+
400
+ // Check if the method exists
401
+ SEL pickerSelector = NSSelectorFromString(@"presentLimitedLibraryPickerFromViewController:completionHandler:");
402
+
403
+ if ([photoLibrary respondsToSelector:pickerSelector]) {
404
+ NSLog(@"🎯 Presenting limited library picker with completion handler");
405
+
406
+ // Use NSInvocation to call the method safely
407
+ NSMethodSignature *signature = [photoLibrary methodSignatureForSelector:pickerSelector];
408
+ NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
409
+ [invocation setTarget:photoLibrary];
410
+ [invocation setSelector:pickerSelector];
411
+ [invocation setArgument:&presentingViewController atIndex:2];
412
+
413
+ // Create completion handler block
414
+ void (^completionHandler)(NSArray<NSString *> *) = ^(NSArray<NSString *> *identifiers) {
415
+ NSLog(@"✅ Limited library picker completed with %lu newly selected identifiers", (unsigned long)identifiers.count);
416
+
417
+ // Trigger gallery refresh to show updated selection
418
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
419
+ [self sendEventWithName:@"PhotoLibraryChanged" body:@{}];
420
+ });
421
+ };
422
+
423
+ [invocation setArgument:&completionHandler atIndex:3];
424
+ [invocation invoke];
425
+
426
+ NSLog(@"✅ Limited library picker should now be showing");
427
+ resolve(@{@"method": @"limited_picker", @"ios_version": @"15+"});
428
+
429
+ } else {
430
+ NSLog(@"❌ Method not available even on iOS 15+");
431
+ // Fall back to settings
432
+ [self openSettingsApp:resolve reject:reject];
433
+ }
434
+
435
+ } else if (@available(iOS 14, *)) {
436
+ NSLog(@"⚠️ iOS 14 detected - limited picker not available, using fallback");
437
+ // iOS 14 doesn't have presentLimitedLibraryPicker, fall back to settings
438
+ [self openSettingsApp:resolve reject:reject];
439
+
440
+ } else {
441
+ NSLog(@"❌ iOS < 14, limited access not supported");
442
+ reject(@"IOS_VERSION_TOO_OLD", @"iOS 14+ required for limited photo access", nil);
443
+ }
444
+ });
445
+ }
446
+
447
+ - (void)openSettingsApp:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject
448
+ {
449
+ NSLog(@"📱 Opening Settings app as fallback for photo management");
450
+ NSURL *settingsURL = [NSURL URLWithString:UIApplicationOpenSettingsURLString];
451
+ if ([[UIApplication sharedApplication] canOpenURL:settingsURL]) {
452
+ [[UIApplication sharedApplication] openURL:settingsURL options:@{} completionHandler:^(BOOL success) {
453
+ if (success) {
454
+ NSLog(@"✅ Settings app opened successfully");
455
+ resolve(@{@"method": @"settings_fallback"});
456
+ } else {
457
+ reject(@"SETTINGS_FAILED", @"Failed to open Settings app", nil);
458
+ }
459
+ }];
460
+ } else {
461
+ reject(@"SETTINGS_UNAVAILABLE", @"Cannot open Settings app", nil);
462
+ }
463
+ }
464
+
465
+ RCT_EXPORT_METHOD(openPhotoLibraryLimitedSettings:(RCTPromiseResolveBlock)resolve
466
+ rejecter:(RCTPromiseRejectBlock)reject)
467
+ {
468
+ dispatch_async(dispatch_get_main_queue(), ^{
469
+ // This method always opens settings
470
+ [self openAppSettings:resolve];
471
+ });
472
+ }
473
+
474
+ - (void)openAppSettings:(RCTPromiseResolveBlock)resolve
475
+ {
476
+ NSURL *settingsURL = [NSURL URLWithString:UIApplicationOpenSettingsURLString];
477
+ if ([[UIApplication sharedApplication] canOpenURL:settingsURL]) {
478
+ [[UIApplication sharedApplication] openURL:settingsURL options:@{} completionHandler:^(BOOL success) {
479
+ resolve(@(success));
480
+ }];
481
+ } else {
482
+ resolve(@NO);
483
+ }
484
+ }
485
+
486
+
487
+ - (void)requestCameraPermissionWithCompletion:(void (^)(BOOL granted))completion
488
+ {
489
+ AVAuthorizationStatus status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];
490
+
491
+ if (status == AVAuthorizationStatusAuthorized) {
492
+ completion(YES);
493
+ } else if (status == AVAuthorizationStatusNotDetermined) {
494
+ [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) {
495
+ dispatch_async(dispatch_get_main_queue(), ^{
496
+ completion(granted);
497
+ });
498
+ }];
499
+ } else {
500
+ completion(NO);
501
+ }
502
+ }
503
+
504
+ - (void)requestGalleryPermissionWithCompletion:(void (^)(BOOL granted))completion
505
+ {
506
+ PHAuthorizationStatus status = [PHPhotoLibrary authorizationStatus];
507
+
508
+ BOOL isAuthorized = (status == PHAuthorizationStatusAuthorized);
509
+
510
+ // Check for limited status on iOS 14+
511
+ if (@available(iOS 14, *)) {
512
+ isAuthorized = isAuthorized || (status == PHAuthorizationStatusLimited);
513
+ }
514
+
515
+ if (isAuthorized) {
516
+ completion(YES);
517
+ } else if (status == PHAuthorizationStatusNotDetermined) {
518
+ [PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus newStatus) {
519
+ dispatch_async(dispatch_get_main_queue(), ^{
520
+ BOOL isGranted = (newStatus == PHAuthorizationStatusAuthorized);
521
+
522
+ // Check for limited status on iOS 14+
523
+ if (@available(iOS 14, *)) {
524
+ isGranted = isGranted || (newStatus == PHAuthorizationStatusLimited);
525
+ }
526
+
527
+ completion(isGranted);
528
+ });
529
+ }];
530
+ } else {
531
+ completion(NO);
532
+ }
533
+ }
534
+
535
+ - (void)presentImagePickerWithSourceType:(UIImagePickerControllerSourceType)sourceType
536
+ {
537
+ if (![UIImagePickerController isSourceTypeAvailable:sourceType]) {
538
+ self.reject(@"SOURCE_NOT_AVAILABLE", @"Source type not available", nil);
539
+ return;
540
+ }
541
+
542
+ UIImagePickerController *picker = [[UIImagePickerController alloc] init];
543
+ picker.delegate = self;
544
+ picker.sourceType = sourceType;
545
+ picker.mediaTypes = @[@"public.image"];
546
+
547
+ // Configure options
548
+ if (self.options[@"allowsEditing"]) {
549
+ picker.allowsEditing = [self.options[@"allowsEditing"] boolValue];
550
+ }
551
+
552
+ UIViewController *presentingViewController = RCTPresentedViewController();
553
+ [presentingViewController presentViewController:picker animated:YES completion:nil];
554
+ }
555
+
556
+ - (void)presentMultiSelectPicker:(NSInteger)maxSelectionLimit
557
+ {
558
+ if (@available(iOS 14, *)) {
559
+ PHPickerConfiguration *configuration = [[PHPickerConfiguration alloc] init];
560
+
561
+ // Configure filter based on media type from options
562
+ NSString *mediaType = self.options[@"mediaType"] ?: @"photo";
563
+ if ([mediaType isEqualToString:@"video"]) {
564
+ configuration.filter = [PHPickerFilter videosFilter];
565
+ } else if ([mediaType isEqualToString:@"mixed"]) {
566
+ configuration.filter = [PHPickerFilter anyFilterMatchingSubfilters:@[[PHPickerFilter imagesFilter], [PHPickerFilter videosFilter]]];
567
+ } else {
568
+ configuration.filter = [PHPickerFilter imagesFilter];
569
+ }
570
+
571
+ configuration.selectionLimit = maxSelectionLimit;
572
+ configuration.preferredAssetRepresentationMode = PHPickerConfigurationAssetRepresentationModeAutomatic;
573
+
574
+ PHPickerViewController *pickerViewController = [[PHPickerViewController alloc] initWithConfiguration:configuration];
575
+ pickerViewController.delegate = self;
576
+
577
+ UIViewController *presentingViewController = RCTPresentedViewController();
578
+ [presentingViewController presentViewController:pickerViewController animated:YES completion:nil];
579
+
580
+ NSLog(@"✅ Multi-select PHPickerViewController presented with mediaType: %@, limit: %ld", mediaType, (long)maxSelectionLimit);
581
+ } else {
582
+ // Fallback for iOS < 14: use UIImagePickerController (single selection)
583
+ NSLog(@"❌ iOS < 14 detected, multi-select not available. Using single image picker as fallback");
584
+ [self presentImagePickerWithSourceType:UIImagePickerControllerSourceTypePhotoLibrary];
585
+ }
586
+ }
587
+
588
+ #pragma mark - UIImagePickerControllerDelegate
589
+
590
+ - (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary<UIImagePickerControllerInfoKey,id> *)info
591
+ {
592
+ [picker dismissViewControllerAnimated:YES completion:nil];
593
+
594
+ UIImage *image = info[UIImagePickerControllerEditedImage] ?: info[UIImagePickerControllerOriginalImage];
595
+
596
+ if (!image) {
597
+ self.reject(@"NO_IMAGE", @"No image selected", nil);
598
+ return;
599
+ }
600
+
601
+ // Apply quality and size constraints
602
+ CGFloat quality = self.options[@"quality"] ? [self.options[@"quality"] floatValue] : 0.8;
603
+ NSNumber *maxWidth = self.options[@"maxWidth"];
604
+ NSNumber *maxHeight = self.options[@"maxHeight"];
605
+
606
+ if (maxWidth || maxHeight) {
607
+ image = [self resizeImage:image maxWidth:maxWidth maxHeight:maxHeight];
608
+ }
609
+
610
+ NSData *imageData = UIImageJPEGRepresentation(image, quality);
611
+
612
+ // Save to temporary directory
613
+ NSString *fileName = [NSString stringWithFormat:@"IMG_%@.jpg", [[NSUUID UUID] UUIDString]];
614
+ NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:fileName];
615
+
616
+ if ([imageData writeToFile:filePath atomically:YES]) {
617
+ NSString *fileURI = [NSString stringWithFormat:@"file://%@", filePath];
618
+
619
+ NSDictionary *result = @{
620
+ @"success": @YES,
621
+ @"uri": fileURI,
622
+ @"fileName": fileName,
623
+ @"fileSize": @(imageData.length),
624
+ @"width": @(image.size.width),
625
+ @"height": @(image.size.height),
626
+ @"type": @"image/jpeg"
627
+ };
628
+
629
+ self.resolve(result);
630
+ } else {
631
+ self.reject(@"SAVE_ERROR", @"Failed to save image", nil);
632
+ }
633
+ }
634
+
635
+ - (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker
636
+ {
637
+ [picker dismissViewControllerAnimated:YES completion:nil];
638
+
639
+ NSDictionary *result = @{
640
+ @"success": @NO,
641
+ @"error": @"User cancelled"
642
+ };
643
+
644
+ self.resolve(result);
645
+ }
646
+
647
+ - (UIImage *)resizeImage:(UIImage *)image maxWidth:(NSNumber *)maxWidth maxHeight:(NSNumber *)maxHeight
648
+ {
649
+ CGSize size = image.size;
650
+ CGFloat targetWidth = maxWidth ? [maxWidth floatValue] : size.width;
651
+ CGFloat targetHeight = maxHeight ? [maxHeight floatValue] : size.height;
652
+
653
+ CGFloat widthRatio = targetWidth / size.width;
654
+ CGFloat heightRatio = targetHeight / size.height;
655
+ CGFloat ratio = MIN(widthRatio, heightRatio);
656
+
657
+ if (ratio >= 1.0) {
658
+ return image;
659
+ }
660
+
661
+ CGSize newSize = CGSizeMake(size.width * ratio, size.height * ratio);
662
+
663
+ UIGraphicsBeginImageContextWithOptions(newSize, NO, 0.0);
664
+ [image drawInRect:CGRectMake(0, 0, newSize.width, newSize.height)];
665
+ UIImage *resizedImage = UIGraphicsGetImageFromCurrentImageContext();
666
+ UIGraphicsEndImageContext();
667
+
668
+ return resizedImage;
669
+ }
670
+
671
+ #pragma mark - PHPhotoLibraryChangeObserver
672
+
673
+ - (void)photoLibraryDidChange:(PHChange *)changeInstance
674
+ {
675
+ NSLog(@"📸 Photo library changed - limited selection may have been updated");
676
+
677
+ // Notify React Native that the photo library changed
678
+ // This will trigger a refresh of the gallery
679
+ [self sendEventWithName:@"PhotoLibraryChanged" body:@{}];
680
+ }
681
+
682
+ #pragma mark - PHPickerViewControllerDelegate
683
+
684
+ - (void)picker:(PHPickerViewController *)picker didFinishPicking:(NSArray<PHPickerResult *> *)results API_AVAILABLE(ios(14))
685
+ {
686
+ NSLog(@"📸 PHPickerViewController finished with %lu results", (unsigned long)results.count);
687
+
688
+ [picker dismissViewControllerAnimated:YES completion:^{
689
+ NSLog(@"✅ PHPickerViewController dismissed");
690
+
691
+ // Check if this was a multi-select operation
692
+ if (self.options[@"isMultiSelect"] && [self.options[@"isMultiSelect"] boolValue]) {
693
+ if (results.count == 0) {
694
+ // User cancelled or selected no images
695
+ NSDictionary *result = @{
696
+ @"success": @NO,
697
+ @"error": @"User cancelled or no images selected",
698
+ @"images": @[]
699
+ };
700
+ self.resolve(result);
701
+ self.resolve = nil;
702
+ self.reject = nil;
703
+ return;
704
+ }
705
+
706
+ NSLog(@"🎯 Processing %lu multi-select results", (unsigned long)results.count);
707
+ [self processMultiSelectResults:results];
708
+ } else {
709
+ // This was a limited library picker interaction
710
+ NSLog(@"📸 Limited library picker interaction completed - triggering gallery refresh");
711
+
712
+ // Trigger a refresh to show any changes in the photo selection
713
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
714
+ [self sendEventWithName:@"PhotoLibraryChanged" body:@{}];
715
+ });
716
+ }
717
+ }];
718
+ }
719
+
720
+
721
+ - (void)processMultiSelectResults:(NSArray<PHPickerResult *> *)results API_AVAILABLE(ios(14))
722
+ {
723
+ NSMutableArray *processedMedia = [[NSMutableArray alloc] init];
724
+ dispatch_group_t group = dispatch_group_create();
725
+
726
+ BOOL includeBase64 = [self.options[@"includeBase64"] boolValue];
727
+ CGFloat quality = [self.options[@"quality"] floatValue];
728
+ NSNumber *maxWidth = self.options[@"maxWidth"];
729
+ NSNumber *maxHeight = self.options[@"maxHeight"];
730
+
731
+ NSLog(@"🔄 Starting to process %lu media items with options: includeBase64=%@, quality=%.1f",
732
+ (unsigned long)results.count, includeBase64 ? @"YES" : @"NO", quality);
733
+
734
+ for (NSInteger i = 0; i < results.count; i++) {
735
+ PHPickerResult *result = results[i];
736
+
737
+ dispatch_group_enter(group);
738
+
739
+ // Check what type of media this is
740
+ if ([result.itemProvider hasItemConformingToTypeIdentifier:@"public.image"]) {
741
+ // Process as image
742
+ [result.itemProvider loadObjectOfClass:[UIImage class] completionHandler:^(__kindof id<NSItemProviderReading> _Nullable object, NSError * _Nullable error) {
743
+ if ([object isKindOfClass:[UIImage class]]) {
744
+ UIImage *image = (UIImage *)object;
745
+
746
+ // Apply size constraints if specified
747
+ if (maxWidth || maxHeight) {
748
+ image = [self resizeImage:image maxWidth:maxWidth maxHeight:maxHeight];
749
+ }
750
+
751
+ // Generate unique filename
752
+ NSString *fileName = [NSString stringWithFormat:@"multi_select_img_%ld_%@.jpg", (long)i, [[NSUUID UUID] UUIDString]];
753
+ NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:fileName];
754
+
755
+ // Convert to JPEG data
756
+ NSData *imageData = UIImageJPEGRepresentation(image, quality);
757
+
758
+ if ([imageData writeToFile:filePath atomically:YES]) {
759
+ NSString *fileURI = [NSString stringWithFormat:@"file://%@", filePath];
760
+
761
+ NSMutableDictionary *imageInfo = [[NSMutableDictionary alloc] init];
762
+ imageInfo[@"uri"] = fileURI;
763
+ imageInfo[@"fileName"] = fileName;
764
+ imageInfo[@"fileSize"] = @(imageData.length);
765
+ imageInfo[@"width"] = @(image.size.width);
766
+ imageInfo[@"height"] = @(image.size.height);
767
+ imageInfo[@"type"] = @"image/jpeg";
768
+ imageInfo[@"id"] = [[NSUUID UUID] UUIDString];
769
+
770
+ // Add base64 if requested
771
+ if (includeBase64) {
772
+ NSString *base64String = [imageData base64EncodedStringWithOptions:0];
773
+ imageInfo[@"base64"] = [NSString stringWithFormat:@"data:image/jpeg;base64,%@", base64String];
774
+ }
775
+
776
+ @synchronized (processedMedia) {
777
+ [processedMedia addObject:imageInfo];
778
+ }
779
+
780
+ NSLog(@"✅ Processed image %ld: %@", (long)i, fileName);
781
+ } else {
782
+ NSLog(@"❌ Failed to save image %ld", (long)i);
783
+ }
784
+ } else {
785
+ NSLog(@"❌ Failed to load image %ld: %@", (long)i, error.localizedDescription);
786
+ }
787
+
788
+ dispatch_group_leave(group);
789
+ }];
790
+ } else if ([result.itemProvider hasItemConformingToTypeIdentifier:@"public.movie"]) {
791
+ // Process as video - but skip base64 conversion for iOS
792
+ [result.itemProvider loadFileRepresentationForTypeIdentifier:@"public.movie" completionHandler:^(NSURL * _Nullable url, NSError * _Nullable error) {
793
+ if (url && !error) {
794
+ // Generate unique filename
795
+ NSString *fileName = [NSString stringWithFormat:@"multi_select_video_%ld_%@.mp4", (long)i, [[NSUUID UUID] UUIDString]];
796
+ NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:fileName];
797
+ NSURL *destinationURL = [NSURL fileURLWithPath:filePath];
798
+
799
+ NSError *copyError;
800
+ if ([[NSFileManager defaultManager] copyItemAtURL:url toURL:destinationURL error:&copyError]) {
801
+ NSString *fileURI = [NSString stringWithFormat:@"file://%@", filePath];
802
+
803
+ // Get video metadata
804
+ AVAsset *asset = [AVAsset assetWithURL:destinationURL];
805
+ CMTime duration = asset.duration;
806
+ CGSize videoSize = CGSizeZero;
807
+
808
+ NSArray<AVAssetTrack *> *videoTracks = [asset tracksWithMediaType:AVMediaTypeVideo];
809
+ if (videoTracks.count > 0) {
810
+ AVAssetTrack *videoTrack = videoTracks[0];
811
+ videoSize = videoTrack.naturalSize;
812
+ }
813
+
814
+ NSData *videoData = [NSData dataWithContentsOfURL:destinationURL];
815
+
816
+ NSMutableDictionary *videoInfo = [[NSMutableDictionary alloc] init];
817
+ videoInfo[@"uri"] = fileURI;
818
+ videoInfo[@"fileName"] = fileName;
819
+ videoInfo[@"fileSize"] = @(videoData.length);
820
+ videoInfo[@"width"] = @((int)videoSize.width);
821
+ videoInfo[@"height"] = @((int)videoSize.height);
822
+ videoInfo[@"type"] = @"video/mp4";
823
+ videoInfo[@"id"] = [[NSUUID UUID] UUIDString];
824
+
825
+ // For iOS videos, skip base64 conversion entirely
826
+ if (includeBase64) {
827
+ NSLog(@"iOS video base64 conversion skipped for performance reasons");
828
+ videoInfo[@"base64"] = @""; // Empty base64 for iOS videos
829
+ }
830
+
831
+ @synchronized (processedMedia) {
832
+ [processedMedia addObject:videoInfo];
833
+ }
834
+
835
+ NSLog(@"✅ Processed video %ld: %@ (no base64)", (long)i, fileName);
836
+ } else {
837
+ NSLog(@"❌ Failed to copy video %ld: %@", (long)i, copyError.localizedDescription);
838
+ }
839
+ } else {
840
+ NSLog(@"❌ Failed to load video %ld: %@", (long)i, error.localizedDescription);
841
+ }
842
+
843
+ dispatch_group_leave(group);
844
+ }];
845
+ } else {
846
+ // Unknown media type
847
+ NSLog(@"❌ Unknown media type for item %ld", (long)i);
848
+ dispatch_group_leave(group);
849
+ }
850
+ }
851
+
852
+ dispatch_group_notify(group, dispatch_get_main_queue(), ^{
853
+ // Sort media to maintain order
854
+ NSArray *sortedMedia = [processedMedia sortedArrayUsingComparator:^NSComparisonResult(NSDictionary *a, NSDictionary *b) {
855
+ NSString *fileNameA = a[@"fileName"];
856
+ NSString *fileNameB = b[@"fileName"];
857
+ return [fileNameA compare:fileNameB];
858
+ }];
859
+
860
+ NSLog(@"✅ All media processed successfully. Total: %lu", (unsigned long)sortedMedia.count);
861
+
862
+ NSDictionary *result = @{
863
+ @"success": @YES,
864
+ @"images": sortedMedia,
865
+ @"count": @(sortedMedia.count)
866
+ };
867
+
868
+ if (self.resolve) {
869
+ self.resolve(result);
870
+ self.resolve = nil;
871
+ self.reject = nil;
872
+ }
873
+ });
874
+ }
875
+
876
+ @end