diff --git a/Examples/UIExplorer/AlertExample.js b/Examples/UIExplorer/AlertExample.js
new file mode 100644
index 000000000..702c49a6c
--- /dev/null
+++ b/Examples/UIExplorer/AlertExample.js
@@ -0,0 +1,137 @@
+/**
+ * The examples provided by Facebook are for non-commercial testing and
+ * evaluation purposes only.
+ *
+ * Facebook reserves all rights not expressly granted.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+ * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL
+ * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
+ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ */
+
+'use strict';
+
+var React = require('react-native');
+var {
+ Alert,
+ Platform,
+ StyleSheet,
+ Text,
+ TouchableHighlight,
+ View,
+} = React;
+
+var UIExplorerBlock = require('./UIExplorerBlock');
+
+// corporate ipsum > lorem ipsum
+var alertMessage = 'Credibly reintermediate next-generation potentialities after goal-oriented ' +
+ 'catalysts for change. Dynamically revolutionize.';
+
+/**
+ * Simple alert examples.
+ */
+var SimpleAlertExampleBlock = React.createClass({
+
+ render: function() {
+ return (
+
+ Alert.alert(
+ 'Alert Title',
+ alertMessage,
+ )}>
+
+ Alert with message and default button
+
+
+ Alert.alert(
+ 'Alert Title',
+ alertMessage,
+ [
+ {text: 'OK', onPress: () => console.log('OK Pressed!')},
+ ]
+ )}>
+
+ Alert with one button
+
+
+ Alert.alert(
+ 'Alert Title',
+ alertMessage,
+ [
+ {text: 'Cancel', onPress: () => console.log('Cancel Pressed!')},
+ {text: 'OK', onPress: () => console.log('OK Pressed!')},
+ ]
+ )}>
+
+ Alert with two buttons
+
+
+ Alert.alert(
+ 'Alert Title',
+ null,
+ [
+ {text: 'Foo', onPress: () => console.log('Foo Pressed!')},
+ {text: 'Bar', onPress: () => console.log('Bar Pressed!')},
+ {text: 'Baz', onPress: () => console.log('Baz Pressed!')},
+ ]
+ )}>
+
+ Alert with three buttons
+
+
+ Alert.alert(
+ 'Foo Title',
+ alertMessage,
+ '..............'.split('').map((dot, index) => ({
+ text: 'Button ' + index,
+ onPress: () => console.log('Pressed ' + index)
+ }))
+ )}>
+
+ Alert with too many buttons
+
+
+
+ );
+ },
+});
+
+var AlertExample = React.createClass({
+ statics: {
+ title: 'Alert',
+ description: 'Alerts display a concise and informative message ' +
+ 'and prompt the user to make a decision.',
+ },
+ render: function() {
+ return (
+
+
+
+ );
+ }
+});
+
+var styles = StyleSheet.create({
+ wrapper: {
+ borderRadius: 5,
+ marginBottom: 5,
+ },
+ button: {
+ backgroundColor: '#eeeeee',
+ padding: 10,
+ },
+});
+
+module.exports = {
+ AlertExample,
+ SimpleAlertExampleBlock,
+}
diff --git a/Examples/UIExplorer/AlertIOSExample.js b/Examples/UIExplorer/AlertIOSExample.js
index 8666d5ba9..7532e4151 100644
--- a/Examples/UIExplorer/AlertIOSExample.js
+++ b/Examples/UIExplorer/AlertIOSExample.js
@@ -24,77 +24,15 @@ var {
AlertIOS,
} = React;
+var { SimpleAlertExampleBlock } = require('./AlertExample');
+
exports.framework = 'React';
exports.title = 'AlertIOS';
exports.description = 'iOS alerts and action sheets';
exports.examples = [{
title: 'Alerts',
render() {
- return (
-
- AlertIOS.alert(
- 'Foo Title',
- 'My Alert Msg'
- )}>
-
- Alert with message and default button
-
-
- AlertIOS.alert(
- 'Foo Title',
- null,
- [
- {text: 'Button', onPress: () => console.log('Button Pressed!')},
- ]
- )}>
-
- Alert with only one button
-
-
- AlertIOS.alert(
- 'Foo Title',
- 'My Alert Msg',
- [
- {text: 'Foo', onPress: () => console.log('Foo Pressed!')},
- {text: 'Bar', onPress: () => console.log('Bar Pressed!')},
- ]
- )}>
-
- Alert with two buttons
-
-
- AlertIOS.alert(
- 'Foo Title',
- null,
- [
- {text: 'Foo', onPress: () => console.log('Foo Pressed!')},
- {text: 'Bar', onPress: () => console.log('Bar Pressed!')},
- {text: 'Baz', onPress: () => console.log('Baz Pressed!')},
- ]
- )}>
-
- Alert with 3 buttons
-
-
- AlertIOS.alert(
- 'Foo Title',
- 'My Alert Msg',
- '..............'.split('').map((dot, index) => ({
- text: 'Button ' + index,
- onPress: () => console.log('Pressed ' + index)
- }))
- )}>
-
- Alert with too many buttons
-
-
-
- );
+ return ;
}
},
{
diff --git a/Examples/UIExplorer/UIExplorerList.android.js b/Examples/UIExplorer/UIExplorerList.android.js
index 36746c558..e3d05a2b1 100644
--- a/Examples/UIExplorer/UIExplorerList.android.js
+++ b/Examples/UIExplorer/UIExplorerList.android.js
@@ -38,6 +38,7 @@ var COMPONENTS = [
var APIS = [
require('./AccessibilityAndroidExample.android'),
+ require('./AlertExample').AlertExample,
require('./BorderExample'),
require('./ClipboardExample'),
require('./GeolocationExample'),
diff --git a/Libraries/Utilities/Alert.js b/Libraries/Utilities/Alert.js
new file mode 100644
index 000000000..de434c987
--- /dev/null
+++ b/Libraries/Utilities/Alert.js
@@ -0,0 +1,126 @@
+/**
+ * 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.
+ *
+ * @providesModule Alert
+ * @flow
+ */
+'use strict';
+
+var AlertIOS = require('AlertIOS');
+var Platform = require('Platform');
+var DialogModuleAndroid = require('NativeModules').DialogManagerAndroid;
+
+import type { AlertType, AlertButtonStyle } from 'AlertIOS';
+
+type Buttons = Array<{
+ text?: string;
+ onPress?: ?Function;
+ style?: AlertButtonStyle;
+}>;
+
+/**
+ * Launches an alert dialog with the specified title and message.
+ *
+ * Optionally provide a list of buttons. Tapping any button will fire the
+ * 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.
+ *
+ * ## iOS
+ *
+ * On iOS you can specify any number of buttons. Each button can optionally
+ * specify a style and you can also specify type of the alert.
+ * Refer to `AlertIOS` for details.
+ *
+ * ## Android
+ *
+ * On Android at most three buttons can be specified. Android has a concept
+ * of a 'neutral', 'negative' and a 'positive' button:
+ *
+ * - If you specify one button, it will be the 'positive' one (such as 'OK')
+ * - Two buttons mean 'negative', 'positive' (such as 'Cancel', 'OK')
+ * - Three buttons mean 'neutral', 'negative', 'positive' (such as 'Later', 'Cancel', 'OK')
+ *
+ * ```
+ * Alert.alert(
+ * 'Alert Title',
+ * 'My Alert Msg',
+ * [
+ * {text: 'Ask me later', onPress: () => console.log('Ask me later pressed')},
+ * {text: 'Cancel', onPress: () => console.log('Cancel Pressed')},
+ * {text: 'OK', onPress: () => console.log('OK Pressed')},
+ * ]
+ * )
+ * ```
+ */
+class Alert {
+
+ static alert(
+ title: ?string,
+ message?: ?string,
+ buttons?: Buttons,
+ type?: AlertType
+ ): void {
+ if (Platform.OS === 'ios') {
+ AlertIOS.alert(title, message, buttons, type);
+ } else if (Platform.OS === 'android') {
+ AlertAndroid.alert(title, message, buttons);
+ }
+ }
+}
+
+/**
+ * Wrapper around the Android native module.
+ */
+class AlertAndroid {
+
+ static alert(
+ title: ?string,
+ message?: ?string,
+ buttons?: Buttons,
+ ): void {
+ var config = {
+ title: title || '',
+ message: message || '',
+ };
+ // At most three buttons (neutral, negative, positive). Ignore rest.
+ // The text 'OK' should be probably localized. iOS Alert does that in native.
+ var validButtons: Buttons = buttons ? buttons.slice(0, 3) : [{text: 'OK'}];
+ var buttonPositive = validButtons.pop();
+ var buttonNegative = validButtons.pop();
+ var buttonNeutral = validButtons.pop();
+ if (buttonNeutral) {
+ config = {...config, buttonNeutral: buttonNeutral.text || '' }
+ }
+ if (buttonNegative) {
+ config = {...config, buttonNegative: buttonNegative.text || '' }
+ }
+ if (buttonPositive) {
+ config = {...config, buttonPositive: buttonPositive.text || '' }
+ }
+ DialogModuleAndroid.showAlert(
+ config,
+ (errorMessage) => console.warn(message),
+ (action, buttonKey) => {
+ if (action !== DialogModuleAndroid.buttonClicked) {
+ return;
+ }
+ if (buttonKey === DialogModuleAndroid.buttonNeutral) {
+ buttonNeutral.onPress && buttonNeutral.onPress();
+ } else if (buttonKey === DialogModuleAndroid.buttonNegative) {
+ buttonNegative.onPress && buttonNegative.onPress();
+ } else if (buttonKey === DialogModuleAndroid.buttonPositive) {
+ buttonPositive.onPress && buttonPositive.onPress();
+ }
+ }
+ );
+ }
+}
+
+module.exports = Alert;
diff --git a/Libraries/Utilities/AlertIOS.js b/Libraries/Utilities/AlertIOS.js
index 2bd1a3223..38fd0f88c 100644
--- a/Libraries/Utilities/AlertIOS.js
+++ b/Libraries/Utilities/AlertIOS.js
@@ -14,14 +14,14 @@
var RCTAlertManager = require('NativeModules').AlertManager;
var invariant = require('invariant');
-type AlertType = $Enum<{
+export type AlertType = $Enum<{
'default': string;
'plain-text': string;
'secure-text': string;
'login-password': string;
}>;
-type AlertButtonStyle = $Enum<{
+export type AlertButtonStyle = $Enum<{
'default': string;
'cancel': string;
'destructive': string;
@@ -32,20 +32,19 @@ type AlertButtonStyle = $Enum<{
*
* Optionally provide a list of buttons. Tapping any button will fire the
* respective onPress callback and dismiss the alert. By default, the only
- * button will be an 'OK' button
+ * button will be an 'OK' button.
*
* ```
* AlertIOS.alert(
* 'Foo Title',
* 'My Alert Msg',
* [
- * {text: 'OK', onPress: () => console.log('OK Pressed!')},
- * {text: 'Cancel', onPress: () => console.log('Cancel Pressed!'), style: 'cancel'},
+ * {text: 'OK', onPress: () => console.log('OK Pressed')},
+ * {text: 'Cancel', onPress: () => console.log('Cancel Pressed'), style: 'cancel'},
* ]
* )
* ```
*/
-
class AlertIOS {
static alert(
title: ?string,
diff --git a/Libraries/react-native/react-native.js b/Libraries/react-native/react-native.js
index be3dfd26a..130c4439a 100644
--- a/Libraries/react-native/react-native.js
+++ b/Libraries/react-native/react-native.js
@@ -51,6 +51,7 @@ var ReactNative = {
// APIs
get ActionSheetIOS() { return require('ActionSheetIOS'); },
get AdSupportIOS() { return require('AdSupportIOS'); },
+ get Alert() { return require('Alert'); },
get AlertIOS() { return require('AlertIOS'); },
get Animated() { return require('Animated'); },
get AppRegistry() { return require('AppRegistry'); },
diff --git a/ReactAndroid/build.gradle b/ReactAndroid/build.gradle
index 464a2ad18..850d2f41f 100644
--- a/ReactAndroid/build.gradle
+++ b/ReactAndroid/build.gradle
@@ -257,7 +257,7 @@ dependencies {
testCompile "org.powermock:powermock-classloading-xstream:${POWERMOCK_VERSION}"
testCompile "org.mockito:mockito-core:${MOCKITO_CORE_VERSION}"
testCompile "org.easytesting:fest-assert-core:${FEST_ASSERT_CORE_VERSION}"
- testCompile("org.robolectric:robolectric:${ROBOLECTRIC_VERSION}")
+ testCompile "org.robolectric:robolectric:${ROBOLECTRIC_VERSION}"
}
apply from: 'release.gradle'
diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java b/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java
index e8793d241..17fb538b8 100644
--- a/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java
+++ b/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java
@@ -186,7 +186,7 @@ public abstract class ReactInstanceManager {
/**
* Path to the JS bundle file to be loaded from the file system.
*
- * Example: {@code "assets://index.android.js" or "/sdcard/main.jsbundle}
+ * Example: {@code "assets://index.android.js" or "/sdcard/main.jsbundle"}
*/
public Builder setJSBundleFile(String jsBundleFile) {
mJSBundleFile = jsBundleFile;
diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/dialog/AlertFragment.java b/ReactAndroid/src/main/java/com/facebook/react/modules/dialog/AlertFragment.java
new file mode 100644
index 000000000..04d6d421a
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/modules/dialog/AlertFragment.java
@@ -0,0 +1,77 @@
+/**
+ * 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.
+ */
+
+package com.facebook.react.modules.dialog;
+
+import javax.annotation.Nullable;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.content.DialogInterface;
+import android.content.Context;
+import android.os.Bundle;
+
+/**
+ * A fragment used to display the dialog.
+ */
+/* package */ class AlertFragment extends DialogFragment implements DialogInterface.OnClickListener {
+
+ /* package */ static final String ARG_TITLE = "title";
+ /* package */ static final String ARG_MESSAGE = "message";
+ /* package */ static final String ARG_BUTTON_POSITIVE = "button_positive";
+ /* package */ static final String ARG_BUTTON_NEGATIVE = "button_negative";
+ /* package */ static final String ARG_BUTTON_NEUTRAL = "button_neutral";
+
+ private final @Nullable DialogModule.AlertFragmentListener mListener;
+
+ public AlertFragment(@Nullable DialogModule.AlertFragmentListener listener, Bundle arguments) {
+ mListener = listener;
+ setArguments(arguments);
+ }
+
+ public static Dialog createDialog(
+ Context activityContext, Bundle arguments, DialogInterface.OnClickListener fragment) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(activityContext)
+ .setTitle(arguments.getString(ARG_TITLE))
+ .setMessage(arguments.getString(ARG_MESSAGE));
+
+ if (arguments.containsKey(ARG_BUTTON_POSITIVE)) {
+ builder.setPositiveButton(arguments.getString(ARG_BUTTON_POSITIVE), fragment);
+ }
+ if (arguments.containsKey(ARG_BUTTON_NEGATIVE)) {
+ builder.setNegativeButton(arguments.getString(ARG_BUTTON_NEGATIVE), fragment);
+ }
+ if (arguments.containsKey(ARG_BUTTON_NEUTRAL)) {
+ builder.setNeutralButton(arguments.getString(ARG_BUTTON_NEUTRAL), fragment);
+ }
+
+ return builder.create();
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ return createDialog(getActivity(), getArguments(), this);
+ }
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ if (mListener != null) {
+ mListener.onClick(dialog, which);
+ }
+ }
+
+ @Override
+ public void onDismiss(DialogInterface dialog) {
+ super.onDismiss(dialog);
+ if (mListener != null) {
+ mListener.onDismiss(dialog);
+ }
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/dialog/DialogModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/dialog/DialogModule.java
new file mode 100644
index 000000000..10dfbc8d4
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/modules/dialog/DialogModule.java
@@ -0,0 +1,255 @@
+/**
+ * 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.
+ */
+
+package com.facebook.react.modules.dialog;
+
+import javax.annotation.Nullable;
+
+import java.util.Map;
+
+import android.app.Activity;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.DialogInterface.OnDismissListener;
+import android.os.Bundle;
+import android.support.v4.app.FragmentActivity;
+
+import com.facebook.infer.annotation.Assertions;
+import com.facebook.react.bridge.Callback;
+import com.facebook.react.bridge.LifecycleEventListener;
+import com.facebook.react.bridge.ReactApplicationContext;
+import com.facebook.react.bridge.ReactContextBaseJavaModule;
+import com.facebook.react.bridge.ReactMethod;
+import com.facebook.react.bridge.ReadableMap;
+import com.facebook.react.common.MapBuilder;
+
+public class DialogModule extends ReactContextBaseJavaModule implements LifecycleEventListener {
+
+ /* package */ static final String FRAGMENT_TAG =
+ "com.facebook.catalyst.react.dialog.DialogModule";
+
+ /* package */ static final String NAME = "DialogManagerAndroid";
+
+ /* package */ static final String ACTION_BUTTON_CLICKED = "buttonClicked";
+ /* package */ static final String ACTION_DISMISSED = "dismissed";
+ /* package */ static final String KEY_TITLE = "title";
+ /* package */ static final String KEY_MESSAGE = "message";
+ /* package */ static final String KEY_BUTTON_POSITIVE = "buttonPositive";
+ /* package */ static final String KEY_BUTTON_NEGATIVE = "buttonNegative";
+ /* package */ static final String KEY_BUTTON_NEUTRAL = "buttonNeutral";
+
+ /* package */ static final Map CONSTANTS = MapBuilder.of(
+ ACTION_BUTTON_CLICKED, ACTION_BUTTON_CLICKED,
+ ACTION_DISMISSED, ACTION_DISMISSED,
+ KEY_BUTTON_POSITIVE, DialogInterface.BUTTON_POSITIVE,
+ KEY_BUTTON_NEGATIVE, DialogInterface.BUTTON_NEGATIVE,
+ KEY_BUTTON_NEUTRAL, DialogInterface.BUTTON_NEUTRAL);
+
+ private boolean mIsInForeground;
+
+ public DialogModule(ReactApplicationContext reactContext) {
+ super(reactContext);
+ }
+
+ @Override
+ public String getName() {
+ return NAME;
+ }
+
+ /**
+ * Helper to allow this module to work with both the standard FragmentManager
+ * and the Support FragmentManager (for apps that need to use it for legacy reasons).
+ * Since the two APIs don't share a common interface there's unfortunately some
+ * code duplication.
+ */
+ private class FragmentManagerHelper {
+
+ // Exactly one of the two is null
+ private final @Nullable android.app.FragmentManager mFragmentManager;
+ private final @Nullable android.support.v4.app.FragmentManager mSupportFragmentManager;
+
+ private @Nullable Object mFragmentToShow;
+
+ private boolean isUsingSupportLibrary() {
+ return mSupportFragmentManager != null;
+ }
+
+ public FragmentManagerHelper(android.support.v4.app.FragmentManager supportFragmentManager) {
+ mFragmentManager = null;
+ mSupportFragmentManager = supportFragmentManager;
+ }
+ public FragmentManagerHelper(android.app.FragmentManager fragmentManager) {
+ mFragmentManager = fragmentManager;
+ mSupportFragmentManager = null;
+ }
+
+ public void showPendingAlert() {
+ if (mFragmentToShow == null) {
+ return;
+ }
+ if (isUsingSupportLibrary()) {
+ ((SupportAlertFragment) mFragmentToShow).show(mSupportFragmentManager, FRAGMENT_TAG);
+ } else {
+ ((AlertFragment) mFragmentToShow).show(mFragmentManager, FRAGMENT_TAG);
+ }
+ mFragmentToShow = null;
+ }
+
+ private void dismissExisting() {
+ if (isUsingSupportLibrary()) {
+ SupportAlertFragment oldFragment =
+ (SupportAlertFragment) mSupportFragmentManager.findFragmentByTag(FRAGMENT_TAG);
+ if (oldFragment != null) {
+ oldFragment.dismiss();
+ }
+ } else {
+ AlertFragment oldFragment =
+ (AlertFragment) mFragmentManager.findFragmentByTag(FRAGMENT_TAG);
+ if (oldFragment != null) {
+ oldFragment.dismiss();
+ }
+ }
+ }
+
+ public void showNewAlert(boolean isInForeground, Bundle arguments, Callback actionCallback) {
+ dismissExisting();
+
+ AlertFragmentListener actionListener =
+ actionCallback != null ? new AlertFragmentListener(actionCallback) : null;
+
+ if (isUsingSupportLibrary()) {
+ SupportAlertFragment alertFragment = new SupportAlertFragment(actionListener, arguments);
+ if (isInForeground) {
+ alertFragment.show(mSupportFragmentManager, FRAGMENT_TAG);
+ } else {
+ mFragmentToShow = alertFragment;
+ }
+ } else {
+ AlertFragment alertFragment = new AlertFragment(actionListener, arguments);
+ if (isInForeground) {
+ alertFragment.show(mFragmentManager, FRAGMENT_TAG);
+ } else {
+ mFragmentToShow = alertFragment;
+ }
+ }
+ }
+ }
+
+ /* package */ class AlertFragmentListener implements OnClickListener, OnDismissListener {
+
+ private final Callback mCallback;
+ private boolean mCallbackConsumed = false;
+
+ public AlertFragmentListener(Callback callback) {
+ mCallback = callback;
+ }
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ if (!mCallbackConsumed) {
+ if (getReactApplicationContext().hasActiveCatalystInstance()) {
+ mCallback.invoke(ACTION_BUTTON_CLICKED, which);
+ mCallbackConsumed = true;
+ }
+ }
+ }
+
+ @Override
+ public void onDismiss(DialogInterface dialog) {
+ if (!mCallbackConsumed) {
+ if (getReactApplicationContext().hasActiveCatalystInstance()) {
+ mCallback.invoke(ACTION_DISMISSED);
+ mCallbackConsumed = true;
+ }
+ }
+ }
+ }
+
+ @Override
+ public Map getConstants() {
+ return CONSTANTS;
+ }
+
+ @Override
+ public void initialize() {
+ getReactApplicationContext().addLifecycleEventListener(this);
+ }
+
+ @Override
+ public void onHostPause() {
+ // Don't show the dialog if the host is paused.
+ mIsInForeground = false;
+ }
+
+ @Override
+ public void onHostDestroy() {
+ }
+
+ @Override
+ public void onHostResume() {
+ mIsInForeground = true;
+ // Check if a dialog has been created while the host was paused, so that we can show it now.
+ FragmentManagerHelper fragmentManagerHelper = getFragmentManagerHelper();
+ Assertions.assertNotNull(
+ fragmentManagerHelper,
+ "Attached DialogModule to host with pending alert but no FragmentManager " +
+ "(not attached to an Activity).");
+ fragmentManagerHelper.showPendingAlert();
+ }
+
+ @ReactMethod
+ public void showAlert(
+ ReadableMap options,
+ Callback errorCallback,
+ Callback actionCallback) {
+ FragmentManagerHelper fragmentManagerHelper = getFragmentManagerHelper();
+ if (fragmentManagerHelper == null) {
+ errorCallback.invoke("Tried to show an alert while not attached to an Activity");
+ return;
+ }
+
+ final Bundle args = new Bundle();
+ if (options.hasKey(KEY_TITLE)) {
+ args.putString(AlertFragment.ARG_TITLE, options.getString(KEY_TITLE));
+ }
+ if (options.hasKey(KEY_MESSAGE)) {
+ args.putString(AlertFragment.ARG_MESSAGE, options.getString(KEY_MESSAGE));
+ }
+ if (options.hasKey(KEY_BUTTON_POSITIVE)) {
+ args.putString(AlertFragment.ARG_BUTTON_POSITIVE, options.getString(KEY_BUTTON_POSITIVE));
+ }
+ if (options.hasKey(KEY_BUTTON_NEGATIVE)) {
+ args.putString(AlertFragment.ARG_BUTTON_NEGATIVE, options.getString(KEY_BUTTON_NEGATIVE));
+ }
+ if (options.hasKey(KEY_BUTTON_NEUTRAL)) {
+ args.putString(AlertFragment.ARG_BUTTON_NEUTRAL, options.getString(KEY_BUTTON_NEUTRAL));
+ }
+
+ fragmentManagerHelper.showNewAlert(mIsInForeground, args, actionCallback);
+ }
+
+ /**
+ * Creates a new helper to work with either the FragmentManager or the legacy support
+ * FragmentManager transparently. Returns null if we're not attached to an Activity.
+ *
+ * DO NOT HOLD LONG-LIVED REFERENCES TO THE OBJECT RETURNED BY THIS METHOD, AS THIS WILL CAUSE
+ * MEMORY LEAKS.
+ */
+ private @Nullable FragmentManagerHelper getFragmentManagerHelper() {
+ Activity activity = getCurrentActivity();
+ if (activity == null) {
+ return null;
+ }
+ if (activity instanceof FragmentActivity) {
+ return new FragmentManagerHelper(((FragmentActivity) activity).getSupportFragmentManager());
+ } else {
+ return new FragmentManagerHelper(activity.getFragmentManager());
+ }
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/dialog/SupportAlertFragment.java b/ReactAndroid/src/main/java/com/facebook/react/modules/dialog/SupportAlertFragment.java
new file mode 100644
index 000000000..fb987cd05
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/modules/dialog/SupportAlertFragment.java
@@ -0,0 +1,52 @@
+/**
+ * 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.
+ */
+
+package com.facebook.react.modules.dialog;
+
+import javax.annotation.Nullable;
+
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.os.Bundle;
+
+import android.support.v4.app.DialogFragment;
+
+/**
+ * {@link AlertFragment} for apps that use the Support FragmentActivity and FragmentManager
+ * for legacy reasons.
+ */
+/* package */ class SupportAlertFragment extends DialogFragment implements DialogInterface.OnClickListener {
+
+ private final @Nullable DialogModule.AlertFragmentListener mListener;
+
+ public SupportAlertFragment(@Nullable DialogModule.AlertFragmentListener listener, Bundle arguments) {
+ mListener = listener;
+ setArguments(arguments);
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ return AlertFragment.createDialog(getActivity(), getArguments(), this);
+ }
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ if (mListener != null) {
+ mListener.onClick(dialog, which);
+ }
+ }
+
+ @Override
+ public void onDismiss(DialogInterface dialog) {
+ super.onDismiss(dialog);
+ if (mListener != null) {
+ mListener.onDismiss(dialog);
+ }
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java b/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java
index d6d8e37fb..aae0e9322 100644
--- a/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java
+++ b/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java
@@ -17,6 +17,7 @@ import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.JavaScriptModule;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
+import com.facebook.react.modules.dialog.DialogModule;
import com.facebook.react.modules.fresco.FrescoModule;
import com.facebook.react.modules.intent.IntentModule;
import com.facebook.react.modules.location.LocationModule;
@@ -53,6 +54,7 @@ public class MainReactPackage implements ReactPackage {
return Arrays.asList(
new AsyncStorageModule(reactContext),
new ClipboardModule(reactContext),
+ new DialogModule(reactContext),
new FrescoModule(reactContext),
new IntentModule(reactContext),
new LocationModule(reactContext),
diff --git a/ReactAndroid/src/test/java/com/facebook/react/modules/dialog/DialogModuleTest.java b/ReactAndroid/src/test/java/com/facebook/react/modules/dialog/DialogModuleTest.java
new file mode 100644
index 000000000..f0ae5dc3a
--- /dev/null
+++ b/ReactAndroid/src/test/java/com/facebook/react/modules/dialog/DialogModuleTest.java
@@ -0,0 +1,170 @@
+/**
+ * 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.
+ */
+
+package com.facebook.react.modules.dialog;
+
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.app.Activity;
+
+import com.facebook.react.bridge.Callback;
+import com.facebook.react.bridge.ReactApplicationContext;
+import com.facebook.react.bridge.SimpleMap;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.powermock.api.mockito.PowerMockito;
+import org.powermock.core.classloader.annotations.PowerMockIgnore;
+import org.robolectric.Robolectric;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.util.ActivityController;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+@RunWith(RobolectricTestRunner.class)
+@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*"})
+public class DialogModuleTest {
+
+ private ActivityController mActivityController;
+ private Activity mActivity;
+ private DialogModule mDialogModule;
+
+ final static class SimpleCallback implements Callback {
+ private Object[] mArgs;
+ private int mCalls;
+
+ @Override
+ public void invoke(Object... args) {
+ mCalls++;
+ mArgs = args;
+ }
+
+ public int getCalls() {
+ return mCalls;
+ }
+
+ public Object[] getArgs() {
+ return mArgs;
+ }
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ mActivityController = Robolectric.buildActivity(Activity.class);
+ mActivity = mActivityController
+ .create()
+ .start()
+ .resume()
+ .get();
+
+ final ReactApplicationContext context = PowerMockito.mock(ReactApplicationContext.class);
+ PowerMockito.when(context.hasActiveCatalystInstance()).thenReturn(true);
+ PowerMockito.when(context, "getCurrentActivity").thenReturn(mActivity);
+
+ mDialogModule = new DialogModule(context);
+ mDialogModule.onHostResume();
+ }
+
+ @After
+ public void tearDown() {
+ mActivityController.pause().stop().destroy();
+
+ mActivityController = null;
+ mDialogModule = null;
+ }
+
+ @Test
+ public void testAllOptions() {
+ final SimpleMap options = new SimpleMap();
+ options.putString("title", "Title");
+ options.putString("message", "Message");
+ options.putString("buttonPositive", "OK");
+ options.putString("buttonNegative", "Cancel");
+ options.putString("buttonNeutral", "Later");
+
+ mDialogModule.showAlert(options, null, null);
+
+ final AlertFragment fragment = getFragment();
+ assertNotNull("Fragment was not displayed", fragment);
+
+ final AlertDialog dialog = (AlertDialog) fragment.getDialog();
+ assertEquals("OK", dialog.getButton(DialogInterface.BUTTON_POSITIVE).getText().toString());
+ assertEquals("Cancel", dialog.getButton(DialogInterface.BUTTON_NEGATIVE).getText().toString());
+ assertEquals("Later", dialog.getButton(DialogInterface.BUTTON_NEUTRAL).getText().toString());
+ }
+
+ @Test
+ public void testCallbackPositive() {
+ final SimpleMap options = new SimpleMap();
+ options.putString("buttonPositive", "OK");
+
+ final SimpleCallback actionCallback = new SimpleCallback();
+ mDialogModule.showAlert(options, null, actionCallback);
+
+ final AlertDialog dialog = (AlertDialog) getFragment().getDialog();
+ dialog.getButton(DialogInterface.BUTTON_POSITIVE).performClick();
+
+ assertEquals(1, actionCallback.getCalls());
+ assertEquals(DialogModule.ACTION_BUTTON_CLICKED, actionCallback.getArgs()[0]);
+ assertEquals(DialogInterface.BUTTON_POSITIVE, actionCallback.getArgs()[1]);
+ }
+
+ @Test
+ public void testCallbackNegative() {
+ final SimpleMap options = new SimpleMap();
+ options.putString("buttonNegative", "Cancel");
+
+ final SimpleCallback actionCallback = new SimpleCallback();
+ mDialogModule.showAlert(options, null, actionCallback);
+
+ final AlertDialog dialog = (AlertDialog) getFragment().getDialog();
+ dialog.getButton(DialogInterface.BUTTON_NEGATIVE).performClick();
+
+ assertEquals(1, actionCallback.getCalls());
+ assertEquals(DialogModule.ACTION_BUTTON_CLICKED, actionCallback.getArgs()[0]);
+ assertEquals(DialogInterface.BUTTON_NEGATIVE, actionCallback.getArgs()[1]);
+ }
+
+ @Test
+ public void testCallbackNeutral() {
+ final SimpleMap options = new SimpleMap();
+ options.putString("buttonNeutral", "Later");
+
+ final SimpleCallback actionCallback = new SimpleCallback();
+ mDialogModule.showAlert(options, null, actionCallback);
+
+ final AlertDialog dialog = (AlertDialog) getFragment().getDialog();
+ dialog.getButton(DialogInterface.BUTTON_NEUTRAL).performClick();
+
+ assertEquals(1, actionCallback.getCalls());
+ assertEquals(DialogModule.ACTION_BUTTON_CLICKED, actionCallback.getArgs()[0]);
+ assertEquals(DialogInterface.BUTTON_NEUTRAL, actionCallback.getArgs()[1]);
+ }
+
+ @Test
+ public void testCallbackDismiss() {
+ final SimpleMap options = new SimpleMap();
+
+ final SimpleCallback actionCallback = new SimpleCallback();
+ mDialogModule.showAlert(options, null, actionCallback);
+
+ getFragment().getDialog().dismiss();
+
+ assertEquals(1, actionCallback.getCalls());
+ assertEquals(DialogModule.ACTION_DISMISSED, actionCallback.getArgs()[0]);
+ }
+
+ private AlertFragment getFragment() {
+ return (AlertFragment) mActivity.getFragmentManager()
+ .findFragmentByTag(DialogModule.FRAGMENT_TAG);
+ }
+}