/* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import #import #import #import #import #import #import #import "CoreModulesPlugins.h" using namespace facebook::react; @interface RCTActionSheetManager () @property (nonatomic, strong) NSMutableArray *alertControllers; @end @implementation RCTActionSheetManager - (instancetype)init { self = [super init]; if (self) { _alertControllers = [NSMutableArray new]; } return self; } + (BOOL)requiresMainQueueSetup { return NO; } RCT_EXPORT_MODULE() @synthesize viewRegistry_DEPRECATED = _viewRegistry_DEPRECATED; - (dispatch_queue_t)methodQueue { return dispatch_get_main_queue(); } - (void)presentViewController:(UIViewController *)alertController onParentViewController:(UIViewController *)parentViewController anchorViewTag:(NSNumber *)anchorViewTag { alertController.modalPresentationStyle = UIModalPresentationPopover; UIView *sourceView = parentViewController.view; if (anchorViewTag) { sourceView = [self.viewRegistry_DEPRECATED viewForReactTag:anchorViewTag]; } else { alertController.popoverPresentationController.permittedArrowDirections = 0; } alertController.popoverPresentationController.sourceView = sourceView; alertController.popoverPresentationController.sourceRect = sourceView.bounds; [parentViewController presentViewController:alertController animated:YES completion:nil]; } RCT_EXPORT_METHOD(showActionSheetWithOptions : (JS::NativeActionSheetManager::SpecShowActionSheetWithOptionsOptions &)options callback : (RCTResponseSenderBlock)callback) { if (RCTRunningInAppExtension()) { RCTLogError(@"Unable to show action sheet from app extension"); return; } NSString *title = options.title(); NSString *message = options.message(); NSArray *buttons = RCTConvertOptionalVecToArray(options.options(), ^id(NSString *element) { return element; }); NSArray *disabledButtonIndices; NSInteger cancelButtonIndex = options.cancelButtonIndex() ? [RCTConvert NSInteger:@(*options.cancelButtonIndex())] : -1; NSArray *destructiveButtonIndices; if (options.disabledButtonIndices()) { disabledButtonIndices = RCTConvertVecToArray(*options.disabledButtonIndices(), ^id(double element) { return @(element); }); } if (options.destructiveButtonIndices()) { destructiveButtonIndices = RCTConvertVecToArray(*options.destructiveButtonIndices(), ^id(double element) { return @(element); }); } else { NSNumber *destructiveButtonIndex = @-1; destructiveButtonIndices = @[ destructiveButtonIndex ]; } UIViewController *controller = RCTPresentedViewController(); NSNumber *anchor = [RCTConvert NSNumber:options.anchor() ? @(*options.anchor()) : nil]; UIColor *tintColor = [RCTConvert UIColor:options.tintColor() ? @(*options.tintColor()) : nil]; UIColor *cancelButtonTintColor = [RCTConvert UIColor:options.cancelButtonTintColor() ? @(*options.cancelButtonTintColor()) : nil]; if (controller == nil) { RCTLogError( @"Tried to display action sheet but there is no application window. options: %@", @{ @"title" : title, @"message" : message, @"options" : buttons, @"cancelButtonIndex" : @(cancelButtonIndex), @"destructiveButtonIndices" : destructiveButtonIndices, @"anchor" : anchor, @"tintColor" : tintColor, @"cancelButtonTintColor" : cancelButtonTintColor, @"disabledButtonIndices" : disabledButtonIndices, }); return; } /* * The `anchor` option takes a view to set as the anchor for the share * popup to point to, on iPads running iOS 8. If it is not passed, it * defaults to centering the share popup on screen without any arrows. */ NSNumber *anchorViewTag = anchor; UIAlertController *alertController = [UIAlertController alertControllerWithTitle:title message:message preferredStyle:UIAlertControllerStyleActionSheet]; NSInteger index = 0; bool isCancelButtonIndex = false; // The handler for a button might get called more than once when tapping outside // the action sheet on iPad. RCTResponseSenderBlock can only be called once so // keep track of callback invocation here. __block bool callbackInvoked = false; for (NSString *option in buttons) { UIAlertActionStyle style = UIAlertActionStyleDefault; if ([destructiveButtonIndices containsObject:@(index)]) { style = UIAlertActionStyleDestructive; } else if (index == cancelButtonIndex) { style = UIAlertActionStyleCancel; isCancelButtonIndex = true; } NSInteger localIndex = index; UIAlertAction *actionButton = [UIAlertAction actionWithTitle:option style:style handler:^(__unused UIAlertAction *action) { if (!callbackInvoked) { callbackInvoked = true; [self->_alertControllers removeObject:alertController]; callback(@[ @(localIndex) ]); } }]; if (isCancelButtonIndex) { [actionButton setValue:cancelButtonTintColor forKey:@"titleTextColor"]; } [alertController addAction:actionButton]; index++; } if (disabledButtonIndices) { for (NSNumber *disabledButtonIndex in disabledButtonIndices) { if ([disabledButtonIndex integerValue] < buttons.count) { [alertController.actions[[disabledButtonIndex integerValue]] setEnabled:false]; } else { RCTLogError( @"Index %@ from `disabledButtonIndices` is out of bounds. Maximum index value is %@.", @([disabledButtonIndex integerValue]), @(buttons.count - 1)); return; } } } alertController.view.tintColor = tintColor; #if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && defined(__IPHONE_13_0) && \ __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_13_0 if (@available(iOS 13.0, *)) { NSString *userInterfaceStyle = [RCTConvert NSString:options.userInterfaceStyle()]; if (userInterfaceStyle == nil || [userInterfaceStyle isEqualToString:@""]) { alertController.overrideUserInterfaceStyle = UIUserInterfaceStyleUnspecified; } else if ([userInterfaceStyle isEqualToString:@"dark"]) { alertController.overrideUserInterfaceStyle = UIUserInterfaceStyleDark; } else if ([userInterfaceStyle isEqualToString:@"light"]) { alertController.overrideUserInterfaceStyle = UIUserInterfaceStyleLight; } } #endif [_alertControllers addObject:alertController]; [self presentViewController:alertController onParentViewController:controller anchorViewTag:anchorViewTag]; } RCT_EXPORT_METHOD(dismissActionSheet) { if (_alertControllers.count == 0) { RCTLogWarn(@"Unable to dismiss action sheet"); } id _alertController = [_alertControllers lastObject]; [_alertController dismissViewControllerAnimated:YES completion:nil]; [_alertControllers removeLastObject]; } RCT_EXPORT_METHOD(showShareActionSheetWithOptions : (JS::NativeActionSheetManager::SpecShowShareActionSheetWithOptionsOptions &)options failureCallback : (RCTResponseSenderBlock)failureCallback successCallback : (RCTResponseSenderBlock)successCallback) { if (RCTRunningInAppExtension()) { RCTLogError(@"Unable to show action sheet from app extension"); return; } NSMutableArray *items = [NSMutableArray array]; NSString *message = options.message(); if (message) { [items addObject:message]; } NSURL *URL = [RCTConvert NSURL:options.url()]; if (URL) { if ([URL.scheme.lowercaseString isEqualToString:@"data"]) { NSError *error; NSData *data = [NSData dataWithContentsOfURL:URL options:(NSDataReadingOptions)0 error:&error]; if (!data) { failureCallback(@[ RCTJSErrorFromNSError(error) ]); return; } [items addObject:data]; } else { [items addObject:URL]; } } if (items.count == 0) { RCTLogError(@"No `url` or `message` to share"); return; } UIActivityViewController *shareController = [[UIActivityViewController alloc] initWithActivityItems:items applicationActivities:nil]; NSString *subject = options.subject(); if (subject) { [shareController setValue:subject forKey:@"subject"]; } NSArray *excludedActivityTypes = RCTConvertOptionalVecToArray(options.excludedActivityTypes(), ^id(NSString *element) { return element; }); if (excludedActivityTypes) { shareController.excludedActivityTypes = excludedActivityTypes; } UIViewController *controller = RCTPresentedViewController(); shareController.completionWithItemsHandler = ^(NSString *activityType, BOOL completed, __unused NSArray *returnedItems, NSError *activityError) { if (activityError) { failureCallback(@[ RCTJSErrorFromNSError(activityError) ]); } else if (completed || activityType == nil) { successCallback(@[ @(completed), RCTNullIfNil(activityType) ]); } }; NSNumber *anchorViewTag = [RCTConvert NSNumber:options.anchor() ? @(*options.anchor()) : nil]; shareController.view.tintColor = [RCTConvert UIColor:options.tintColor() ? @(*options.tintColor()) : nil]; #if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && defined(__IPHONE_13_0) && \ __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_13_0 if (@available(iOS 13.0, *)) { NSString *userInterfaceStyle = [RCTConvert NSString:options.userInterfaceStyle()]; if (userInterfaceStyle == nil || [userInterfaceStyle isEqualToString:@""]) { shareController.overrideUserInterfaceStyle = UIUserInterfaceStyleUnspecified; } else if ([userInterfaceStyle isEqualToString:@"dark"]) { shareController.overrideUserInterfaceStyle = UIUserInterfaceStyleDark; } else if ([userInterfaceStyle isEqualToString:@"light"]) { shareController.overrideUserInterfaceStyle = UIUserInterfaceStyleLight; } } #endif [self presentViewController:shareController onParentViewController:controller anchorViewTag:anchorViewTag]; } - (std::shared_ptr)getTurboModule:(const ObjCTurboModule::InitParams &)params { return std::make_shared(params); } @end Class RCTActionSheetManagerCls(void) { return RCTActionSheetManager.class; }