/** * Copyright (c) 2015-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. */ #import "RCTAlertManager.h" #import "RCTAssert.h" #import "RCTConvert.h" #import "RCTLog.h" #import "RCTUtils.h" @implementation RCTConvert (UIAlertViewStyle) RCT_ENUM_CONVERTER(UIAlertViewStyle, (@{ @"default": @(UIAlertViewStyleDefault), @"secure-text": @(UIAlertViewStyleSecureTextInput), @"plain-text": @(UIAlertViewStylePlainTextInput), @"login-password": @(UIAlertViewStyleLoginAndPasswordInput), }), UIAlertViewStyleDefault, integerValue) @end @interface RCTAlertManager() @end @implementation RCTAlertManager { NSMutableArray *_alerts; NSMutableArray *_alertControllers; NSMutableArray *_alertCallbacks; NSMutableArray *> *_alertButtonKeys; } RCT_EXPORT_MODULE() - (dispatch_queue_t)methodQueue { return dispatch_get_main_queue(); } - (void)invalidate { for (UIAlertView *alert in _alerts) { [alert dismissWithClickedButtonIndex:0 animated:YES]; } for (UIAlertController *alertController in _alertControllers) { [alertController.presentingViewController dismissViewControllerAnimated:YES completion:nil]; } } /** * @param {NSDictionary} args Dictionary of the form * * @{ * @"message": @"", * @"buttons": @[ * @{@"": @""}, * @{@"": @""}, * ], * @"cancelButtonKey": @"", * } * The key from the `buttons` dictionary is passed back in the callback on click. * Buttons are displayed in the order they are specified. */ RCT_EXPORT_METHOD(alertWithArgs:(NSDictionary *)args callback:(RCTResponseSenderBlock)callback) { NSString *title = [RCTConvert NSString:args[@"title"]]; NSString *message = [RCTConvert NSString:args[@"message"]]; UIAlertViewStyle type = [RCTConvert UIAlertViewStyle:args[@"type"]]; NSArray *buttons = [RCTConvert NSDictionaryArray:args[@"buttons"]]; NSString *cancelButtonKey = [RCTConvert NSString:args[@"cancelButtonKey"]]; NSString *destructiveButtonKey = [RCTConvert NSString:args[@"destructiveButtonKey"]]; if (!title && !message) { RCTLogError(@"Must specify either an alert title, or message, or both"); return; } if (buttons.count == 0) { if (type == UIAlertViewStyleDefault) { buttons = @[@{@"0": RCTUIKitLocalizedString(@"OK")}]; cancelButtonKey = @"0"; } else { buttons = @[ @{@"0": RCTUIKitLocalizedString(@"OK")}, @{@"1": RCTUIKitLocalizedString(@"Cancel")}, ]; cancelButtonKey = @"1"; } } #if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_8_0 // TODO: we've encountered some bug when presenting alerts on top of a window // that is subsequently dismissed. As a temporary solution to this, we'll use // UIAlertView preferentially if it's available and supports our use case. BOOL preferAlertView = (!RCTRunningInAppExtension() && !destructiveButtonKey && UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone); if (preferAlertView || [UIAlertController class] == nil) { UIAlertView *alertView = RCTAlertView(title, nil, self, nil, nil); alertView.alertViewStyle = type; alertView.message = message; NSMutableArray *buttonKeys = [[NSMutableArray alloc] initWithCapacity:buttons.count]; NSInteger index = 0; for (NSDictionary *button in buttons) { if (button.count != 1) { RCTLogError(@"Button definitions should have exactly one key."); } NSString *buttonKey = button.allKeys.firstObject; NSString *buttonTitle = [RCTConvert NSString:button[buttonKey]]; [alertView addButtonWithTitle:buttonTitle]; if ([buttonKey isEqualToString:cancelButtonKey]) { alertView.cancelButtonIndex = buttonKeys.count; } [buttonKeys addObject:buttonKey]; index ++; } if (!_alerts) { _alerts = [NSMutableArray new]; _alertCallbacks = [NSMutableArray new]; _alertButtonKeys = [NSMutableArray new]; } [_alerts addObject:alertView]; [_alertCallbacks addObject:callback ?: ^(__unused id unused) {}]; [_alertButtonKeys addObject:buttonKeys]; [alertView show]; } else #endif { UIViewController *presentingController = RCTKeyWindow().rootViewController; if (presentingController == nil) { RCTLogError(@"Tried to display alert view but there is no application window. args: %@", args); return; } // Walk the chain up to get the topmost modal view controller. If modals are // presented the root view controller's view might not be in the window // hierarchy, and presenting from it will fail. while (presentingController.presentedViewController) { presentingController = presentingController.presentedViewController; } UIAlertController *alertController = [UIAlertController alertControllerWithTitle:title message:nil preferredStyle:UIAlertControllerStyleAlert]; switch (type) { case UIAlertViewStylePlainTextInput: [alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) { textField.secureTextEntry = NO; }]; break; case UIAlertViewStyleSecureTextInput: [alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) { textField.placeholder = RCTUIKitLocalizedString(@"Password"); textField.secureTextEntry = YES; }]; break; case UIAlertViewStyleLoginAndPasswordInput: [alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) { textField.placeholder = RCTUIKitLocalizedString(@"Login"); }]; [alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) { textField.placeholder = RCTUIKitLocalizedString(@"Password"); textField.secureTextEntry = YES; }]; case UIAlertViewStyleDefault: break; } alertController.message = message; for (NSDictionary *button in buttons) { if (button.count != 1) { RCTLogError(@"Button definitions should have exactly one key."); } NSString *buttonKey = button.allKeys.firstObject; NSString *buttonTitle = [RCTConvert NSString:button[buttonKey]]; UIAlertActionStyle buttonStyle = UIAlertActionStyleDefault; if ([buttonKey isEqualToString:cancelButtonKey]) { buttonStyle = UIAlertActionStyleCancel; } else if ([buttonKey isEqualToString:destructiveButtonKey]) { buttonStyle = UIAlertActionStyleDestructive; } [alertController addAction:[UIAlertAction actionWithTitle:buttonTitle style:buttonStyle handler:^(__unused UIAlertAction *action) { switch (type) { case UIAlertViewStylePlainTextInput: case UIAlertViewStyleSecureTextInput: callback(@[buttonKey, [alertController.textFields.firstObject text]]); break; case UIAlertViewStyleLoginAndPasswordInput: { NSDictionary *loginCredentials = @{ @"login": [alertController.textFields.firstObject text], @"password": [alertController.textFields.lastObject text] }; callback(@[buttonKey, loginCredentials]); break; } case UIAlertViewStyleDefault: callback(@[buttonKey]); break; } }]]; } if (!_alertControllers) { _alertControllers = [NSMutableArray new]; } [_alertControllers addObject:alertController]; [presentingController presentViewController:alertController animated:YES completion:nil]; } } #pragma mark - UIAlertViewDelegate - (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex { NSUInteger index = [_alerts indexOfObject:alertView]; RCTAssert(index != NSNotFound, @"Dismissed alert was not recognised"); RCTResponseSenderBlock callback = _alertCallbacks[index]; NSArray *buttonKeys = _alertButtonKeys[index]; switch (alertView.alertViewStyle) { case UIAlertViewStylePlainTextInput: case UIAlertViewStyleSecureTextInput: callback(@[buttonKeys[buttonIndex], [alertView textFieldAtIndex:0].text]); break; case UIAlertViewStyleLoginAndPasswordInput: { NSDictionary *loginCredentials = @{ @"login": [alertView textFieldAtIndex:0].text, @"password": [alertView textFieldAtIndex:1].text, }; callback(@[buttonKeys[buttonIndex], loginCredentials]); break; } case UIAlertViewStyleDefault: callback(@[buttonKeys[buttonIndex]]); break; } [_alerts removeObjectAtIndex:index]; [_alertCallbacks removeObjectAtIndex:index]; [_alertButtonKeys removeObjectAtIndex:index]; } @end