Add secure and login-password types to AlertIOS.

Summary: Request from issue #3893

* Added support for `secure-text` and `login-password` types to AlertIOS.
* Fixed and extended the cancel button highlighting functionality, which was broken at some point
* Added localization for default `OK` and `Cancel` labels when using UIAlertController

Closes https://github.com/facebook/react-native/pull/4401

Reviewed By: javache

Differential Revision: D2702052

Pulled By: nicklockwood

fb-gh-sync-id: cce312d7fec949f5fd2a7c656e65c657c4832c8f
This commit is contained in:
Christopher Dro 2015-11-30 18:44:06 -08:00 committed by facebook-github-bot-7
parent 7242efde0a
commit f025049b6c
5 changed files with 249 additions and 83 deletions

View File

@ -43,7 +43,7 @@ exports.examples = [{
</TouchableHighlight>
<TouchableHighlight style={styles.wrapper}
onPress={() => AlertIOS.alert(
null,
'Foo Title',
null,
[
{text: 'Button', onPress: () => console.log('Button Pressed!')},
@ -97,6 +97,87 @@ exports.examples = [{
);
}
},
{
title: 'Alert Types',
render() {
return (
<View>
<TouchableHighlight
style={styles.wrapper}
onPress={() => AlertIOS.alert(
'Hello World',
null,
[
{text: 'OK', onPress: (text) => console.log('OK pressed')},
],
'default'
)}>
<View style={styles.button}>
<Text>
{'default'}
</Text>
</View>
</TouchableHighlight>
<TouchableHighlight
style={styles.wrapper}
onPress={() => AlertIOS.alert(
'Plain Text Entry',
null,
[
{text: 'Submit', onPress: (text) => console.log('Text: ' + text)},
],
'plain-text'
)}>
<View style={styles.button}>
<Text>
plain-text
</Text>
</View>
</TouchableHighlight>
<TouchableHighlight
style={styles.wrapper}
onPress={() => AlertIOS.alert(
'Secure Text Entry',
null,
[
{text: 'Submit', onPress: (text) => console.log('Password: ' + text)},
],
'secure-text'
)}>
<View style={styles.button}>
<Text>
secure-text
</Text>
</View>
</TouchableHighlight>
<TouchableHighlight
style={styles.wrapper}
onPress={() => AlertIOS.alert(
'Login & Password',
null,
[
{text: 'Submit', onPress: (details) => console.log('Login: ' + details.login + '; Password: ' + details.password)},
],
'login-password'
)}>
<View style={styles.button}>
<Text>
login-password
</Text>
</View>
</TouchableHighlight>
</View>
);
}
},
{
title: 'Prompt',
render(): React.Component {
@ -116,10 +197,11 @@ class PromptExample extends React.Component {
this.title = 'Type a value';
this.defaultValue = 'Default value';
this.buttons = [{
text: 'Custom cancel',
}, {
text: 'Custom OK',
onPress: this.promptResponse
}, {
text: 'Custom Cancel',
style: 'cancel',
}];
}

View File

@ -14,11 +14,18 @@
var RCTAlertManager = require('NativeModules').AlertManager;
var invariant = require('invariant');
var DEFAULT_BUTTON_TEXT = 'OK';
var DEFAULT_BUTTON = {
text: DEFAULT_BUTTON_TEXT,
onPress: null,
};
type AlertType = $Enum<{
'default': string;
'plain-text': string;
'secure-text': string;
'login-password': string;
}>;
type AlertButtonStyle = $Enum<{
'default': string;
'cancel': string;
'destructive': string;
}>;
/**
* Launches an alert dialog with the specified title and message.
@ -27,16 +34,13 @@ var DEFAULT_BUTTON = {
* respective onPress callback and dismiss the alert. By default, the only
* button will be an 'OK' button
*
* The last button in the list will be considered the 'Primary' button and
* it will appear bold.
*
* ```
* AlertIOS.alert(
* 'Foo Title',
* 'My Alert Msg',
* [
* {text: 'Foo', onPress: () => console.log('Foo Pressed!')},
* {text: 'Bar', onPress: () => console.log('Bar Pressed!')},
* {text: 'OK', onPress: () => console.log('OK Pressed!')},
* {text: 'Cancel', onPress: () => console.log('Cancel Pressed!'), style: 'cancel'},
* ]
* )
* ```
@ -47,29 +51,36 @@ class AlertIOS {
title: ?string,
message?: ?string,
buttons?: Array<{
text: ?string;
text?: string;
onPress?: ?Function;
style?: AlertButtonStyle;
}>,
type?: ?string
type?: ?AlertType
): void {
var callbacks = [];
var buttonsSpec = [];
title = title || '';
message = message || '';
buttons = buttons || [DEFAULT_BUTTON];
type = type || '';
buttons.forEach((btn, index) => {
var cancelButtonKey;
var destructiveButtonKey;
buttons && buttons.forEach((btn, index) => {
callbacks[index] = btn.onPress;
if (btn.style == 'cancel') {
cancelButtonKey = String(index);
} else if (btn.style == 'destructive') {
destructiveButtonKey = String(index);
}
if (btn.text || index < (buttons || []).length - 1) {
var btnDef = {};
btnDef[index] = btn.text || DEFAULT_BUTTON_TEXT;
btnDef[index] = btn.text || '';
buttonsSpec.push(btnDef);
}
});
RCTAlertManager.alertWithArgs({
title,
message,
title: title || undefined,
message: message || undefined,
buttons: buttonsSpec,
type,
type: type || undefined,
cancelButtonKey,
destructiveButtonKey,
}, (id, value) => {
var cb = callbacks[id];
cb && cb(value);
@ -80,8 +91,9 @@ class AlertIOS {
title: string,
value?: string,
buttons?: Array<{
text: ?string;
text?: string;
onPress?: ?Function;
style?: AlertButtonStyle;
}>,
callback?: ?Function
): void {
@ -104,12 +116,7 @@ class AlertIOS {
);
if (!buttons) {
buttons = [{
text: 'Cancel',
}, {
text: 'OK',
onPress: callback
}];
buttons = [{ onPress: callback }];
}
this.alert(title, value, buttons, 'plain-text');
}

View File

@ -102,3 +102,6 @@ RCT_EXTERN BOOL RCTIsXCAssetURL(NSURL *imageURL);
// Converts a CGColor to a hex string
RCT_EXTERN NSString *RCTColorToHexString(CGColorRef color);
// Get standard localized string (if it exists)
RCT_EXTERN NSString *RCTUIKitLocalizedString(NSString *string);

View File

@ -575,3 +575,11 @@ NSString *RCTColorToHexString(CGColorRef color)
return [NSString stringWithFormat:@"#%02x%02x%02x", r, g, b];
}
}
// (https://github.com/0xced/XCDFormInputAccessoryView/blob/master/XCDFormInputAccessoryView/XCDFormInputAccessoryView.m#L10-L14)
RCT_EXTERN NSString *RCTUIKitLocalizedString(NSString *string)
{
NSBundle *UIKitBundle = [NSBundle bundleForClass:[UIApplication class]];
return UIKitBundle ? [UIKitBundle localizedStringForKey:string value:string table:nil] : string;
}

View File

@ -14,6 +14,17 @@
#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() <UIAlertViewDelegate>
@end
@ -50,58 +61,69 @@ RCT_EXPORT_MODULE()
* @"message": @"<Alert message>",
* @"buttons": @[
* @{@"<key1>": @"<title1>"},
* @{@"<key2>": @"<cancelButtonTitle>"},
* ]
* @{@"<key2>": @"<title2>"},
* ],
* @"cancelButtonKey": @"<key2>",
* }
* The key from the `buttons` dictionary is passed back in the callback on click.
* Buttons are displayed in the order they are specified. If "cancel" is used as
* the button key, it will be differently highlighted, according to iOS UI conventions.
* 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"]];
NSString *type = [RCTConvert NSString:args[@"type"]];
NSDictionaryArray *buttons = [RCTConvert NSDictionaryArray:args[@"buttons"]];
BOOL allowsTextInput = [type isEqual:@"plain-text"];
UIAlertViewStyle type = [RCTConvert UIAlertViewStyle:args[@"type"]];
NSArray<NSDictionary *> *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;
} else if (buttons.count == 0) {
RCTLogError(@"Must have at least one button.");
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.
BOOL preferAlertView = (!RCTRunningInAppExtension() && UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone);
// 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);
NSMutableArray<NSString *> *buttonKeys = [[NSMutableArray alloc] initWithCapacity:buttons.count];
if (allowsTextInput) {
alertView.alertViewStyle = UIAlertViewStylePlainTextInput;
[alertView textFieldAtIndex:0].text = message;
} else {
alertView.alertViewStyle = type;
alertView.message = message;
}
NSMutableArray<NSString *> *buttonKeys =
[[NSMutableArray alloc] initWithCapacity:buttons.count];
NSInteger index = 0;
for (NSDictionary *button in buttons) {
for (NSDictionary<NSString *, id> *button in buttons) {
if (button.count != 1) {
RCTLogError(@"Button definitions should have exactly one key.");
}
NSString *buttonKey = button.allKeys.firstObject;
NSString *buttonTitle = [button[buttonKey] description];
NSString *buttonTitle = [RCTConvert NSString:button[buttonKey]];
[alertView addButtonWithTitle:buttonTitle];
if ([buttonKey isEqualToString:@"cancel"]) {
alertView.cancelButtonIndex = index;
if ([buttonKey isEqualToString:cancelButtonKey]) {
alertView.cancelButtonIndex = buttonKeys.count;
}
[buttonKeys addObject:buttonKey];
index ++;
@ -129,8 +151,9 @@ RCT_EXPORT_METHOD(alertWithArgs:(NSDictionary *)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.
// 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;
}
@ -139,32 +162,63 @@ RCT_EXPORT_METHOD(alertWithArgs:(NSDictionary *)args
[UIAlertController alertControllerWithTitle:title
message:nil
preferredStyle:UIAlertControllerStyleAlert];
if (allowsTextInput) {
switch (type) {
case UIAlertViewStylePlainTextInput:
[alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) {
textField.text = message;
textField.secureTextEntry = NO;
}];
} else {
alertController.message = message;
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;
}
for (NSDictionary *button in buttons) {
alertController.message = message;
for (NSDictionary<NSString *, id> *button in buttons) {
if (button.count != 1) {
RCTLogError(@"Button definitions should have exactly one key.");
}
NSString *buttonKey = button.allKeys.firstObject;
NSString *buttonTitle = [button[buttonKey] description];
UIAlertActionStyle buttonStyle = [buttonKey isEqualToString:@"cancel"] ? UIAlertActionStyleCancel : UIAlertActionStyleDefault;
UITextField *textField = allowsTextInput ? alertController.textFields.firstObject : nil;
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) {
if (callback) {
if (allowsTextInput) {
callback(@[buttonKey, textField.text]);
} else {
callback(@[buttonKey]);
switch (type) {
case UIAlertViewStylePlainTextInput:
case UIAlertViewStyleSecureTextInput:
callback(@[buttonKey, [alertController.textFields.firstObject text]]);
break;
case UIAlertViewStyleLoginAndPasswordInput: {
NSDictionary<NSString *, NSString *> *loginCredentials = @{
@"login": [alertController.textFields.firstObject text],
@"password": [alertController.textFields.lastObject text]
};
callback(@[buttonKey, loginCredentials]);
break;
}
case UIAlertViewStyleDefault:
callback(@[buttonKey]);
break;
}
}]];
}
@ -188,10 +242,22 @@ RCT_EXPORT_METHOD(alertWithArgs:(NSDictionary *)args
RCTResponseSenderBlock callback = _alertCallbacks[index];
NSArray<NSString *> *buttonKeys = _alertButtonKeys[index];
if (alertView.alertViewStyle == UIAlertViewStylePlainTextInput) {
switch (alertView.alertViewStyle) {
case UIAlertViewStylePlainTextInput:
case UIAlertViewStyleSecureTextInput:
callback(@[buttonKeys[buttonIndex], [alertView textFieldAtIndex:0].text]);
} else {
break;
case UIAlertViewStyleLoginAndPasswordInput: {
NSDictionary<NSString *, NSString *> *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];